Implement metadata editing (WIP)
This commit is contained in:
parent
8b4cdef690
commit
f8f945acc4
22
js/api.js
22
js/api.js
@ -34,21 +34,23 @@ class API {
|
|||||||
{
|
{
|
||||||
method,
|
method,
|
||||||
headers: (body != null ? { "Content-Type": "application/json" } : undefined),
|
headers: (body != null ? { "Content-Type": "application/json" } : undefined),
|
||||||
body
|
body: (body != null ? JSON.stringify(body) : undefined)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
let json = null;
|
let json = null;
|
||||||
try {
|
try {
|
||||||
json = await response.json();
|
json = await response.json();
|
||||||
} catch (e) {
|
} catch (e) { }
|
||||||
throw "Failed to decode response";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw json?.error || "Request failed: " + response.status + " " + response.statusText;
|
throw json?.error || "Request failed: " + response.status + " " + response.statusText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (json == null) {
|
||||||
|
throw "Failed to decode response";
|
||||||
|
}
|
||||||
|
|
||||||
return json
|
return json
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,13 +74,22 @@ class API {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} id
|
* @param {string} id
|
||||||
* @returns {Video}
|
* @returns {FullVideoMetadata}
|
||||||
* @throws {APIError}
|
* @throws {APIError}
|
||||||
*/
|
*/
|
||||||
async getVideoMetadata(id) {
|
async getVideoMetadata(id) {
|
||||||
return await this.call("GET", "/library/video/" + encodeURIComponent(id) + "/metadata", null, null);
|
return await this.call("GET", "/library/video/" + encodeURIComponent(id) + "/metadata", null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
* @param {VideoMetadata} metadata
|
||||||
|
* @returns {VideoMetadata}
|
||||||
|
*/
|
||||||
|
async updateVideoMetadata(id, metadata) {
|
||||||
|
return await this.call("PUT", "/library/video/" + encodeURIComponent(id) + "/metadata", null, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class APIError {
|
class APIError {
|
||||||
@ -122,6 +133,7 @@ class Video {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {{series:string, title: string, author: string, index: number, [key: string]: any}} VideoMetadata
|
* @typedef {{series:string, title: string, author: string, index: number, [key: string]: any}} VideoMetadata
|
||||||
|
* @typedef {{raw: VideoMetadata, inherited: VideoMetadata, effective: VideoMetadata}} FullVideoMetadata
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -64,7 +64,7 @@ const components = {
|
|||||||
* @param {Video} video
|
* @param {Video} video
|
||||||
* @returns {HTMLElement}
|
* @returns {HTMLElement}
|
||||||
*/
|
*/
|
||||||
player(video) {
|
async player(video) {
|
||||||
const streamURL = API_URL + "/library/video/" + encodeURIComponent(video.id) + "/stream";
|
const streamURL = API_URL + "/library/video/" + encodeURIComponent(video.id) + "/stream";
|
||||||
|
|
||||||
const downloadVideo = () => {
|
const downloadVideo = () => {
|
||||||
@ -81,7 +81,7 @@ const components = {
|
|||||||
element("img", { attributes: { "src": "/icon/download.svg", "width": "24", "height": "24" } }),
|
element("img", { attributes: { "src": "/icon/download.svg", "width": "24", "height": "24" } }),
|
||||||
element("span", { content: "Download" }),
|
element("span", { content: "Download" }),
|
||||||
]),
|
]),
|
||||||
components.videoMetadata(video)
|
await components.videoMetadata(video)
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -89,30 +89,73 @@ const components = {
|
|||||||
* @param {Video} video
|
* @param {Video} video
|
||||||
* @returns {HTMLElement}
|
* @returns {HTMLElement}
|
||||||
*/
|
*/
|
||||||
videoMetadata(video) {
|
async videoMetadata(video) {
|
||||||
|
let metadata;
|
||||||
|
try {
|
||||||
|
metadata = await api.getVideoMetadata(video.id);
|
||||||
|
} catch (e) {
|
||||||
|
showError(e);
|
||||||
|
return element("span", { content: "Failed to load" });
|
||||||
|
}
|
||||||
|
|
||||||
|
let form = element("form", { classNames: ["video-metadata-table"] }, [
|
||||||
|
...components.metadataInput(metadata, "Title", "title"),
|
||||||
|
...components.metadataInput(metadata, "Series", "series"),
|
||||||
|
...components.metadataInput(metadata, "Author", "author"),
|
||||||
|
...components.metadataInput(metadata, "Index", "index"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let updateMetadata = async () => {
|
||||||
|
let formData = new FormData(form);
|
||||||
|
let metadata = {};
|
||||||
|
formData.forEach((v, k) => {
|
||||||
|
if (v != "") {
|
||||||
|
if (k == "index") { // TODO: card coded check, improve
|
||||||
|
v = parseInt(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata[k] = v;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.updateVideoMetadata(video.id, metadata);
|
||||||
|
window.location.reload();
|
||||||
|
} catch (e) {
|
||||||
|
showError(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return element("details", { classNames: ["video-metadata"] }, [
|
return element("details", { classNames: ["video-metadata"] }, [
|
||||||
element("summary", { content: "Metadata" }),
|
element("summary", { content: "Metadata" }),
|
||||||
element("div", { classNames: ["video-metadata-table"] }, [
|
form,
|
||||||
element("span", { content: "Title" }),
|
element("button", { content: "Update", onClick: updateMetadata })
|
||||||
element("input", { attributes: { "value": video.metadata.title } }),
|
|
||||||
element("span", { content: "Series" }),
|
|
||||||
element("input", { attributes: { "value": video.metadata.series } }),
|
|
||||||
element("span", { content: "Author" }),
|
|
||||||
element("input", { attributes: { "value": video.metadata.author } }),
|
|
||||||
element("span", { content: "Index" }),
|
|
||||||
element("input", { attributes: { "value": video.metadata.index } }),
|
|
||||||
]),
|
|
||||||
element("button", { content: "Update" })
|
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {FullVideoMetadata} metadata
|
||||||
|
* @param {string} label
|
||||||
|
* @param {string} key
|
||||||
|
* @returns {HTMLElement}
|
||||||
|
*/
|
||||||
|
metadataInput(metadata, label, key) {
|
||||||
|
let raw = metadata.raw[key];
|
||||||
|
let inherited = metadata.inherited[key];
|
||||||
|
|
||||||
|
return [
|
||||||
|
element("span", { content: label }),
|
||||||
|
element("input", { attributes: { "name": key, "value": raw ?? "", "placeholder": "(inherited: " + inherited + ")" } }),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {any} error
|
* @param {any} error
|
||||||
* @returns {HTMLElement}
|
* @returns {HTMLElement}
|
||||||
*/
|
*/
|
||||||
error(error) {
|
error(error) {
|
||||||
return element("div", { classNames: ["error"] }, [
|
return element("div", { classNames: ["error"] }, [
|
||||||
element("b", { content: "Oops, an unexpected error occurred" }),
|
element("b", { content: "Oops, an error occurred" }),
|
||||||
element("span", { content: errorToString(error) }),
|
element("span", { content: errorToString(error) }),
|
||||||
element("button", { content: "Reload", onClick: () => window.location.reload() })
|
element("button", { content: "Reload", onClick: () => window.location.reload() })
|
||||||
]);
|
]);
|
||||||
|
9
js/series.js
Normal file
9
js/series.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
const api = new API(API_URL);
|
||||||
|
|
||||||
|
const root = document.getElementById("root");
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
finishedLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
11
js/watch.js
11
js/watch.js
@ -5,8 +5,15 @@ const root = document.getElementById("root");
|
|||||||
async function init() {
|
async function init() {
|
||||||
let query = new URLSearchParams(document.location.search);
|
let query = new URLSearchParams(document.location.search);
|
||||||
|
|
||||||
let video = await api.getVideo(query.get("id"));
|
let video;
|
||||||
root.appendChild(components.player(video));
|
try {
|
||||||
|
video = await api.getVideo(query.get("id"));
|
||||||
|
} catch (e) {
|
||||||
|
showError(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.appendChild(await components.player(video));
|
||||||
|
|
||||||
finishedLoading();
|
finishedLoading();
|
||||||
}
|
}
|
||||||
|
27
series.html
Normal file
27
series.html
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>VideoBase</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<script src="js/config.js" defer></script>
|
||||||
|
<script src="js/api.js" defer></script>
|
||||||
|
<script src="js/components.js" defer></script>
|
||||||
|
<script src="js/base.js" defer></script>
|
||||||
|
<script src="js/series.js" defer></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="style/base.css">
|
||||||
|
<link rel="stylesheet" href="style/components.css">
|
||||||
|
<link rel="stylesheet" href="style/series.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<noscript>Unfortunately, this page requires JavaScript to work correctly</noscript>
|
||||||
|
<div id="loading">Loading...</div>
|
||||||
|
<div id="error" style="display: none;"></div>
|
||||||
|
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -61,6 +61,7 @@
|
|||||||
|
|
||||||
.player>video {
|
.player>video {
|
||||||
width: 1000px;
|
width: 1000px;
|
||||||
|
max-height: 500px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
1
style/series.css
Normal file
1
style/series.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
Loading…
x
Reference in New Issue
Block a user