15 Commits

Author SHA1 Message Date
658addae56 feat: add CLI arguments + env variables support 2025-05-03 16:07:24 +02:00
79cd7a32ed feat: add sorting and filtering in file list 2025-05-03 16:07:23 +02:00
02279b8c6f feat: rework home page file selector 2025-05-03 16:07:23 +02:00
ed3c6d7cc7 feat: add support for series 2025-05-03 16:07:23 +02:00
bc5371de71 feat: add toolbar + notifications 2025-05-03 16:07:22 +02:00
2afecd1c04 feat: add improve name button 2025-05-03 16:07:22 +02:00
8fbe5ae3c4 fix: add gitignore 2025-05-03 16:07:21 +02:00
acf7b5047f feat: add basic integrity checks + corrections 2025-05-03 16:07:21 +02:00
82d02cfe76 refactor: split editor in JS modules 2025-05-03 16:07:21 +02:00
d19ab90f38 feat: add saving 2025-05-03 16:07:20 +02:00
477e8951a9 feat: add hotone bool 2025-05-03 16:07:20 +02:00
5110f41152 Merge pull request 'fix/improve-gpt-script' (#1) from fix/improve-gpt-script into main
Reviewed-on: Klagarge/medias-migration#1
2025-05-03 13:24:47 +00:00
2de5044dfe feat: output directory path by output flag
BREAKING-CHANGE: output flag use now directory path instead of output file
2025-05-03 15:22:31 +02:00
9d4f4319c2 fix: remove unnecessary flags from metadata extraction 2025-05-03 15:19:43 +02:00
ca24665574 fix: remove useless cq
The constant quality parameter is already present in the command
2025-05-03 15:19:00 +02:00
8 changed files with 234 additions and 38 deletions

View File

@ -22,6 +22,31 @@
<div class="title"></div>
<div class="episodes"><span class="num"></span> episode(s)</div>
</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>
</main>
</body>

View File

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

View File

@ -4,6 +4,7 @@
grid-auto-rows: 15em;
gap: 0.8em;
place-items: center;
padding: 0.8em 0;
.file {
display: flex;
@ -19,6 +20,10 @@
padding: 0.4em;
border-radius: 1.2em;
&.hidden {
display: none;
}
&:hover {
background-color: #f8f8f8;
}
@ -34,4 +39,58 @@
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,3 +1,5 @@
let fileNodes = []
function makeFilm(meta) {
const file = document.getElementById("film-template").cloneNode(true)
file.querySelector(".title").innerText = meta.title
@ -47,13 +49,59 @@ function addFiles(files) {
const meta = files[i]
const file = makeFile(meta)
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", () => {
fetch("/api/files").then(res => {
return res.json()
}).then(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

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

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

@ -1,15 +1,38 @@
from http import HTTPStatus
from http.server import SimpleHTTPRequestHandler
#!/usr/bin/env python3
import argparse
import json
import os
import socketserver
from http import HTTPStatus
from http.server import SimpleHTTPRequestHandler
from typing import Optional
from urllib.parse import urlparse, parse_qs, unquote
from urllib.parse import parse_qs, unquote, urlparse
# 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):
MAX_PAYLOAD_SIZE = 1e6
DATA_DIR = "metadata"
CACHE = {}
@ -25,9 +48,9 @@ class MyHandler(SimpleHTTPRequestHandler):
def read_body_data(self):
try:
size: int = int(self.headers["Content-Length"])
if size > MAX_SIZE:
if size > self.MAX_PAYLOAD_SIZE:
self.send_error(HTTPStatus.CONTENT_TOO_LARGE)
self.log_error(f"Payload is too big ({MAX_SIZE=}B)")
self.log_error(f"Payload is too big ({self.MAX_PAYLOAD_SIZE=}B)")
return False
raw_data = self.rfile.read(size)
self.data = json.loads(raw_data)
@ -149,8 +172,42 @@ class MyHandler(SimpleHTTPRequestHandler):
def main():
with socketserver.TCPServer(("", PORT), MyHandler) as httpd:
print(f"Serving on port {PORT}")
parser = argparse.ArgumentParser(
description="Starts the Melies server",
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()

View File

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

View File

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