@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.
- package/LICENSE +9 -0
- package/README.md +5 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/lib/liase-details.d.ts +18 -0
- package/dist/lib/liase-details.d.ts.map +1 -0
- package/dist/lib/liase-details.js +105 -0
- package/dist/lib/liase-query.d.ts +8 -0
- package/dist/lib/liase-query.d.ts.map +1 -0
- package/dist/lib/liase-query.js +25 -0
- package/dist/lib/proxy.d.ts +6 -0
- package/dist/lib/proxy.d.ts.map +1 -0
- package/dist/lib/proxy.js +48 -0
- package/dist/lib/secrets.d.ts +2 -0
- package/dist/lib/secrets.d.ts.map +1 -0
- package/dist/lib/secrets.js +30 -0
- package/dist/lib/zod.d.ts +50 -0
- package/dist/lib/zod.d.ts.map +1 -0
- package/dist/lib/zod.js +196 -0
- package/dist/subcommands/run.d.ts +3 -0
- package/dist/subcommands/run.d.ts.map +1 -0
- package/dist/subcommands/run.js +123 -0
- package/dist/subcommands/show-schema.d.ts +3 -0
- package/dist/subcommands/show-schema.d.ts.map +1 -0
- package/dist/subcommands/show-schema.js +44 -0
- package/dist/subcommands/web-ui.d.ts +3 -0
- package/dist/subcommands/web-ui.d.ts.map +1 -0
- package/dist/subcommands/web-ui.js +137 -0
- package/dist/web-ui/components/display-media.js +19 -0
- package/dist/web-ui/components/media-preview.js +132 -0
- package/dist/web-ui/components/page.js +205 -0
- package/dist/web-ui/index.html +133 -0
- package/package.json +55 -0
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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
|
+
};
|