From 6a0669df4afbafc4c4f064228cf4209084c56583 Mon Sep 17 00:00:00 2001 From: LordBaryhobal Date: Wed, 30 Apr 2025 21:16:34 +0200 Subject: [PATCH] feat: add support for series --- editor/public/edit/index.html | 6 ++ editor/public/static/css/edit.css | 41 ++++++++++++ editor/public/static/images/next.svg | 64 ++++++++++++++++++ editor/public/static/images/prev.svg | 64 ++++++++++++++++++ editor/public/static/js/editor.mjs | 51 ++++++++++++-- editor/public/static/js/index.js | 2 +- editor/public/static/js/media_file.mjs | 5 ++ editor/public/static/js/metadata.mjs | 85 ++++++++++++++++++++++++ editor/public/static/js/tracks_table.mjs | 32 +++++---- editor/public/static/js/utils.mjs | 7 +- editor/server.py | 9 +-- 11 files changed, 339 insertions(+), 27 deletions(-) create mode 100644 editor/public/static/images/next.svg create mode 100644 editor/public/static/images/prev.svg create mode 100644 editor/public/static/js/media_file.mjs create mode 100644 editor/public/static/js/metadata.mjs diff --git a/editor/public/edit/index.html b/editor/public/edit/index.html index 696fa3e..8fc6535 100644 --- a/editor/public/edit/index.html +++ b/editor/public/edit/index.html @@ -23,6 +23,12 @@

Editing - Unsaved

