9 Commits

8 changed files with 38 additions and 234 deletions

View File

@ -22,31 +22,6 @@
<div class="title"></div> <div class="title"></div>
<div class="episodes"><span class="num"></span> episode(s)</div> <div class="episodes"><span class="num"></span> episode(s)</div>
</a> </a>
<div class="toolbar">
<div class="tool">
<label for="sort-by">Sort by</label>
<select id="sort-by">
<option value="title">Title</option>
<option value="ts">Last modified</option>
</select>
</div>
<div class="tool">
<label for="sort-desc">Order</label>
<label class="toggle">
<input type="checkbox" id="sort-desc">
<div class="off">ASC</div>
<div class="on">DESC</div>
</label>
</div>
<div class="tool">
<label for="filter">Filter</label>
<select id="filter">
<option value="all">All</option>
<option value="film">Films</option>
<option value="series">Series</option>
</select>
</div>
</div>
<div id="files"></div> <div id="files"></div>
</main> </main>
</body> </body>

View File

@ -338,11 +338,9 @@ button.improve {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-width: 30em; max-width: 30em;
transform: translateX(0%);
transition: transform 0.5s cubic-bezier(0.22, 0.61, 0.36, 1);
&:not(.show) { &:not(.show) {
transform: translateX(100%); display: none;
} }
#close-notifs { #close-notifs {

View File

@ -4,7 +4,6 @@
grid-auto-rows: 15em; grid-auto-rows: 15em;
gap: 0.8em; gap: 0.8em;
place-items: center; place-items: center;
padding: 0.8em 0;
.file { .file {
display: flex; display: flex;
@ -20,10 +19,6 @@
padding: 0.4em; padding: 0.4em;
border-radius: 1.2em; border-radius: 1.2em;
&.hidden {
display: none;
}
&:hover { &:hover {
background-color: #f8f8f8; background-color: #f8f8f8;
} }
@ -39,58 +34,4 @@
font-weight: bold; font-weight: bold;
} }
} }
}
.toolbar {
display: flex;
gap: 1.2em;
border-bottom: solid black 1px;
padding: 0.4em 0;
.tool {
display: flex;
flex-direction: column;
gap: 0.2em;
label[for] {
font-weight: bold;
}
input, select {
font-family: inherit;
font-size: inherit;
height: 100%;
}
.toggle {
height: 2em;
border-radius: 1em;
display: grid;
grid-template-columns: 1fr 1fr;
user-select: none;
cursor: pointer;
input {
display: none;
&:not(:checked) ~ .off, &:checked ~ .on {
background-color: #6ee74a;
}
}
div {
padding: 0 0.4em;
display: grid;
place-items: center;
&.off {
border-radius: 1em 0 0 1em;
}
&.on {
border-radius: 0 1em 1em 0;
}
}
}
}
} }

View File

@ -1,5 +1,3 @@
let fileNodes = []
function makeFilm(meta) { function makeFilm(meta) {
const file = document.getElementById("film-template").cloneNode(true) const file = document.getElementById("film-template").cloneNode(true)
file.querySelector(".title").innerText = meta.title file.querySelector(".title").innerText = meta.title
@ -49,59 +47,13 @@ function addFiles(files) {
const meta = files[i] const meta = files[i]
const file = makeFile(meta) const file = makeFile(meta)
list.appendChild(file) list.appendChild(file)
fileNodes.push([meta, file])
}) })
} }
function sortFiles() {
const sortBy = document.getElementById("sort-by").value
const sortDesc = document.getElementById("sort-desc").checked
const filter = document.getElementById("filter").value
fileNodes.forEach(([meta, node]) => {
if (node.classList.contains(filter) || filter === "all") {
node.classList.remove("hidden")
} else {
node.classList.add("hidden")
}
})
let changed = false
do {
changed = false
for (let i = 0; i < fileNodes.length - 1; i++) {
/** @type {[object, HTMLElement]} */
const pair1 = fileNodes[i]
/** @type {[object, HTMLElement]} */
const pair2 = fileNodes[i + 1]
const [meta1, node1] = pair1
const [meta2, node2] = pair2
let swap = false
if (sortDesc) {
swap = !(meta1[sortBy] >= meta2[sortBy])
} else {
swap = !(meta2[sortBy] >= meta1[sortBy])
}
if (swap) {
fileNodes[i] = pair2
fileNodes[i + 1] = pair1
node2.parentElement.insertBefore(node2, node1)
changed = true
}
}
} while (changed)
}
window.addEventListener("load", () => { window.addEventListener("load", () => {
fetch("/api/files").then(res => { fetch("/api/files").then(res => {
return res.json() return res.json()
}).then(files => { }).then(files => {
addFiles(files) addFiles(files)
sortFiles()
}) })
document.getElementById("sort-by").addEventListener("change", () => sortFiles())
document.getElementById("sort-desc").addEventListener("click", () => sortFiles())
document.getElementById("filter").addEventListener("change", () => sortFiles())
}) })

