Implement metadata editing (WIP)

This commit is contained in:
MrLetsplay 2025-02-17 23:04:50 +01:00
parent 8b4cdef690
commit f8f945acc4
Signed by: mr
SSH Key Fingerprint: SHA256:92jBH80vpXyaZHjaIl47pjRq+Yt7XGTArqQg1V7hSqg
7 changed files with 122 additions and 22 deletions

View File

@ -34,21 +34,23 @@ class API {
{
method,
headers: (body != null ? { "Content-Type": "application/json" } : undefined),
body
body: (body != null ? JSON.stringify(body) : undefined)
}
);
let json = null;
try {
json = await response.json();
} catch (e) {
throw "Failed to decode response";
}
} catch (e) { }
if (!response.ok) {
throw json?.error || "Request failed: " + response.status + " " + response.statusText;
}
if (json == null) {
throw "Failed to decode response";
}
return json
}
@ -72,13 +74,22 @@ class API {
/**
* @param {string} id
* @returns {Video}
* @returns {FullVideoMetadata}
* @throws {APIError}
*/
async getVideoMetadata(id) {
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 {
@ -122,6 +133,7 @@ class Video {
/**
* @typedef {{series:string, title: string, author: string, index: number, [key: string]: any}} VideoMetadata
* @typedef {{raw: VideoMetadata, inherited: VideoMetadata, effective: VideoMetadata}} FullVideoMetadata
*/
/**

View File

@ -64,7 +64,7 @@ const components = {
* @param {Video} video
* @returns {HTMLElement}
*/
player(video) {
async player(video) {
const streamURL = API_URL + "/library/video/" + encodeURIComponent(video.id) + "/stream";
const downloadVideo = () => {
@ -81,7 +81,7 @@ const components = {
element("img", { attributes: { "src": "/icon/download.svg", "width": "24", "height": "24" } }),
element("span", { content: "Download" }),
]),
components.videoMetadata(video)
await components.videoMetadata(video)
]);
},
@ -89,30 +89,73 @@ const components = {
* @param {Video} video
* @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"] }, [
element("summary", { content: "Metadata" }),
element("div", { classNames: ["video-metadata-table"] }, [
element("span", { content: "Title" }),
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" })
form,
element("button", { content: "Update", onClick: updateMetadata })
]);
},
/**
* @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
* @returns {HTMLElement}
*/
error(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("button", { content: "Reload", onClick: () => window.location.reload() })
]);

9
js/series.js Normal file
View File

@ -0,0 +1,9 @@
const api = new API(API_URL);
const root = document.getElementById("root");
async function init() {
finishedLoading();
}
init();

View File

@ -5,8 +5,15 @@ const root = document.getElementById("root");
async function init() {
let query = new URLSearchParams(document.location.search);
let video = await api.getVideo(query.get("id"));
root.appendChild(components.player(video));
let video;
try {
video = await api.getVideo(query.get("id"));
} catch (e) {
showError(e);
return;
}
root.appendChild(await components.player(video));
finishedLoading();
}

27
series.html Normal file
View 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>

View File

@ -61,6 +61,7 @@
.player>video {
width: 1000px;
max-height: 500px;
max-width: 100%;
}

1
style/series.css Normal file
View File

@ -0,0 +1 @@