+
+ +
+ +
+
diff --git a/editor/public/static/css/edit.css b/editor/public/static/css/edit.css index 0b80eba..4d57952 100644 --- a/editor/public/static/css/edit.css +++ b/editor/public/static/css/edit.css @@ -337,6 +337,7 @@ button.improve { height: 100%; display: flex; flex-direction: column; + max-width: 30em; &:not(.show) { display: none; @@ -369,4 +370,44 @@ button.improve { padding: 0.4em; } } +} + +.sep { + border-bottom: solid black 1px; +} + +#series-toolbar { + display: flex; + gap: 0.4em; + padding: 0.4em; + align-items: center; + + &:not(.show) { + display: none; + } + + button { + background: var(--img); + width: 2.4em; + height: 2.4em; + background-color: transparent; + border: none; + background-size: 80%; + background-position: center; + background-repeat: no-repeat; + cursor: pointer; + border-radius: 0.4em; + + &:hover { + background-color: #f1f1f1; + } + + &#prev-episode { + --img: url("/static/images/prev.svg"); + } + + &#next-episode { + --img: url("/static/images/next.svg"); + } + } } \ No newline at end of file diff --git a/editor/public/static/images/next.svg b/editor/public/static/images/next.svg new file mode 100644 index 0000000..db48ec2 --- /dev/null +++ b/editor/public/static/images/next.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + diff --git a/editor/public/static/images/prev.svg b/editor/public/static/images/prev.svg new file mode 100644 index 0000000..4217564 --- /dev/null +++ b/editor/public/static/images/prev.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + diff --git a/editor/public/static/js/editor.mjs b/editor/public/static/js/editor.mjs index ba62581..1cfd1ea 100644 --- a/editor/public/static/js/editor.mjs +++ b/editor/public/static/js/editor.mjs @@ -1,6 +1,7 @@ import TracksTable from "./tracks_table.mjs" import IntegrityManager from "./integrity_manager.mjs" import { updateObjectFromJoinedKey } from "./utils.mjs" +import { loadMetadata, SeriesMetadata } from "./metadata.mjs" export default class Editor { constructor() { @@ -18,10 +19,11 @@ export default class Editor { subtitle: new TracksTable(this, "subtitle", "subtitle-tracks", "subtitle_tracks") } + this.metadata = null this.data = {} this.dirty = false - this.integrity_mgr = new IntegrityManager(this) + this.integrityMgr = new IntegrityManager(this) document.getElementById("check-integrity").addEventListener("click", () => this.checkIntegrity()) document.getElementById("improve-all").addEventListener("click", () => this.improveAllNames()) @@ -29,6 +31,14 @@ export default class Editor { document.getElementById("reload").addEventListener("click", () => window.location.reload()) document.getElementById("toggle-notifs").addEventListener("click", () => this.toggleNotifications()) document.getElementById("close-notifs").addEventListener("click", () => this.closeNotifications()) + document.getElementById("prev-episode").addEventListener("click", () => this.prevEpisode()) + document.getElementById("next-episode").addEventListener("click", () => this.nextEpisode()) + + this.titleInput = document.getElementById("title") + this.titleInput.addEventListener("change", () => { + this.data.title = this.titleInput.value + this.setDirty() + }) this.setup() } @@ -46,14 +56,27 @@ export default class Editor { return null }).then(res => { if (res !== null) { - this.data = res + this.metadata = loadMetadata(res) this.displayData() } }) } displayData() { - document.getElementById("title").value = this.data.title + const seriesToolbar = document.getElementById("series-toolbar") + if (this.metadata instanceof SeriesMetadata) { + seriesToolbar.classList.add("show") + const cur = this.metadata.episodeIdx + 1 + const tot = this.metadata.episodes.length + const epMeta = this.metadata.getCurrentEpisode() + const season = epMeta.season + const episode = epMeta.episode + seriesToolbar.querySelector("#cur-episode").innerText = `S${season}E${episode} (${cur} / ${tot})` + } else { + seriesToolbar.classList.remove("show") + } + this.data = this.metadata.getData() + this.titleInput.value = this.data.title this.tables.audio.loadTracks(this.data.audio_tracks) this.tables.subtitle.loadTracks(this.data.subtitle_tracks) } @@ -61,7 +84,7 @@ export default class Editor { save() { fetch(`/api/file/${this.filename}`, { method: "POST", - body: JSON.stringify(this.data), + body: JSON.stringify(this.metadata.data), headers: { "Content-Type": "application/json" } @@ -100,13 +123,13 @@ export default class Editor { } checkIntegrity() { - if (this.integrity_mgr.checkIntegrity()) { + if (this.integrityMgr.checkIntegrity()) { this.notify("No integrity error detected !", "success") } } improveAllNames() { - this.integrity_mgr.improveAllNames() + this.integrityMgr.improveAllNames() this.notify("Improved all names !", "success") } @@ -128,4 +151,20 @@ export default class Editor { const hist = document.getElementById("notifs-hist") hist.classList.remove("show") } + + prevEpisode() { + if (this.metadata instanceof SeriesMetadata) { + if (this.metadata.prev()) { + this.displayData() + } + } + } + + nextEpisode() { + if (this.metadata instanceof SeriesMetadata) { + if (this.metadata.next()) { + this.displayData() + } + } + } } \ No newline at end of file diff --git a/editor/public/static/js/index.js b/editor/public/static/js/index.js index c9c646f..71fd02d 100644 --- a/editor/public/static/js/index.js +++ b/editor/public/static/js/index.js @@ -5,7 +5,7 @@ function addOptions(files) { defaultOpt.innerText = "----- Select a file -----" defaultOpt.value = "" select.appendChild(defaultOpt) - files.forEach(file => { + files.sort().forEach(file => { const option = document.createElement("option") option.innerText = file option.value = file diff --git a/editor/public/static/js/media_file.mjs b/editor/public/static/js/media_file.mjs new file mode 100644 index 0000000..6568723 --- /dev/null +++ b/editor/public/static/js/media_file.mjs @@ -0,0 +1,5 @@ +export default class MediaFile { + constructor(data) { + + } +} \ No newline at end of file diff --git a/editor/public/static/js/metadata.mjs b/editor/public/static/js/metadata.mjs new file mode 100644 index 0000000..4eb48db --- /dev/null +++ b/editor/public/static/js/metadata.mjs @@ -0,0 +1,85 @@ +export default class Metadata { + constructor(data) { + this.data = data + } + + getData() { + return this.data + } +} + +export class MediaMetadata extends Metadata { + constructor(data) { + super(data) + } +} + +export class EpisodeMetadata extends MediaMetadata { + REGEXP = /s(?\d+)e(?\d+)/i + + /** + * + * @param {object} data + * @param {string} episodeKey + */ + constructor(data, episodeKey) { + super(data) + this.key = episodeKey + + let m = this.key.match(this.REGEXP) ?? this.data.filename.match(this.REGEXP) + this.season = "xx" + this.episode = "xx" + if (m) { + this.season = m.groups.season + this.episode = m.groups.episode + } + } +} + +export class SeriesMetadata extends Metadata { + constructor(data) { + super(data) + const episodeKeys = Object.keys(data).sort() + this.episodes = episodeKeys.map(key => { + return new EpisodeMetadata(data[key], key) + }) + + this.episodeIdx = 0 + } + + getCurrentEpisode() { + return this.episodes[this.episodeIdx] + } + + getData() { + return this.getCurrentEpisode().getData() + } + + prev() { + if (this.episodeIdx === 0) { + return false + } + this.episodeIdx -= 1 + return true + } + + next() { + if (this.episodeIdx === this.episodes.length - 1) { + return false + } + this.episodeIdx += 1 + return true + } +} + +/** + * + * @param {object} data + * @returns {Metadata} + */ +export function loadMetadata(data) { + if ("filename" in data) { + return new MediaMetadata(data) + } + return new SeriesMetadata(data) +} \ No newline at end of file diff --git a/editor/public/static/js/tracks_table.mjs b/editor/public/static/js/tracks_table.mjs index 34474b2..2069323 100644 --- a/editor/public/static/js/tracks_table.mjs +++ b/editor/public/static/js/tracks_table.mjs @@ -113,6 +113,7 @@ export class Track { "warning" ) value = lang + this.editValue(field.key, value) } } @@ -140,19 +141,21 @@ export class Track { this.table.editTrack(this.idx, key, value) const input = this.row.querySelector(`[data-key='${key}']`) - const fieldType = this.table.getFieldProps(key).type - switch (fieldType) { - case "bool": - input.checked = value - break - default: - input.value = value - break + if (input) { + const fieldType = this.table.getFieldProps(key).type + switch (fieldType) { + case "bool": + input.checked = value + break + default: + input.value = value + break + } } } improveName() { - this.table.editor.integrity_mgr.improveName(this) + this.table.editor.integrityMgr.improveName(this) } } @@ -184,9 +187,10 @@ export default class TracksTable { this.table = document.getElementById(tableId) this.headers = this.table.querySelector("thead tr") this.body = this.table.querySelector("tbody") - this.fields = [] - this.tracks = [] this.dataKey = dataKey + + this.tracks = [] + this.fields = [] this.onehots = {} } @@ -195,8 +199,8 @@ export default class TracksTable { } loadTracks(tracks) { - this.tracks = tracks.map((t, i) => new Track(this, i, t)) this.clear() + this.tracks = tracks.map((t, i) => new Track(this, i, t)) if (tracks.length === 0) { return } @@ -208,9 +212,11 @@ export default class TracksTable { } clear() { + this.tracks = [] + this.fields = [] + this.onehots = {} this.headers.innerHTML = "" this.body.innerHTML = "" - this.fields = [] } detectFields() { diff --git a/editor/public/static/js/utils.mjs b/editor/public/static/js/utils.mjs index e771bbb..3b711a3 100644 --- a/editor/public/static/js/utils.mjs +++ b/editor/public/static/js/utils.mjs @@ -41,6 +41,11 @@ export const LANGUAGES = { code: "de", aliases: ["de", "ger", "german", "allemand", "deutsch", "germany", "allemagne"] }, + "ita": { + display: "Italiano", + code: "it", + aliases: ["it", "ita", "italian", "italien", "italiano", "italy", "italie"] + }, "kor": { display: "Korean", code: "kr", @@ -64,7 +69,7 @@ export const LANGUAGES = { } export function getLanguageAliases(langTag) { - return (langTag === "und" ? [] : [langTag]).concat(LANGUAGES[langTag].aliases) + return [langTag].concat(LANGUAGES[langTag].aliases) } /** diff --git a/editor/server.py b/editor/server.py index 5d67811..2fbd994 100644 --- a/editor/server.py +++ b/editor/server.py @@ -22,7 +22,6 @@ class MyHandler(SimpleHTTPRequestHandler): self.data: Optional[dict|list] = None def read_body_data(self): - self.log_message("Reading body data") try: size: int = int(self.headers["Content-Length"]) if size > MAX_SIZE: @@ -54,7 +53,7 @@ class MyHandler(SimpleHTTPRequestHandler): self.send_error(HTTPStatus.NOT_FOUND) def handle_api_get(self, path: str): - print(f"API request at {path}") + self.log_message(f"API request at {path}") if path == "files": files: list[str] = self.get_files() self.send_json(files) @@ -63,9 +62,7 @@ class MyHandler(SimpleHTTPRequestHandler): data = self.read_file(filename) 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) else: self.send_response(HTTPStatus.NOT_FOUND, f"Unknown path {path}") @@ -104,8 +101,8 @@ class MyHandler(SimpleHTTPRequestHandler): return False try: - with open(os.path.join(self.DATA_DIR, filename), "w") as f: - json.dump(data, f, indent=2) + with open(os.path.join(self.DATA_DIR, filename), "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) except: self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR) return False