diff --git a/Dockerfile b/Dockerfile index 8d841e0..174973a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,10 +27,10 @@ RUN true \ moreutils \ cowsay -COPY scripts /scripts +COPY sucker.py /usr/bin COPY abcde.conf httpd.conf /etc/ COPY --chown=linuxserver:linuxserver www /www USER linuxserver -ENTRYPOINT ["/scripts/init.sh"] +ENTRYPOINT ["/usr/bin/sucker.py"] # vi: ts=2 sw=2 et ai diff --git a/scripts/common.sh b/scripts/common.sh index 9187f1d..6a8b26e 100755 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -22,3 +22,7 @@ setenv () { && mv env.json.new env.json } +status () { + echo "$2" > status.$1.new + mv status.$1.new status.$1 +} diff --git a/scripts/encoder.sh b/scripts/encoder.sh index 3e08219..a8dd4db 100755 --- a/scripts/encoder.sh +++ b/scripts/encoder.sh @@ -3,7 +3,7 @@ . $(dirname $0)/common.sh while sleep 2; do - echo "idle" > $OUTDIR/status.encoder + status encoder idle for mtype in audio video; do ls $mtype | while read d; do encode=$SCRIPTS/$mtype.encode.sh @@ -14,7 +14,7 @@ while sleep 2; do (cd $workdir && setenv status "encode interrupted") ;; "read finished") - echo "encoding" > $OUTDIR/status.encoder + status encoder encoding (cd $workdir && setenv status "encoding") if ! (cd $workdir && $encode); then log "$encode failed" diff --git a/scripts/reader.sh b/scripts/reader.sh index 18f11bc..273c3a3 100755 --- a/scripts/reader.sh +++ b/scripts/reader.sh @@ -14,7 +14,7 @@ with_time_dir () { setenv directory "$dir" setenv status "reading" - echo "$mtype" > $OUTDIR/status.reader + tatusreader "$mtype" if ! "$@"; then log "$1 failed" setenv status "read failed" @@ -26,7 +26,7 @@ with_time_dir () { } while sleep 2; do - echo "idle" > $OUTDIR/status.reader + status reader idle case $(setcd -i) in *"Disc found in drive: audio"*) log "Found audio disc" diff --git a/scripts/status.json.sh b/scripts/status.json.sh index 4448971..ca952a8 100755 --- a/scripts/status.json.sh +++ b/scripts/status.json.sh @@ -1,10 +1,12 @@ #! /bin/sh +set -x # Why the heck is this eating 100% CPU in an unkillable state? + . $(dirname $0)/common.sh queue () { - ls audio/*/env.json video/*/env.json 2>/dev/null \ - | while read envjson + find audio video -name env.json \ + | while read envjson # This is the line that's dying at 100% CPU do dir=${envjson%/env.json} cat $envjson \ diff --git a/sucker.py b/sucker.py new file mode 100644 index 0000000..15e745a --- /dev/null +++ b/sucker.py @@ -0,0 +1,190 @@ +#! /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": [], +} + + +class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler): + def do_GET(self): + if self.path == "/status.json": + return self.get_status() + return super().do_GET() + + def get_status(self): + self.send_response(200) + self.end_headers() + buf = json.dumps(status).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") + parser.add_argument("-www", type=pathlib.Path, default="/www") + parser.add_argument("-port", type=int, default=8080) + 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() + + handler = lambda r, a, s: HTTPRequestHandler(r, a, s, directory=args.www) + httpd = http.server.ThreadingHTTPServer(('', args.port), handler) + httpd.serve_forever() + +if __name__ == "__main__": + main() + +# vi: sw=4 ts=4 et ai diff --git a/www/index.html b/www/index.html index a9a5bef..bf7b275 100644 --- a/www/index.html +++ b/www/index.html @@ -17,9 +17,8 @@
Status
- - -
+
+
diff --git a/www/sucker.mjs b/www/sucker.mjs index 4163e76..5aef909 100644 --- a/www/sucker.mjs +++ b/www/sucker.mjs @@ -5,26 +5,28 @@ async function update() { let resp = await fetch("status.json", {cache: "no-store"}) let s = await resp.json() - for (let activity of ["reader", "encoder"]) { - let val = s.status[activity] - let e = document.querySelector(`.status .${activity}`) - e.textContent = val - if (val == "idle") { - e.classList.add("is-hidden") - } else { - e.classList.remove("is-hidden") - } - } + let jobs = document.querySelector(".workers.jobs") - let qtmpl = document.querySelector("template.job-item").content - let qelem = document.querySelector(".jobs") - while (qelem.firstChild) qelem.firstChild.remove() - for (let qitem of s.queue) { - let e = qtmpl.cloneNode(true) - e.querySelector(".job-title").textContent = qitem.title - e.querySelector(".job-status").textContent = qitem.status - e.querySelector("progress").value = qitem.complete - qelem.append(e) + while (jobs.firstChild) jobs.firstChild.remove() + + for (let worker of s.workers) { + if (worker.state != "idle") { + let job = jobs.appendChild(document.createElement("div")) + + let tag = job.appendChild(document.createElement("span")) + tag.classList.add("tag", "is-info") + tag.textContent = worker.type + + let txt = job.appendChild(document.createElement("span")) + txt.textContent = worker.title + txt.classList.add("mx-1") + + if (worker.complete) { + let progress = job.appendChild(document.createElement("progress")) + progress.classList.add("is-primary") + progress.value = worker.complete + } + } } let fileItem = document.querySelector("template.panel-file-item").content