@liase/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,123 @@
1
+ import { Command, Option } from "commander";
2
+ import open from "open";
3
+ import { getLiaseDetailsFromArgs, getRequestFromArgs, getSharedLiaseOptions, } from "../lib/liase-details.js";
4
+ import { getLiaseQuery } from "../lib/liase-query.js";
5
+ import { startProxyServer } from "../lib/proxy.js";
6
+ import { zodSchemaToSimpleSchema } from "../lib/zod.js";
7
+ export async function getRunCommand() {
8
+ const runCommand = new Command();
9
+ const liaseDetails = await getLiaseDetailsFromArgs();
10
+ const { sourceOption, requestHandlerOption, pluginsOption, cacheNetworkRequestsOption, secretsSetOption, } = getSharedLiaseOptions(liaseDetails);
11
+ runCommand
12
+ .name("run")
13
+ .addOption(sourceOption)
14
+ .addOption(requestHandlerOption)
15
+ .addOption(pluginsOption)
16
+ .addOption(cacheNetworkRequestsOption)
17
+ .addOption(secretsSetOption)
18
+ .addOption(new Option("-f, --outputFormat <output format>", `"JSON" will format the output as JSON, "pretty" will format the output in a more human readable way with syntax highlighting, ` +
19
+ `"online" will open a webpage with the results visible. Default is "pretty" unless output is being piped in which case the default is "json".`)
20
+ .choices(["json", "pretty", "online"])
21
+ .default(process.stdout.isTTY ? "pretty" : "json"))
22
+ .action(async (options) => {
23
+ const { outputFormat, cacheNetworkRequests, secretsSet } = options;
24
+ const request = getRequestFromArgs(options, liaseDetails.requestHandler);
25
+ const query = await getLiaseQuery({
26
+ request,
27
+ loadPluginsFromArgs: true,
28
+ cacheNetworkRequests,
29
+ secretsSet,
30
+ });
31
+ const response = await query.getNext();
32
+ if (response === null) {
33
+ throw Error("No response received");
34
+ }
35
+ if (outputFormat === "pretty") {
36
+ console.dir(response, { depth: null });
37
+ }
38
+ else if (outputFormat === "json") {
39
+ console.log(JSON.stringify(response, null, 2));
40
+ }
41
+ else if (outputFormat === "online") {
42
+ const { origin: proxyOrigin } = await startProxyServer();
43
+ for (const media of response.media || []) {
44
+ for (const file of media.files || []) {
45
+ file.url = `${proxyOrigin}/${file.url}`;
46
+ }
47
+ }
48
+ const res = await fetch("https://mediafinderviewer.cals.cafe/api/output", {
49
+ method: "POST",
50
+ headers: {
51
+ "Content-Type": "application/json",
52
+ },
53
+ body: JSON.stringify(response),
54
+ });
55
+ const data = await res.json();
56
+ if (data &&
57
+ typeof data === "object" &&
58
+ "viewerUrl" in data &&
59
+ typeof data.viewerUrl === "string") {
60
+ open(data.viewerUrl);
61
+ }
62
+ else {
63
+ throw Error("Invalid response for server");
64
+ }
65
+ }
66
+ else {
67
+ throw Error(`Unknown output format "${outputFormat}"`);
68
+ }
69
+ });
70
+ const { requestHandler } = liaseDetails;
71
+ if (requestHandler) {
72
+ const simpleSchema = zodSchemaToSimpleSchema(requestHandler.requestSchema);
73
+ if (simpleSchema.type !== "object") {
74
+ throw Error("Internal error: Request schema was not an object");
75
+ }
76
+ const requestOpts = Object.entries(simpleSchema.children);
77
+ for (const [name, requestOption] of requestOpts) {
78
+ if (["source", "queryType"].includes(name))
79
+ continue;
80
+ let valueType;
81
+ let valueParser = undefined;
82
+ let choices = undefined;
83
+ if (Array.isArray(requestOption.type)) {
84
+ if (requestOption.type.every((subtype) => subtype.type === "literal")) {
85
+ choices = requestOption.type.map((unionSubtype) => String(unionSubtype.value));
86
+ // Add all subtypes to a set to work out the list of unique subtypes
87
+ const unionSubtypes = new Set(requestOption.type.map((subtype) => subtype.valueType));
88
+ valueType = [...unionSubtypes].join(" | ");
89
+ }
90
+ else {
91
+ // TODO: Deal with this case
92
+ valueType = "unknown";
93
+ }
94
+ }
95
+ else {
96
+ valueType = requestOption.type;
97
+ }
98
+ if (valueType === "number") {
99
+ valueParser = Number.parseFloat;
100
+ }
101
+ const flagDetails = valueType === "boolean" ? `--${name}` : `--${name} <${valueType}>`;
102
+ const description = [
103
+ requestOption.description,
104
+ requestOption.optional ? undefined : "(required)",
105
+ ]
106
+ .filter((part) => part)
107
+ .join(" ");
108
+ const option = new Option(flagDetails, description);
109
+ if (requestOption.default !== undefined) {
110
+ option.default(requestOption.default);
111
+ }
112
+ option.makeOptionMandatory(!requestOption.optional);
113
+ if (valueParser) {
114
+ option.argParser(valueParser);
115
+ }
116
+ if (choices) {
117
+ option.choices(choices);
118
+ }
119
+ runCommand.addOption(option);
120
+ }
121
+ }
122
+ return runCommand;
123
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function getShowSchemaCommand(): Promise<Command>;
3
+ //# sourceMappingURL=show-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"show-schema.d.ts","sourceRoot":"","sources":["../../src/subcommands/show-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAS5C,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC,CA6C7D"}
@@ -0,0 +1,44 @@
1
+ import { Command, Option } from "commander";
2
+ import { z } from "zod";
3
+ import { getLiaseDetailsFromArgs, getSharedLiaseOptions, } from "../lib/liase-details.js";
4
+ import { getLiaseQuery } from "../lib/liase-query.js";
5
+ import { zodSchemaToSimpleSchema } from "../lib/zod.js";
6
+ export async function getShowSchemaCommand() {
7
+ const showSchemaCommand = new Command();
8
+ const liaseDetails = await getLiaseDetailsFromArgs();
9
+ const { sourceOption, requestHandlerOption, pluginsOption } = getSharedLiaseOptions(liaseDetails);
10
+ const { requestHandler } = liaseDetails;
11
+ showSchemaCommand
12
+ .name("show-schema")
13
+ .addOption(sourceOption)
14
+ .addOption(requestHandlerOption)
15
+ .addOption(new Option("-t, --schemaType <schemaType>", 'Type of schema to return. If type is "response" then any required request options must be given in order to determine which response schema will be returned')
16
+ .choices(["request", "secrets", "response"])
17
+ .default("response"))
18
+ .action(async (options) => {
19
+ if (!requestHandler) {
20
+ throw Error("Internal error: Trying to show schema without request handler being set first");
21
+ }
22
+ let schema;
23
+ if (options.schemaType === "request") {
24
+ schema = requestHandler.requestSchema;
25
+ }
26
+ else if (options.schemaType === "secrets") {
27
+ schema = requestHandler.secretsSchema || z.object({}).strict();
28
+ }
29
+ else if (options.schemaType === "response") {
30
+ const { plugins, outputFormat, request } = options;
31
+ const liaseQuery = await getLiaseQuery({
32
+ request,
33
+ loadPluginsFromArgs: true,
34
+ });
35
+ schema = liaseQuery.getResponseDetails().schema;
36
+ }
37
+ else {
38
+ throw Error(`Unknown schema type option "${options.schemaType}"`);
39
+ }
40
+ const simpleSchema = zodSchemaToSimpleSchema(schema);
41
+ console.dir(simpleSchema, { depth: null });
42
+ });
43
+ return showSchemaCommand;
44
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function getWebUiCommand(): Promise<Command>;
3
+ //# sourceMappingURL=web-ui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-ui.d.ts","sourceRoot":"","sources":["../../src/subcommands/web-ui.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC,wBAAsB,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC,CAUxD"}
@@ -0,0 +1,137 @@
1
+ import fs from "node:fs/promises";
2
+ import http from "node:http";
3
+ import path from "node:path";
4
+ import util from "node:util";
5
+ import AnsiToHtml from "ansi-to-html";
6
+ import { Command } from "commander";
7
+ import mimeTypes from "mime-types";
8
+ import { getSharedLiaseOptions } from "../lib/liase-details.js";
9
+ import { getLiaseQuery } from "../lib/liase-query.js";
10
+ import { getSecretsSets } from "../lib/secrets.js";
11
+ export async function getWebUiCommand() {
12
+ const webUiCommand = new Command();
13
+ const { pluginsOption } = getSharedLiaseOptions({ plugins: [] });
14
+ webUiCommand
15
+ .name("web-ui")
16
+ .addOption(pluginsOption)
17
+ .action(async () => {
18
+ await startServer();
19
+ });
20
+ return webUiCommand;
21
+ }
22
+ function startServer() {
23
+ const buildId = Date.now();
24
+ const server = http
25
+ .createServer(async (req, res) => {
26
+ if (req.method === "GET" && req.url === "/build-id") {
27
+ res.writeHead(200, {
28
+ "Content-Type": "text/application/json",
29
+ "Access-Control-Allow-Origin": "*",
30
+ });
31
+ res.end(JSON.stringify(buildId));
32
+ }
33
+ else if (req.method === "GET" && req.url === "/secrets-sets") {
34
+ return handleSecretSetsRequest(req, res);
35
+ }
36
+ else if (req.method === "POST") {
37
+ console.log(`${req.method} ${req.url}`);
38
+ return handleMediaQueryRequest(req, res);
39
+ }
40
+ else if (req.method === "GET") {
41
+ console.log(`${req.method} ${req.url}`);
42
+ return handleStaticFileRequest(req, res);
43
+ }
44
+ })
45
+ .listen(4000);
46
+ function cleanup() {
47
+ server.close();
48
+ }
49
+ process.on("SIGINT", cleanup);
50
+ process.on("SIGQUIT", cleanup);
51
+ process.on("SIGTERM", cleanup);
52
+ const serverAddress = server.address();
53
+ console.log("Server listening:", `http://localhost:${typeof serverAddress === "object" && serverAddress?.port}`);
54
+ return new Promise((resolve, reject) => {
55
+ server.on("close", resolve);
56
+ });
57
+ }
58
+ async function handleSecretSetsRequest(req, res) {
59
+ const secretSets = await getSecretsSets();
60
+ res.writeHead(200, {
61
+ "Content-Type": "application/json",
62
+ "Access-Control-Allow-Origin": "*",
63
+ });
64
+ res.end(JSON.stringify(Object.keys(secretSets)));
65
+ }
66
+ function handleMediaQueryRequest(req, res) {
67
+ let body = "";
68
+ req.on("data", (chunk) => {
69
+ body += chunk;
70
+ });
71
+ req.on("end", async () => {
72
+ const { mediaFinderRequest, secretsSet, cacheNetworkRequests } = JSON.parse(body);
73
+ let response;
74
+ try {
75
+ const query = await getLiaseQuery({
76
+ request: mediaFinderRequest,
77
+ secretsSet,
78
+ loadPluginsFromArgs: true,
79
+ cacheNetworkRequests,
80
+ });
81
+ response = await query.getNext();
82
+ }
83
+ catch (error) {
84
+ res.writeHead(400, {
85
+ "Content-Type": "text/application/json",
86
+ "Access-Control-Allow-Origin": "*",
87
+ });
88
+ const errorString = new AnsiToHtml({
89
+ fg: "#000",
90
+ bg: "#888",
91
+ newline: true,
92
+ }).toHtml(util.inspect(error));
93
+ console.log(error);
94
+ res.end(JSON.stringify({ error: errorString }));
95
+ return;
96
+ }
97
+ res.writeHead(200, {
98
+ "Content-Type": "application/json",
99
+ "Access-Control-Allow-Origin": "*",
100
+ });
101
+ res.end(JSON.stringify(response));
102
+ });
103
+ }
104
+ async function handleStaticFileRequest(req, res) {
105
+ // parse URL
106
+ const parsedUrl = new URL(req.url || "", "http://domain");
107
+ const sanitizePath = path
108
+ .normalize(parsedUrl.pathname)
109
+ .replace(/^(\.\.[/\\])+/, "");
110
+ let pathname = path.join(import.meta.dirname, "../web-ui", sanitizePath);
111
+ try {
112
+ await fs.access(pathname);
113
+ }
114
+ catch (error) {
115
+ // if the file is not found, return 404
116
+ res.statusCode = 404;
117
+ res.end(`File ${pathname} not found!`);
118
+ return;
119
+ }
120
+ // if is a directory, then look for index.html
121
+ if ((await fs.stat(pathname)).isDirectory()) {
122
+ pathname += "/index.html";
123
+ }
124
+ // read file from file system
125
+ try {
126
+ const data = await fs.readFile(pathname);
127
+ // based on the URL path, extract the file extension. e.g. .js, .doc, ...
128
+ const ext = path.parse(pathname).ext;
129
+ // if the file is found, set Content-type and send data
130
+ res.setHeader("Content-type", mimeTypes.lookup(ext) || "text/plain");
131
+ res.end(data);
132
+ }
133
+ catch (error) {
134
+ res.statusCode = 500;
135
+ res.end(`Error getting the file: ${error}.`);
136
+ }
137
+ }
@@ -0,0 +1,19 @@
1
+ import { toRefs } from "vue";
2
+ import MediaPreview from "./media-preview.js";
3
+ export default {
4
+ props: ["response"],
5
+ components: {
6
+ MediaPreview,
7
+ },
8
+ setup(props) {
9
+ const { response } = toRefs(props);
10
+ return { response };
11
+ },
12
+ template: /* html */ `
13
+ <ul class="DisplayMedia">
14
+ <li v-for="media in response.media" :key="media.id">
15
+ <media-preview :media="media" />
16
+ </li>
17
+ </ul>
18
+ `,
19
+ };
@@ -0,0 +1,132 @@
1
+ import { computed, onMounted, ref, useTemplateRef } from "vue";
2
+ import "hls.js";
3
+ export default {
4
+ props: ["media"],
5
+ setup(props) {
6
+ const maxHeight = computed(() =>
7
+ Math.max(...props.media.files.map((file) => file.height || 0)),
8
+ );
9
+
10
+ const thumbnailDisplayHeight = 200;
11
+ const fileSortWeight = (file) => {
12
+ let weight = file.height
13
+ ? Math.abs(file.height - thumbnailDisplayHeight) /
14
+ Math.max(maxHeight.value, thumbnailDisplayHeight)
15
+ : 1;
16
+ if (!file.hasVideo) weight += 1;
17
+ return weight;
18
+ };
19
+
20
+ const mediaAsset = computed(
21
+ () =>
22
+ (props.media.assets?.media || props.media.files || []).toSorted(
23
+ (a, b) =>
24
+ // Listed in order of importance from most important to least important
25
+ (b.videoCodec !== "hevc") - (a.videoCodec !== "hevc") || // Non-hevc streams preferred
26
+ (b.isOriginalTranscode ?? false) -
27
+ (a.isOriginalTranscode ?? false) || // isOriginalTranscode preferred
28
+ (b.mimeType === "application/vnd.apple.mpegurl") -
29
+ (a.mimeType === "application/vnd.apple.mpegurl") || // HLS preferred
30
+ (b.isOriginalResolution ?? false) -
31
+ (a.isOriginalResolution ?? false) || // isOriginalResolution preferred
32
+ (b.duration ?? 0) - (a.duration ?? 0), // duration descending
33
+ )[0],
34
+ );
35
+
36
+ const previewAsset = computed(
37
+ () =>
38
+ (
39
+ props.media.assets?.preview ||
40
+ props.media.files?.filter((file) => file.image) ||
41
+ []
42
+ ).toSorted(
43
+ (a, b) =>
44
+ // Listed in order of importance from most important to least important
45
+ fileSortWeight(a) - fileSortWeight(b), // How close the size it to the displayed size from closest to furthest
46
+ )[0],
47
+ );
48
+
49
+ const displayElement = computed(() =>
50
+ mediaAsset.value?.video && mediaAsset.value?.ext !== "gif"
51
+ ? "video"
52
+ : "image",
53
+ );
54
+
55
+ const videoRef = useTemplateRef("video-elm");
56
+
57
+ onMounted(() => {
58
+ const file = mediaAsset.value;
59
+
60
+ if (!file || videoRef.value === null) {
61
+ return;
62
+ }
63
+
64
+ if (file.ext === "m3u8") {
65
+ if (videoRef.value.canPlayType("application/vnd.apple.mpegurl")) {
66
+ videoRef.value.src = file.url;
67
+ } else if (window.Hls.isSupported()) {
68
+ const hls = new window.Hls();
69
+ hls.loadSource(file.url);
70
+ hls.attachMedia(videoRef.value);
71
+ } else {
72
+ throw Error("Browser can't play HLS");
73
+ }
74
+ } else {
75
+ videoRef.value.src = file.url;
76
+ }
77
+ });
78
+
79
+ const hoverOverPlayCountdown = ref(null);
80
+ function handleMouseEnter() {
81
+ hoverOverPlayCountdown.value = setTimeout(
82
+ () => videoRef.value?.play(),
83
+ 300,
84
+ );
85
+ }
86
+ function handleMouseLeave() {
87
+ clearTimeout(hoverOverPlayCountdown.value);
88
+ videoRef.value?.pause();
89
+ }
90
+ return {
91
+ displayElement,
92
+ mediaAsset,
93
+ previewAsset,
94
+ handleMouseEnter,
95
+ handleMouseLeave,
96
+ };
97
+ },
98
+ template: /* html */ `
99
+ <div
100
+ class="MediaPreview"
101
+ >
102
+ <div
103
+ v-if="displayElement === 'video'"
104
+ class="videoContainer"
105
+ >
106
+ <video
107
+ ref="video-elm"
108
+ preload="none"
109
+ playsinline="true"
110
+ muted="true"
111
+ :poster="previewAsset?.url"
112
+ :style="mediaAsset.aspectRatio ? {'aspect-ratio': mediaAsset.aspectRatio.width + ' / ' + mediaAsset.aspectRatio.height} : {}"
113
+ controls="true"
114
+ ></video>
115
+ <img
116
+ v-if="previewAsset"
117
+ :src="previewAsset?.url"
118
+ >
119
+ </div>
120
+ <img
121
+ v-else-if="displayElement === 'image'"
122
+ :src="mediaAsset?.url"
123
+ >
124
+ <div v-else>
125
+ Unknown display type {{ displayElement }}
126
+ </div>
127
+ <div class="info">
128
+ {{ media.title }}
129
+ </div>
130
+ </div>
131
+ `,
132
+ };
@@ -0,0 +1,205 @@
1
+ import { computed, ref, useTemplateRef, watch } from "vue";
2
+ import DisplayMedia from "./display-media.js";
3
+ import "@alenaksu/json-viewer";
4
+ export default {
5
+ components: {
6
+ DisplayMedia,
7
+ },
8
+ setup() {
9
+ const emptyQuery = {
10
+ id: Date.now(),
11
+ name: "untitled query",
12
+ requestString: "{}",
13
+ secretsSet: "",
14
+ cacheNetworkRequests: "always",
15
+ };
16
+ const queries = ref(
17
+ JSON.parse(
18
+ localStorage.getItem("queries") || JSON.stringify([emptyQuery]),
19
+ ),
20
+ );
21
+ watch(
22
+ queries,
23
+ () => localStorage.setItem("queries", JSON.stringify(queries.value)),
24
+ { deep: true },
25
+ );
26
+
27
+ const currentQueryId = ref(
28
+ JSON.parse(localStorage.getItem("currentQueryId")) ||
29
+ queries.value[0]?.id,
30
+ );
31
+ watch(currentQueryId, () => {
32
+ localStorage.setItem("currentQueryId", currentQueryId.value);
33
+ });
34
+
35
+ const currentQuery = computed(() => {
36
+ const currentQuery = queries.value?.find(
37
+ (query) => query.id === currentQueryId.value,
38
+ );
39
+ if (!currentQuery) {
40
+ console.info("Queries", queries.value);
41
+ if (queries.value?.length) {
42
+ console.warn(
43
+ "Could not find current query with id:",
44
+ currentQueryId.value,
45
+ );
46
+ const firstQuery = queries.value[0];
47
+ currentQueryId.value = firstQuery.id;
48
+ return firstQuery;
49
+ }
50
+ throw Error(
51
+ `Could not find current query with id: ${currentQueryId.value}`,
52
+ );
53
+ }
54
+ return currentQuery;
55
+ });
56
+
57
+ const secretsSets = ref([]);
58
+ async function updateSecretsSets() {
59
+ const res = await fetch("/secrets-sets");
60
+ secretsSets.value = await res.json();
61
+ }
62
+ updateSecretsSets();
63
+
64
+ const requestValid = computed(() => {
65
+ try {
66
+ JSON.parse(currentQuery.value?.requestString);
67
+ return true;
68
+ } catch (error) {
69
+ return false;
70
+ }
71
+ });
72
+ const responseView = ref(localStorage.getItem("responseView") || "visual");
73
+ watch(responseView, () => {
74
+ localStorage.setItem("responseView", responseView.value);
75
+ });
76
+ const response = ref("");
77
+ const loadingStatus = ref("finished");
78
+ async function fetchMedia() {
79
+ loadingStatus.value = "loading";
80
+ try {
81
+ const res = await fetch("/", {
82
+ method: "POST",
83
+ body: JSON.stringify({
84
+ mediaFinderRequest: JSON.parse(currentQuery.value?.requestString),
85
+ secretsSet: currentQuery.value?.secretsSet,
86
+ cacheNetworkRequests: currentQuery.value?.cacheNetworkRequests,
87
+ }),
88
+ });
89
+ response.value = await res.json();
90
+ if (res.ok) {
91
+ loadingStatus.value = "finished";
92
+ } else {
93
+ loadingStatus.value = "error";
94
+ }
95
+ } catch (error) {
96
+ loadingStatus.value = "error";
97
+ response.value = { error };
98
+ }
99
+ }
100
+ fetchMedia();
101
+
102
+ function duplicateQuery() {
103
+ const clonedCurrentQuery = JSON.parse(JSON.stringify(currentQuery.value));
104
+ const newQuery = {
105
+ ...clonedCurrentQuery,
106
+ id: Date.now(),
107
+ };
108
+ queries.value.push(newQuery);
109
+ currentQueryId.value = newQuery.id;
110
+ }
111
+
112
+ function handleRequestChange(event) {
113
+ currentQuery.value.requestString = JSON.stringify(
114
+ JSON.parse(event.target.value),
115
+ null,
116
+ 2,
117
+ );
118
+ }
119
+
120
+ const jsonViewerRef = useTemplateRef("json-viewer");
121
+
122
+ watch([response, responseView], () => {
123
+ if (responseView.value === "json") {
124
+ setTimeout(() => {
125
+ jsonViewerRef.value?.expand("media.0");
126
+ }, 100);
127
+ }
128
+ });
129
+ return {
130
+ response,
131
+ fetchMedia,
132
+ responseView,
133
+ requestValid,
134
+ loadingStatus,
135
+ handleRequestChange,
136
+ secretsSets,
137
+ currentQuery,
138
+ currentQueryId,
139
+ queries,
140
+ duplicateQuery,
141
+ };
142
+ },
143
+ template: /* html */ `
144
+ <div class="options">
145
+ <div class="group">
146
+ <div class="group">
147
+ <label for="current-query">Current query:</label>
148
+ <select name="current-query" id="current-query" v-model="currentQueryId">
149
+ <option v-for="query, index in queries" :value="query.id">{{index}} - {{query.name}}</option>
150
+ </select>
151
+ </div>
152
+ <div class="group">
153
+ <label for="query-name">Query name:</label>
154
+ <input name="query-name" id="query-name" v-model="currentQuery.name" />
155
+ </div>
156
+ <button @click="duplicateQuery">Duplicate query</button>
157
+ </div>
158
+ <textarea
159
+ :style="{'background-color': requestValid ? 'rgba(56, 255, 0, 0.06)' : '#ff00001a'}"
160
+ id="request"
161
+ v-model="currentQuery.requestString"
162
+ @change="handleRequestChange"
163
+ ></textarea>
164
+ <div class="group">
165
+ <div class="group">
166
+ <label for="secret-set">Secrets Set:</label>
167
+ <select name="secret-set" id="secret-set" v-model="currentQuery.secretsSet">
168
+ <option value="">--None--</option>
169
+ <option v-for="secretsSet in secretsSets" :value="secretsSet">{{secretsSet}}</option>
170
+ </select>
171
+ </div>
172
+ <div class="group">
173
+ <label for="cache-network-requests">Cache Network Requests:</label>
174
+ <select name="cache-network-requests" id="cache-network-requests" v-model="currentQuery.cacheNetworkRequests">
175
+ <option value="never">Never</option>
176
+ <option value="auto">Auto</option>
177
+ <option value="always">Always</option>
178
+ </select>
179
+ </div>
180
+ <button @click="fetchMedia" :disabled="!requestValid">Fetch</button>
181
+ </div>
182
+ </div>
183
+ <div class="buttons">
184
+ <button @click="responseView = responseView === 'json' ? 'visual' : 'json'">Show {{responseView === 'json' ? 'Media' : 'JSON'}}</button>
185
+ </div>
186
+ <div v-if="loadingStatus !== 'finished'">{{loadingStatus}}</div>
187
+ <div
188
+ v-if="loadingStatus === 'error'"
189
+ >
190
+ <pre class="error" v-html="response?.error"></pre>
191
+ </div>
192
+ <template v-else>
193
+ <json-viewer
194
+ v-if="responseView === 'json'"
195
+ id="response"
196
+ :data="response"
197
+ ref="json-viewer"
198
+ />
199
+ <display-media
200
+ v-else
201
+ :response="response"
202
+ />
203
+ </template>
204
+ `,
205
+ };