From 79b3a2196c166843e757a78fc5434daa6d7361ce Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Mon, 28 Apr 2025 09:07:45 +0200 Subject: [PATCH] feat: add web editor base --- editor/public/base.css | 9 ++ editor/public/edit/index.css | 13 +++ editor/public/edit/index.html | 48 +++++++++++ editor/public/edit/index.js | 157 ++++++++++++++++++++++++++++++++++ editor/public/index.html | 13 +++ editor/public/index.js | 31 +++++++ editor/server.py | 90 +++++++++++++++++++ 7 files changed, 361 insertions(+) create mode 100644 editor/public/base.css create mode 100644 editor/public/edit/index.css create mode 100644 editor/public/edit/index.html create mode 100644 editor/public/edit/index.js create mode 100644 editor/public/index.html create mode 100644 editor/public/index.js create mode 100644 editor/server.py diff --git a/editor/public/base.css b/editor/public/base.css new file mode 100644 index 0000000..77fd275 --- /dev/null +++ b/editor/public/base.css @@ -0,0 +1,9 @@ +* { + /*padding: 0;*/ + /*margin: 0;*/ + box-sizing: border-box; +} + +body { + font-family: Ubuntu; +} \ No newline at end of file diff --git a/editor/public/edit/index.css b/editor/public/edit/index.css new file mode 100644 index 0000000..27ee460 --- /dev/null +++ b/editor/public/edit/index.css @@ -0,0 +1,13 @@ +#filename { + font-size: 80%; + font-style: italic; +} + +#unsaved { + color: #9d4916; + font-size: 80%; + + &:not(.show) { + display: none; + } +} \ No newline at end of file diff --git a/editor/public/edit/index.html b/editor/public/edit/index.html new file mode 100644 index 0000000..a394faa --- /dev/null +++ b/editor/public/edit/index.html @@ -0,0 +1,48 @@ + + + + + + Edit + + + + + +
+ +
+ +
+

Edit - Unsaved

+
+
+ + +
+
+
+

Audio Tracks

+ + + + + +
+
+
+

Subtitle Tracks

+ + + + + +
+
+
+ + \ No newline at end of file diff --git a/editor/public/edit/index.js b/editor/public/edit/index.js new file mode 100644 index 0000000..04da087 --- /dev/null +++ b/editor/public/edit/index.js @@ -0,0 +1,157 @@ +/** @type TracksTable */ +let audioTable + +/** @type TracksTable */ +let subtitleTable + +function flattenObj(obj) { + const res = {} + Object.entries(obj).forEach(([key, value]) => { + if (typeof value === "object") { + value = flattenObj(value) + Object.entries(value).forEach(([key2, value2]) => { + res[key + "/" + key2] = value2 + }) + } else { + res[key] = value + } + }) + return res +} + +class TracksTable { + OPTIONS = { + "language": ["fre", "eng"] + } + + constructor(table) { + this.table = table + this.headers = this.table.querySelector("thead tr") + this.body = this.table.querySelector("tbody") + this.fields = [] + this.tracks = [] + } + + showTracks(tracks) { + this.tracks = tracks.map(flattenObj) + this.clear() + if (tracks.length === 0) { + return + } + this.detectFields() + this.addHeaders() + this.tracks.forEach((track, i) => { + const tr = document.createElement("tr") + tr.dataset.i = i + this.fields.forEach(field => { + const td = tr.insertCell(-1) + const input = this.makeInput(field, track[field.key]) + td.appendChild(input) + }) + this.body.appendChild(tr) + }) + } + + clear() { + this.headers.innerHTML = "" + this.body.innerHTML = "" + this.fields = [] + } + + detectFields() { + Object.entries(this.tracks[0]).forEach(([key, value]) => { + let type = { + boolean: "bool", + number: "num" + }[typeof value] ?? "str" + if (key === "language") { + type = "sel" + } + + const name = key.split("/").slice(-1)[0] + this.fields.push({name, type, key}) + }) + } + + addHeaders() { + this.fields.forEach(field => { + const th = document.createElement("th") + th.innerText = field.name + this.headers.appendChild(th) + }) + } + + makeInput(field, value) { + let input = document.createElement("input") + switch (field.type) { + case "num": + input.type = "number" + input.value = value + break + + case "str": + input.type = "text" + input.value = value + break + + case "bool": + input.type = "checkbox" + input.checked = value + break + + case "sel": + input = document.createElement("select") + const options = this.OPTIONS[field.name] + options.forEach(option => { + const opt = document.createElement("option") + opt.innerText = option + opt.value = option + input.appendChild(opt) + }) + input.value = value + + default: + break + } + return input + } +} + +function fetchData(filename) { + fetch("/api/file", { + method: "POST", + body: JSON.stringify({ + file: filename + }), + headers: { + "Content-Type": "application/json" + } + }).then(res => { + if (res.ok) { + return res.json() + } + return null + }).then(res => { + if (res !== null) { + displayData(res) + } + }) +} + +function displayData(data) { + document.getElementById("title").value = data.title + + audioTable.showTracks(data.audio_tracks) + subtitleTable.showTracks(data.subtitle_tracks) +} + +window.addEventListener("load", () => { + audioTable = new TracksTable(document.getElementById("audio-tracks")) + subtitleTable = new TracksTable(document.getElementById("subtitle-tracks")) + + const params = new URLSearchParams(window.location.search) + const file = params.get("f") + document.getElementById("filename").innerText = file + + fetchData(file) +}) \ No newline at end of file diff --git a/editor/public/index.html b/editor/public/index.html new file mode 100644 index 0000000..cdf3ce5 --- /dev/null +++ b/editor/public/index.html @@ -0,0 +1,13 @@ + + + + + + Metadata Editor + + + +