View File

@ -0,0 +1,5 @@
export default class MediaFile {
constructor(data) {
}
}

75
editor/server.py Executable file → Normal file
View File

@ -1,38 +1,15 @@
#!/usr/bin/env python3 from http import HTTPStatus
from http.server import SimpleHTTPRequestHandler
import argparse
import json import json
import os import os
import socketserver import socketserver
from http import HTTPStatus
from http.server import SimpleHTTPRequestHandler
from typing import Optional from typing import Optional
from urllib.parse import parse_qs, unquote, urlparse from urllib.parse import urlparse, parse_qs, unquote
# https://stackoverflow.com/a/10551190/11109181
class EnvDefault(argparse.Action):
def __init__(self, envvar, required=True, default=None, help=None, **kwargs):
if envvar:
if envvar in os.environ:
default = os.environ[envvar]
if required and default is not None:
required = False
if default is not None and help is not None:
help += f" (default: {default})"
if envvar and help is not None:
help += f"\nCan also be specified through the {envvar} environment variable"
super(EnvDefault, self).__init__(default=default, required=required, help=help,
**kwargs)
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values)
PORT = 8000
MAX_SIZE = 10e6
class MyHandler(SimpleHTTPRequestHandler): class MyHandler(SimpleHTTPRequestHandler):
MAX_PAYLOAD_SIZE = 1e6
DATA_DIR = "metadata" DATA_DIR = "metadata"
CACHE = {} CACHE = {}
@ -48,9 +25,9 @@ class MyHandler(SimpleHTTPRequestHandler):
def read_body_data(self): def read_body_data(self):
try: try:
size: int = int(self.headers["Content-Length"]) size: int = int(self.headers["Content-Length"])
if size > self.MAX_PAYLOAD_SIZE: if size > MAX_SIZE:
self.send_error(HTTPStatus.CONTENT_TOO_LARGE) self.send_error(HTTPStatus.CONTENT_TOO_LARGE)
self.log_error(f"Payload is too big ({self.MAX_PAYLOAD_SIZE=}B)") self.log_error(f"Payload is too big ({MAX_SIZE=}B)")
return False return False
raw_data = self.rfile.read(size) raw_data = self.rfile.read(size)
self.data = json.loads(raw_data) self.data = json.loads(raw_data)
@ -172,42 +149,8 @@ class MyHandler(SimpleHTTPRequestHandler):
def main(): def main():
parser = argparse.ArgumentParser( with socketserver.TCPServer(("", PORT), MyHandler) as httpd:
description="Starts the Melies server", print(f"Serving on port {PORT}")
formatter_class=argparse.RawTextHelpFormatter
)
parser.add_argument(
"-p", "--port",
action=EnvDefault,
envvar="MELIES_PORT",
default=8000,
type=int,
help="Port on which the server listens"
)
parser.add_argument(
"--max-payload-size",
action=EnvDefault,
envvar="MELIES_MAX_PAYLOAD_SIZE",
default=1e6,
type=int,
help="Maximum POST payload size in bytes that the server accepts"
)
parser.add_argument(
"--metadata-dir",
action=EnvDefault,
envvar="MELIES_METADATA_DIR",
default="metadata",
help="Path to the directory containing metadata files"
)
args = parser.parse_args()
port = args.port
MyHandler.MAX_PAYLOAD_SIZE = args.max_payload_size
MyHandler.DATA_DIR = args.metadata_dir
with socketserver.TCPServer(("", port), MyHandler) as httpd:
print(f"Serving on port {port}")
httpd.serve_forever() httpd.serve_forever()

