Transition to Python to work around some weird kernel bug

This commit is contained in:
Neale Pickett 2022-01-17 20:55:16 -07:00
parent dbfd5b2d57
commit 22304132f3
8 changed files with 227 additions and 30 deletions

View File

@ -27,10 +27,10 @@ RUN true \
moreutils \ moreutils \
cowsay cowsay
COPY scripts /scripts COPY sucker.py /usr/bin
COPY abcde.conf httpd.conf /etc/ COPY abcde.conf httpd.conf /etc/
COPY --chown=linuxserver:linuxserver www /www COPY --chown=linuxserver:linuxserver www /www
USER linuxserver USER linuxserver
ENTRYPOINT ["/scripts/init.sh"] ENTRYPOINT ["/usr/bin/sucker.py"]
# vi: ts=2 sw=2 et ai # vi: ts=2 sw=2 et ai

View File

@ -22,3 +22,7 @@ setenv () {
&& mv env.json.new env.json && mv env.json.new env.json
} }
status () {
echo "$2" > status.$1.new
mv status.$1.new status.$1
}

View File

@ -3,7 +3,7 @@
. $(dirname $0)/common.sh . $(dirname $0)/common.sh
while sleep 2; do while sleep 2; do
echo "idle" > $OUTDIR/status.encoder status encoder idle
for mtype in audio video; do for mtype in audio video; do
ls $mtype | while read d; do ls $mtype | while read d; do
encode=$SCRIPTS/$mtype.encode.sh encode=$SCRIPTS/$mtype.encode.sh
@ -14,7 +14,7 @@ while sleep 2; do
(cd $workdir && setenv status "encode interrupted") (cd $workdir && setenv status "encode interrupted")
;; ;;
"read finished") "read finished")
echo "encoding" > $OUTDIR/status.encoder status encoder encoding
(cd $workdir && setenv status "encoding") (cd $workdir && setenv status "encoding")
if ! (cd $workdir && $encode); then if ! (cd $workdir && $encode); then
log "$encode failed" log "$encode failed"

View File

@ -14,7 +14,7 @@ with_time_dir () {
setenv directory "$dir" setenv directory "$dir"
setenv status "reading" setenv status "reading"
echo "$mtype" > $OUTDIR/status.reader tatusreader "$mtype"
if ! "$@"; then if ! "$@"; then
log "$1 failed" log "$1 failed"
setenv status "read failed" setenv status "read failed"
@ -26,7 +26,7 @@ with_time_dir () {
} }
while sleep 2; do while sleep 2; do
echo "idle" > $OUTDIR/status.reader status reader idle
case $(setcd -i) in case $(setcd -i) in
*"Disc found in drive: audio"*) *"Disc found in drive: audio"*)
log "Found audio disc" log "Found audio disc"

View File

@ -1,10 +1,12 @@
#! /bin/sh #! /bin/sh
set -x # Why the heck is this eating 100% CPU in an unkillable state?
. $(dirname $0)/common.sh . $(dirname $0)/common.sh
queue () { queue () {
ls audio/*/env.json video/*/env.json 2>/dev/null \ find audio video -name env.json \
| while read envjson | while read envjson # This is the line that's dying at 100% CPU
do do
dir=${envjson%/env.json} dir=${envjson%/env.json}
cat $envjson \ cat $envjson \

190
sucker.py Normal file
View File

@ -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

View File

@ -17,9 +17,8 @@
<div class="card-header-title">Status</div> <div class="card-header-title">Status</div>
</div> </div>
<div class="card-content"> <div class="card-content">
<span class="tag reader is-info is-hidden"></span> <div class="workers tags"></div>
<span class="tag encoder is-info is-hidden"></span> <div class="workers jobs"></div>
<div class="jobs"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,26 +5,28 @@ async function update() {
let resp = await fetch("status.json", {cache: "no-store"}) let resp = await fetch("status.json", {cache: "no-store"})
let s = await resp.json() let s = await resp.json()
for (let activity of ["reader", "encoder"]) { let jobs = document.querySelector(".workers.jobs")
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 qtmpl = document.querySelector("template.job-item").content while (jobs.firstChild) jobs.firstChild.remove()
let qelem = document.querySelector(".jobs")
while (qelem.firstChild) qelem.firstChild.remove() for (let worker of s.workers) {
for (let qitem of s.queue) { if (worker.state != "idle") {
let e = qtmpl.cloneNode(true) let job = jobs.appendChild(document.createElement("div"))
e.querySelector(".job-title").textContent = qitem.title
e.querySelector(".job-status").textContent = qitem.status let tag = job.appendChild(document.createElement("span"))
e.querySelector("progress").value = qitem.complete tag.classList.add("tag", "is-info")
qelem.append(e) 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 let fileItem = document.querySelector("template.panel-file-item").content