From c8b4d570274a34e9c896938ab2bf5cc7b8e4b2b0 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sat, 26 Feb 2022 08:05:20 -0700 Subject: [PATCH] Possibly working? --- .gitignore | 1 + encoder.py | 89 +++++++++++++++++++++++++++ reader.py | 146 ++++++++++++++++++++++++++++++++++++++++++++ statuser.py | 28 +++++++++ sucker.py | 173 ++++++---------------------------------------------- 5 files changed, 281 insertions(+), 156 deletions(-) create mode 100644 .gitignore create mode 100644 encoder.py create mode 100644 reader.py create mode 100644 statuser.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/encoder.py b/encoder.py new file mode 100644 index 0000000..7aad220 --- /dev/null +++ b/encoder.py @@ -0,0 +1,89 @@ +#! /usr/bin/python3 + +import os +import threading +import subprocess +import glob +import os +import json +import io +import shutil +import time +import re + +class Encoder(threading.Thread): + def __init__(self, directory=None, **kwargs): + self.status = {} + self.directory = directory + for d in ("audio", "video"): + os.makedirs(os.path.join(directory, d), exist_ok=True) + return super().__init__(**kwargs) + + def run(self): + while True: + wait = True + self.status = {"type": "encoder", "state": "idle"} + for mtype in ("audio", "video"): + for fn in glob.glob(os.path.join(self.directory, mtype, "*", "sucker.json")): + fdir = os.path.dirname(fn) + with open(fn) as f: + obj = json.load(f) + self.encode(mtype, fdir, obj) + wait = False + if wait: + time.sleep(12) + + def encode(self, mtype, fdir, obj): + if mtype == "audio": + self.encode_audio(fdir, obj) + else: + self.encode_video(fdir, obj) + shutil.rmtree(fdir) + + def encode_audio(self, fdir, obj): + print("Not implemented") + + def encode_video(self, fdir, obj): + self.status["state"] = "encoding" + title = os.path.basename(fdir) + self.status["title"] = title + print("encoding", title, fdir) + + outfn = "%s.mkv" % title + tmppath = os.path.join(fdir, outfn) + outpath = os.path.join(self.directory, outfn) + p = subprocess.Popen( + [ + "HandBrakeCLI", + "--json", + "-i", "%s/VIDEO_TS" % fdir, + "--main-feature", + "--native-language", "eng", + "-Z", "Chromecast 1080p30 Surround", + "-o", tmppath, + ], + encoding="utf-8", + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ) + + # HandBrakeCLI spits out sort of JSON. + # But Python has no built-in way to stream JSON objects. + # Hence this kludge. + progressRe = re.compile(r'^"Progress": ([0-9.]+),') + for line in p.stdout: + line = line.strip() + m = progressRe.search(line) + if m: + progress = float(m[1]) + self.status["complete"] = progress + + os.rename( + src=tmppath, + dst=outpath, + ) + + + pass + +# vi: sw=4 ts=4 et ai diff --git a/reader.py b/reader.py new file mode 100644 index 0000000..cc98085 --- /dev/null +++ b/reader.py @@ -0,0 +1,146 @@ +#! /usr/bin/python3 + +import os +import threading +import subprocess +import time +import re +import fcntl + +CDROM_DRIVE_STATUS = 0x5326 +CDS_NO_INFO = 0 +CDS_NO_DISC = 1 +CDS_TRAY_OPEN = 2 +CDS_DRIVE_NOT_READ =3 +CDS_DISC_OK = 4 +CDS_STR = ["no info", "no disc", "tray open", "drive not read", "disc ok"] + +CDROM_DISC_STATUS = 0x5327 +CDS_AUDIO = 100 +CDS_DATA_1 = 101 +CDS_DATA_2 = 102 + +CDROM_LOCKDOOR = 0x5329 +CDROM_EJECT = 0x5309 + +class Reader(threading.Thread): + def __init__(self, device, directory=None, **kwargs): + self.device = device + self.directory = directory + self.status = { + "type": "reader", + "state": "idle", + "device": self.device, + } + self.complete = 0 + self.drive = os.open(device, os.O_RDONLY | os.O_NONBLOCK) + return super().__init__(**kwargs) + + def run(self): + while True: + self.status["state"] = "idle" + rv = fcntl.ioctl(self.drive, CDROM_DRIVE_STATUS) + if rv == CDS_DISC_OK: + rv = fcntl.ioctl(self.drive, CDROM_DISC_STATUS) + try: + if rv == CDS_AUDIO: + self.handle_audio() + elif rv in [CDS_DATA_1, CDS_DATA_2]: + self.handle_data() + else: + print("Can't handle disc type %d" % rv) + except Exception as e: + print("Error in disc handler:", e) + self.eject() + else: + time.sleep(3) + + def eject(self): + self.status["state"] = "ejecting" + for i in range(20): + try: + fcntl.ioctl(self.drive, CDROM_LOCKDOOR, 0) + fcntl.ioctl(self.drive, CDROM_EJECT) + return + except Exception as e: + time.sleep(i * 5) + + def finished(self, **kwargs): + self.status["state"] = "finished read" + fn = os.path.join(self.directory, "video", self.status["title"], "sucker.json") + newfn = fn + ".new" + with open(newfn, "wb") as fout: + json.dump(obj=self.status, fp=fout) + os.rename(src=newfn, dst=fn) + + def handle_audio(self): + pass # XXX + + def handle_data(self): + self.video_scan() + self.video_copy() + self.finished() + + def video_scan(self): + self.status["state"] = "Scanning for DVD title" + p = subprocess.run( + [ + "dvdbackup", + "--input=" + self.path, + "--info", + ], + encoding="utf-8", + capture_output=True, + ) + mediaSize = 0 + title = "Unknown DVD" + for l in p.stdout.split("\n"): + if l.startswith("DVD-Video information"): + title = l.split('"')[1] + elif l.endswith("MiB"): + parts = l.split() + mediaSize += float(parts[-2]) * 1024 * 1024 + elif l.endswith("KiB"): + parts = l.split() + mediaSize += float(parts[-2]) * 1024 + self.status["title"] = title + if mediaSize == 0: + print("Media size = 0; aborting") + return + self.status["size"] = mediaSize + + def video_copy(self): + self.status["state"] = "copying" + + mediaSize = self.status["size"] + title = self.status["title"] + + basedir = os.path.join(self.directory, "video") + p = subprocess.Popen( + [ + "dvdbackup", + "--input=" + self.path, + "--name=" + title, + "--mirror", + "--progress", + ], + encoding="utf-8", + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=basedir, + ) + totalBytes = titleSize = lastTitleSize = 0 + progressRe = re.compile(r"^Copying.*([0-9.]+)/[0-9.]+ (MiB|KiB)") + for line in p.stdout: + line = line.strip() + m = progressRe.search(line) + if m and m[2] == "MiB": + titleSize = float(m[1]) * 1024 * 1024 + elif m and m[2] == "KiB": + titleSize = float(m[1]) * 1024 + if titleSize < lastTitleSize: + totalBytes += lastTitleSize + lastTitleSize = titleSize + self.status["complete"] = (totalBytes + titleSize) / mediaSize + +# vi: sw=4 ts=4 et ai diff --git a/statuser.py b/statuser.py new file mode 100644 index 0000000..66f9dcb --- /dev/null +++ b/statuser.py @@ -0,0 +1,28 @@ +#! /usr/bin/python3 + +import threading +import json +import glob +import time +import os + +class Statuser(threading.Thread): + def __init__(self, workers, directory=None, **kwargs): + self.workers = workers + self.directory = directory + self.status = {} + super().__init__(**kwargs) + + def run(self): + while True: + self.status["finished"] = { + "video": glob.glob(os.path.join(self.directory, "*.mkv")), + "audio": glob.glob(os.path.join(self.directory, "*/*/*.mp3")), + } + self.status["workers"] = [w.status for w in self.workers] + time.sleep(12) + + def json(self): + return json.dumps(self.status) + +# vi: sw=4 ts=4 et ai diff --git a/sucker.py b/sucker.py index 15e745a..9325c84 100644 --- a/sucker.py +++ b/sucker.py @@ -1,39 +1,16 @@ #! /usr/bin/python3 -import os import argparse -import threading -import subprocess import http.server import pathlib -import json -import glob -import io -import time -import re -import fcntl - -CDROM_DRIVE_STATUS = 0x5326 -CDS_NO_INFO = 0 -CDS_NO_DISC = 1 -CDS_TRAY_OPEN = 2 -CDS_DRIVE_NOT_READ =3 -CDS_DISC_OK = 4 -CDS_STR = ["no info", "no disc", "tray open", "drive not read", "disc ok"] - -CDROM_DISC_STATUS = 0x5327 -CDS_AUDIO = 100 -CDS_DATA_1 = 101 -CDS_DATA_2 = 102 - -CDROM_EJECT = 0x5309 - -status = { - "queue": [], -} +import reader, encoder, statuser class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler): + def __init__(self, r, a, s, directory, statuser): + self.statuser = statuser + super().__init__(r, a, s, directory=directory) + def do_GET(self): if self.path == "/status.json": return self.get_status() @@ -42,129 +19,9 @@ class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler): def get_status(self): self.send_response(200) self.end_headers() - buf = json.dumps(status).encode("utf-8") + buf = self.statuser.json().encode("utf-8") self.wfile.write(buf) -class Reader(threading.Thread): - def __init__(self, path, directory=None, **kwargs): - self.path = path - self.directory = directory - self.status = {"state": "idle"} - self.complete = 0 - self.drive = os.open(path, os.O_RDONLY | os.O_NONBLOCK) - return super().__init__(**kwargs) - - def run(self): - while True: - self.status = { - "type": "reader", - "state": "idle", - "device": self.path, - } - rv = fcntl.ioctl(self.drive, CDROM_DRIVE_STATUS) - if rv == CDS_DISC_OK: - rv = fcntl.ioctl(self.drive, CDROM_DISC_STATUS) - try: - if rv == CDS_AUDIO: - self.handle_audio() - elif rv in [CDS_DATA_1, CDS_DATA_2]: - self.handle_data() - else: - print("Can't handle disc type %d" % rv) - except Exception as e: - print("Error in disc handler:", e) - fcntl.ioctl(self.drive, CDROM_EJECT) - else: - time.sleep(3) - - def handle_audio(self): - pass # XXX - - def handle_data(self): - self.status["state"] = "Scanning for DVD title" - p = subprocess.run( - [ - "dvdbackup", - "--input=" + self.path, - "--info", - ], - encoding="utf-8", - capture_output=True, - ) - mediaSize = 0 - title = None - for l in p.stdout.split("\n"): - if l.startswith("DVD-Video information"): - title = l.split('"')[1] - elif l.endswith("MiB"): - parts = l.split() - mediaSize += float(parts[-2]) * 1024 * 1024 - elif l.endswith("KiB"): - parts = l.split() - mediaSize += float(parts[-2]) * 1024 - self.status["title"] = title - print("Copying %r (%d bytes)" % (title, mediaSize)) - - self.copy(mediaSize) - open(os.path.join(title, "finished"), "w") - - def copy(self, mediaSize): - self.status["state"] = "copying" - p = subprocess.Popen( - [ - "dvdbackup", - "--input=" + self.path, - "--mirror", - "--progress", - ], - encoding="utf-8", - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - cwd=os.path.join(self.directory, "video"), - ) - totalBytes = titleSize = lastTitleSize = 0 - progressRe = re.compile(r"^Copying.*([0-9.]+)/[0-9.]+ (MiB|KiB)") - for line in p.stdout: - line = line.strip() - m = progressRe.search(line) - if m and m[2] == "MiB": - titleSize = float(m[1]) * 1024 * 1024 - elif m and m[2] == "KiB": - titleSize = float(m[1]) * 1024 - if titleSize < lastTitleSize: - totalBytes += lastTitleSize - lastTitleSize = titleSize - self.status["complete"] = (totalBytes + titleSize) / mediaSize - - -class Encoder(threading.Thread): - def __init__(self, directory=None, **kwargs): - self.status = {"state": "idle"} - self.directory = directory - for d in ("audio", "video"): - os.makedirs(os.path.join(directory, d), exist_ok=True) - return super().__init__(**kwargs) - - def run(self): - self.status = {"type": "encoder", "state": "idle"} - - -class Statuser(threading.Thread): - def __init__(self, workers, directory=None, **kwargs): - self.workers = workers - self.directory = directory - super().__init__(**kwargs) - - def run(self): - while True: - status["finished"] = { - "video": glob.glob(os.path.join(self.directory, "*.mkv")), - "audio": glob.glob(os.path.join(self.directory, "*/*/*.mp3")), - } - status["workers"] = [w.status for w in self.workers] - time.sleep(12) - - def main(): parser = argparse.ArgumentParser(description="Rip/encode optical media") parser.add_argument("-incoming", type=pathlib.Path, default="/incoming") @@ -173,14 +30,18 @@ def main(): parser.add_argument("-drive", nargs="+", default=["/dev/sr0"]) args = parser.parse_args() - readers = [Reader(d, directory=args.incoming, daemon=True) for d in args.drive] - encoder = Encoder(directory=args.incoming, daemon=True) - statuser = Statuser(readers + [encoder], directory=args.incoming, daemon=True) - [r.start() for r in readers] - encoder.start() - statuser.start() + readers = [] + for d in args.drive: + readers.append(reader.Reader(d, directory=args.incoming, daemon=True)) + encoders = [] + for i in range(1): + encoders.append(encoder.Encoder(directory=args.incoming, daemon=True)) + st = statuser.Statuser(readers + encoders, directory=args.incoming, daemon=True) - handler = lambda r, a, s: HTTPRequestHandler(r, a, s, directory=args.www) + [w.start() for w in readers + encoders] + st.start() + + handler = lambda r, a, s: HTTPRequestHandler(r, a, s, directory=args.www, statuser=st) httpd = http.server.ThreadingHTTPServer(('', args.port), handler) httpd.serve_forever()