Metadata Editor

+ + + \ No newline at end of file diff --git a/editor/public/index.js b/editor/public/index.js new file mode 100644 index 0000000..c9c646f --- /dev/null +++ b/editor/public/index.js @@ -0,0 +1,31 @@ +function addOptions(files) { + const select = document.getElementById("file-sel") + select.innerHTML = "" + const defaultOpt = document.createElement("option") + defaultOpt.innerText = "----- Select a file -----" + defaultOpt.value = "" + select.appendChild(defaultOpt) + files.forEach(file => { + const option = document.createElement("option") + option.innerText = file + option.value = file + select.appendChild(option) + }) +} + +function selectFile(event) { + const file = event.target.value + if (file !== "") { + const url = new URL("/edit/", window.location.origin) + url.searchParams.set("f", file) + window.location.href = url.href + } +} + +window.addEventListener("load", () => { + fetch("/api/files").then(res => { + return res.json() + }).then(files => { + addOptions(files) + }) +}) \ No newline at end of file diff --git a/editor/server.py b/editor/server.py new file mode 100644 index 0000000..1133468 --- /dev/null +++ b/editor/server.py @@ -0,0 +1,90 @@ +from http import HTTPStatus +from http.server import SimpleHTTPRequestHandler +import json +import os +import socketserver +from typing import Optional +from urllib.parse import urlparse, parse_qs + +PORT = 8000 + +class MyHandler(SimpleHTTPRequestHandler): + DATA_DIR = "metadata" + + def __init__(self, *args, **kwargs): + super().__init__( + *args, + directory="public", + **kwargs + ) + self.query: dict = {} + self.data: Optional[dict|list] = None + + def read_body_data(self): + self.log_message("Reading body data") + try: + raw_data = self.rfile.read(int(self.headers["Content-Length"])) + self.data = json.loads(raw_data) + except: + self.send_error(HTTPStatus.NOT_ACCEPTABLE, "Malformed JSON body") + self.log_error("Malformed JSON body") + return False + return True + + def do_GET(self): + self.query = parse_qs(urlparse(self.path).query) + if self.path.startswith("/api/"): + self.handle_api_get(self.path.removeprefix("/api/").removesuffix("/")) + return + super().do_GET() + + def do_POST(self): + self.query = parse_qs(urlparse(self.path).query) + if self.path.startswith("/api/"): + self.handle_api_post(self.path.removeprefix("/api/").removesuffix("/")) + return + self.send_error(HTTPStatus.NOT_FOUND) + + def handle_api_get(self, path: str): + print(f"API request at {path}") + if path == "files": + files: list[str] = self.get_files() + self.send_json(files) + return + + def handle_api_post(self, path: str): + if path == "file": + if self.read_body_data(): + data = self.get_file(self.data["file"]) + if data is None: + self.send_error(HTTPStatus.NOT_FOUND) + self.log_message("File not found") + else: + self.log_message("Got file") + self.send_json(data) + + def send_json(self, data: dict|list): + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps(data).encode("utf-8")) + + def get_files(self): + return os.listdir(self.DATA_DIR) + + def get_file(self, filename: str) -> Optional[dict|list]: + if filename not in self.get_files(): + return None + with open(os.path.join(self.DATA_DIR, filename), "r") as f: + data = json.load(f) + return data + + +def main(): + with socketserver.TCPServer(("", PORT), MyHandler) as httpd: + print(f"Serving on port {PORT}") + httpd.serve_forever() + + +if __name__ == "__main__": + main()