198 lines
6.8 KiB
Python
198 lines
6.8 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import socketserver
|
|
import time
|
|
from functools import partial
|
|
from http import HTTPStatus
|
|
from http.server import SimpleHTTPRequestHandler
|
|
from logging import Logger
|
|
from typing import Optional
|
|
from urllib.parse import parse_qs, unquote, urlparse
|
|
|
|
from watchdog.events import (FileClosedEvent, FileDeletedEvent, FileMovedEvent,
|
|
FileSystemEventHandler)
|
|
from watchdog.observers import Observer
|
|
from watchdog.observers.api import BaseObserver
|
|
|
|
from src.file_handlers import ToConvertFileHandler, MetadataFileHandler
|
|
|
|
|
|
class HTTPHandler(SimpleHTTPRequestHandler):
|
|
def __init__(self, server: MeliesServer, *args, **kwargs):
|
|
self.server_: MeliesServer = server
|
|
self.to_convert_files: ToConvertFileHandler = self.server_.to_convert_files
|
|
self.metadata_files: MetadataFileHandler = self.server_.metadata_files
|
|
|
|
super().__init__(
|
|
*args,
|
|
directory=os.path.join(os.path.dirname(__file__), "public"),
|
|
**kwargs
|
|
)
|
|
|
|
self.query: dict = {}
|
|
self.data: Optional[dict|list] = None
|
|
|
|
def log_message(self, format, *args):
|
|
self.server_.logger.info("%s - %s" % (
|
|
self.client_address[0],
|
|
format % args
|
|
))
|
|
|
|
def read_body_data(self):
|
|
try:
|
|
size: int = int(self.headers["Content-Length"])
|
|
if size > self.server_.max_payload_size:
|
|
self.send_error(HTTPStatus.CONTENT_TOO_LARGE)
|
|
self.log_error(f"Payload is too big ({self.server_.max_payload_size=}B)")
|
|
return False
|
|
raw_data = self.rfile.read(size)
|
|
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):
|
|
parsed = urlparse(unquote(self.path))
|
|
self.path = parsed.path
|
|
self.query = parse_qs(parsed.query)
|
|
|
|
if self.path.startswith("/api/"):
|
|
self.handle_api_get(self.path.removeprefix("/api/").removesuffix("/"))
|
|
return
|
|
super().do_GET()
|
|
|
|
def do_POST(self):
|
|
parsed = urlparse(unquote(self.path))
|
|
self.path = parsed.path
|
|
self.query = parse_qs(parsed.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):
|
|
self.log_message(f"API request at {path}")
|
|
if path == "files/to_convert":
|
|
base_path: str = self.query.get("f", [""])[0]
|
|
files: list[dict] = self.to_convert_files.get_files_meta(base_path)
|
|
self.send_json(files)
|
|
|
|
elif path == "files/metadata":
|
|
files: list[dict] = self.metadata_files.get_files_meta()
|
|
self.send_json(files)
|
|
|
|
elif path.startswith("file"):
|
|
filename: str = path.split("/", 1)[1]
|
|
data = self.metadata_files.read(filename)
|
|
if data is None:
|
|
self.send_error(HTTPStatus.NOT_FOUND)
|
|
else:
|
|
self.send_json(data)
|
|
else:
|
|
self.send_response(HTTPStatus.NOT_FOUND, f"Unknown path {path}")
|
|
self.end_headers()
|
|
|
|
def handle_api_post(self, path: str):
|
|
if path.startswith("file"):
|
|
if self.read_body_data():
|
|
filename: str = path.split("/", 1)[1]
|
|
if self.metadata_files.write(filename, self.data):
|
|
self.send_response(HTTPStatus.OK)
|
|
self.end_headers()
|
|
else:
|
|
self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
else:
|
|
self.send_response(HTTPStatus.NOT_FOUND, f"Unknown path {path}")
|
|
self.end_headers()
|
|
|
|
def send_json(self, data: dict|list):
|
|
self.send_response(HTTPStatus.OK)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps(data).encode("utf-8"))
|
|
|
|
|
|
class MeliesServer(FileSystemEventHandler):
|
|
def __init__(
|
|
self,
|
|
port: int,
|
|
to_convert_dir: str,
|
|
converted_dir: str,
|
|
metadata_dir: str,
|
|
max_payload_size: int):
|
|
|
|
super().__init__()
|
|
self.logger: Logger = logging.getLogger("MeliesServer")
|
|
|
|
self.port: int = port
|
|
self.to_convert_dir: str = to_convert_dir
|
|
self.converted_dir: str = converted_dir
|
|
self.metadata_dir: str = metadata_dir
|
|
self.max_payload_size: int = max_payload_size
|
|
|
|
if not os.path.exists(self.to_convert_dir):
|
|
os.mkdir(self.to_convert_dir)
|
|
if not os.path.exists(self.converted_dir):
|
|
os.mkdir(self.converted_dir)
|
|
if not os.path.exists(self.metadata_dir):
|
|
os.mkdir(self.metadata_dir)
|
|
|
|
self.to_convert_files: ToConvertFileHandler = ToConvertFileHandler(self.to_convert_dir)
|
|
self.metadata_files: MetadataFileHandler = MetadataFileHandler(self.metadata_dir)
|
|
|
|
self.httpd: Optional[socketserver.TCPServer] = None
|
|
self.observer: BaseObserver = Observer()
|
|
self.observer.schedule(
|
|
self,
|
|
self.converted_dir,
|
|
recursive=True,
|
|
event_filter=[FileDeletedEvent, FileMovedEvent, FileClosedEvent]
|
|
)
|
|
self.last_event: float = time.time()
|
|
|
|
self.http_handler_cls = partial(HTTPHandler, self)
|
|
|
|
def start(self):
|
|
self.observer.start()
|
|
try:
|
|
with socketserver.TCPServer(("", self.port), self.http_handler_cls) as self.httpd:
|
|
self.logger.info(f"Serving on port {self.port}")
|
|
self.httpd.serve_forever()
|
|
except KeyboardInterrupt:
|
|
pass
|
|
self.stop()
|
|
|
|
def stop(self):
|
|
self.observer.stop()
|
|
self.observer.join()
|
|
|
|
def on_deleted(self, event: FileDeletedEvent):
|
|
self.logger.info(f"Converted media deleted: {event.src_path}")
|
|
self.delete_metadata(event.src_path)
|
|
return super().on_deleted(event)
|
|
|
|
def on_moved(self, event: FileMovedEvent):
|
|
self.logger.info(f"Converted media moved: {event.src_path} -> {event.dest_path}")
|
|
self.rename_metadata(event.src_path, event.dest_path)
|
|
return super().on_moved(event)
|
|
|
|
def on_closed(self, event: FileClosedEvent):
|
|
self.logger.info(f"Converted media created or modified: {event.src_path}")
|
|
self.extract_metadata(event.src_path)
|
|
return super().on_closed(event)
|
|
|
|
def extract_metadata(self, path: str):
|
|
pass
|
|
|
|
def rename_metadata(self, src: str, dst: str):
|
|
pass
|
|
|
|
def delete_metadata(self, path: str):
|
|
pass
|