View File

@ -55,6 +55,7 @@ def encode(input_file, codec, remove_source=False, save_log=False):
"ffmpeg", "ffmpeg",
"-i", input_file, "-i", input_file,
"-map", "0", "-map", "0",
"-cq", str(cq),
] + extra_params + [ ] + extra_params + [
"-c:v", ffmpeg_codec, "-c:v", ffmpeg_codec,
"-preset", "p4", "-preset", "p4",

View File

@ -55,6 +55,8 @@ def get_video_metadata(file_path):
"channels": stream.get("channels", 0), "channels": stream.get("channels", 0),
"flags": { "flags": {
"default": stream.get("disposition", {}).get("default", 0) == 1, "default": stream.get("disposition", {}).get("default", 0) == 1,
"forced": stream.get("disposition", {}).get("forced", 0) == 1,
"hearing_impaired": stream.get("disposition", {}).get("hearing_impaired", 0) == 1,
"visual_impaired": stream.get("disposition", {}).get("visual_impaired", 0) == 1, "visual_impaired": stream.get("disposition", {}).get("visual_impaired", 0) == 1,
"original": stream.get("disposition", {}).get("original", 0) == 1, "original": stream.get("disposition", {}).get("original", 0) == 1,
"commentary": stream.get("disposition", {}).get("comment", 0) == 1 "commentary": stream.get("disposition", {}).get("comment", 0) == 1
@ -71,6 +73,7 @@ def get_video_metadata(file_path):
"default": stream.get("disposition", {}).get("default", 0) == 1, "default": stream.get("disposition", {}).get("default", 0) == 1,
"forced": stream.get("disposition", {}).get("forced", 0) == 1, "forced": stream.get("disposition", {}).get("forced", 0) == 1,
"hearing_impaired": stream.get("disposition", {}).get("hearing_impaired", 0) == 1, "hearing_impaired": stream.get("disposition", {}).get("hearing_impaired", 0) == 1,
"visual_impaired": stream.get("disposition", {}).get("visual_impaired", 0) == 1,
"original": stream.get("disposition", {}).get("original", 0) == 1, "original": stream.get("disposition", {}).get("original", 0) == 1,
"commentary": stream.get("disposition", {}).get("comment", 0) == 1 "commentary": stream.get("disposition", {}).get("comment", 0) == 1
} }
@ -83,13 +86,13 @@ def get_video_metadata(file_path):
print(f"❌ Error processing {file_path}: {str(e)}") print(f"❌ Error processing {file_path}: {str(e)}")
return None return None
def process_file(file_path, output_dir=None): def process_file(file_path, output_file=None):
""" """
Process a single video file and write metadata to JSON. Process a single video file and write metadata to JSON.
Args: Args:
file_path (str): Path to the video file file_path (str): Path to the video file
output_dir (str, optional): Directory where the output JSON file will be saved output_file (str, optional): Path to output JSON file
""" """
if not os.path.isfile(file_path): if not os.path.isfile(file_path):
print(f"❌ File not found: {file_path}") print(f"❌ File not found: {file_path}")
@ -103,34 +106,27 @@ def process_file(file_path, output_dir=None):
metadata = get_video_metadata(file_path) metadata = get_video_metadata(file_path)
if metadata: if metadata:
# Generate output filename based on input file if not output_file:
filename = os.path.basename(os.path.splitext(file_path)[0]) + "_metadata.json" # Generate output filename based on input file
if output_dir:
# Ensure output directory exists
os.makedirs(output_dir, exist_ok=True)
output_path = os.path.join(output_dir, filename)
else:
# If no output directory specified, save in the same directory as the input file
base_name = os.path.splitext(file_path)[0] base_name = os.path.splitext(file_path)[0]
output_path = f"{base_name}_metadata.json" output_file = f"{base_name}_metadata.json"
# Write metadata to JSON file # Write metadata to JSON file
with open(output_path, 'w', encoding='utf-8') as f: with open(output_file, 'w', encoding='utf-8') as f:
json.dump(metadata, f, indent=2, ensure_ascii=False) json.dump(metadata, f, indent=2, ensure_ascii=False)
print(f"✅ Metadata saved to {output_path}") print(f"✅ Metadata saved to {output_file}")
return True return True
return False return False
def process_directory(directory_path, output_dir=None): def process_directory(directory_path, output_file=None):
""" """
Process all video files in a directory and write metadata to JSON. Process all video files in a directory and write metadata to JSON.
Args: Args:
directory_path (str): Path to the directory directory_path (str): Path to the directory
output_dir (str, optional): Directory where the output JSON file will be saved output_file (str, optional): Path to output JSON file
""" """
if not os.path.isdir(directory_path): if not os.path.isdir(directory_path):
print(f"❌ Directory not found: {directory_path}") print(f"❌ Directory not found: {directory_path}")
@ -156,38 +152,31 @@ def process_directory(directory_path, output_dir=None):
print(f"❌ No supported video files found in {directory_path}") print(f"❌ No supported video files found in {directory_path}")
return False return False
# Generate output filename based on directory name if not output_file:
dir_name = os.path.basename(os.path.normpath(directory_path)) # Generate output filename based on directory name
filename = f"{dir_name}_metadata.json" dir_name = os.path.basename(os.path.normpath(directory_path))
output_file = f"{dir_name}_metadata.json"
if output_dir:
# Ensure output directory exists
os.makedirs(output_dir, exist_ok=True)
output_path = os.path.join(output_dir, filename)
else:
# If no output directory specified, save in the current directory
output_path = filename
# Write all metadata to a single JSON file # Write all metadata to a single JSON file
with open(output_path, 'w', encoding='utf-8') as f: with open(output_file, 'w', encoding='utf-8') as f:
json.dump(all_metadata, f, indent=2, ensure_ascii=False) json.dump(all_metadata, f, indent=2, ensure_ascii=False)
print(f"✅ Metadata for {file_count} files saved to {output_path}") print(f"✅ Metadata for {file_count} files saved to {output_file}")
return True return True
def main(): def main():
parser = argparse.ArgumentParser(description="Extract metadata from video files and save as JSON.") parser = argparse.ArgumentParser(description="Extract metadata from video files and save as JSON.")
parser.add_argument("input", help="Path to input video file or directory") parser.add_argument("input", help="Path to input video file or directory")
parser.add_argument("-o", "--output", help="Directory path where output JSON files will be saved") parser.add_argument("-o", "--output", help="Path to output JSON file")
args = parser.parse_args() args = parser.parse_args()
input_path = args.input input_path = args.input
output_dir = args.output output_file = args.output
if os.path.isfile(input_path): if os.path.isfile(input_path):
process_file(input_path, output_dir) process_file(input_path, output_file)
elif os.path.isdir(input_path): elif os.path.isdir(input_path):
process_directory(input_path, output_dir) process_directory(input_path, output_file)
else: else:
print(f"❌ Path not found: {input_path}") print(f"❌ Path not found: {input_path}")
sys.exit(1) sys.exit(1)