Compare commits

..

No commits in common. "a559e92d3e96bd882003b193b1511ee9f2da46c4" and "532fc4ec230e2e9f5866dc46275e0e5f4e42f1a3" have entirely different histories.

11 changed files with 287 additions and 405 deletions

View File

@ -10,7 +10,6 @@
"gnudb", "gnudb",
"newfn", "newfn",
"RDONLY", "RDONLY",
"cdparanoia",
"TTITLE" "TTITLE"
] ]
} }

View File

@ -7,17 +7,24 @@ RUN true \
&& sed -i 's/main$/main contrib non-free/' /etc/apt/sources.list \ && sed -i 's/main$/main contrib non-free/' /etc/apt/sources.list \
&& apt-get -y update \ && apt-get -y update \
&& DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends -y install \ && DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends -y install \
ffmpeg \
handbrake-cli libavcodec-extra \
abcde eyed3 \
glyrc setcd eject \
dvdbackup \ dvdbackup \
libdvd-pkg libdvdcss2 \ libdvd-pkg libdvdcss2 \
handbrake-cli libavcodec-extra \
cd-discid cdparanoia lame \
python3 \ python3 \
python3-slugify \ cowsay \
&& true && true
RUN dpkg-reconfigure libdvd-pkg RUN dpkg-reconfigure libdvd-pkg
RUN true \ RUN true \
&& DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends -y install \ && DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends -y install \
lame \
busybox \
jq \
procps \
moreutils \
cowsay cowsay
COPY src/* /app/ COPY src/* /app/

View File

@ -11,23 +11,9 @@ and then re-encode the content to a compressed format.
At the time I'm writing this README, it will: At the time I'm writing this README, it will:
* Rip audio CDs, look them up in cddb, encode them to VBR MP3, then tag them. * ~~Rip audio CDs, look them up in cddb, encode them to VBR MP3, then tag them.~~ A rewrite broke this; I plan to fix it soon.
* It also writes a shell script you can modify to quickly change the tags, since this is a pretty common thing to want to do.
* Rip video DVDs, transcode them to mkv * Rip video DVDs, transcode them to mkv
## Requirements
The requirements are fairly light: a few CD tools, cdparanoia, HandBrakeCLI, and some
DVD libraries.
Most notably, you do *not* need a relational database (SQLite, Postgres, MySQL).
You just need a file system.
For a complete list of requirements,
look at the [Dockerfile](Dockerfile)
to see what Debian packages it installs.
## How To Run This ## How To Run This
You need a place to store your stuff. You need a place to store your stuff.
@ -41,8 +27,9 @@ Mine is `/srv/ext/incoming`.
-v /srv/ext/incoming:/incoming \ -v /srv/ext/incoming:/incoming \
registry.gitlab.com/dartcatcher/media-sucker/media-sucker registry.gitlab.com/dartcatcher/media-sucker/media-sucker
I can't get it to work with docker swarm, I can't get it to work with docker swarm.
which doesn't support `--device`. Presumably some magic is happening with `--device`.
It probably has something to do with selinux.
Stick a video DVD or audio CD in, Stick a video DVD or audio CD in,
and the drive should spin up for a while, and the drive should spin up for a while,
@ -52,14 +39,9 @@ or a new directory of `.mp3` files (for audio).
You can watch what it's doing at http://localhost:8080/ You can watch what it's doing at http://localhost:8080/
## A note on filenames and tags ## A note on filenames and tags
This program does the absolute minimum to try and tag your media properly. This program does the absolute minimum to try and tag your media properly.
Partly because I'm a lazy programmer,
but mostly because the computer can only guess at things that you,
the operator,
can just read off the box.
For DVDs, that means reading the "title" stored on the DVD, For DVDs, that means reading the "title" stored on the DVD,
which I've seen vary from very helpful (eg. "Barbie A Fashion Fairytale") which I've seen vary from very helpful (eg. "Barbie A Fashion Fairytale")
@ -73,10 +55,13 @@ so CDDB takes the length of every track in seconds and tries to match that
against something a user has uploaded in the past. against something a user has uploaded in the past.
This is wrong a whole lot of the time. This is wrong a whole lot of the time.
But the end result in almost every case is that you're going to have to If CDDB can't find a match for an audio CD,
rename the movie file, or re-tag the audio files. this program will append the datestamp of the rip to the album name,
This is why you get a `tag.sh` file with every audio CD rip. in the hopes that you can remember about what time you put each CD in the drive.
So for stuff like multi-CD audiobooks, that's pretty helpful.
But the end result in almost every case is that you're going to have to
manually edit the metadata.
## Answers ## Answers
@ -84,23 +69,35 @@ I'm skipping the part where I make up questions I think people might have.
### Why I Wrote This ### Why I Wrote This
The automatic-ripping-machine looks really badass. The `automatic-ripping-machine` looks really badass.
But after multiple attempts across multiple months But after multiple attempts across multiple months
to get it running, to get it running,
I decided it would probably be faster just to write my own. I decided it would probably be faster just to write my own.
media-sucker isn't as cool as the automatic-ripping-machine. This isn't as cool as the aumomatic-ripping-machine.
But, at least for me, But, at least for me,
it's more useful, it's a lot more functional,
in that I can get it to actually do something. in that it actually does something.
### Why You Should Run This ### Why You Should Run This
The only reason I can think of that anybody would want to use this is if they, The only reason I can think of that anybody would want to use this is if they,
like me, like me,
are too dumb to get the automatic-ripping-machine to work. are too dumb to get the `automatic-ripping-machine` to work.
### What Kind Of Hardware I Use ### What Kind Of Hardware I Use
I run it on a Raspberry Pi 4, I run it on a Raspberry Pi 4,
with a Samsung DVD drive from the stone age. with a Samsung DVD drive from the stone age.
## Parting note
As of 2022-08-22, large sections of this code were written under COVID brain-fog.
This means it's going to look a lot like a 13-year-old wrote it.
I hope one day to clean it up a bit,
but it's working fairly well,
despite the mess.
Please don't judge me for the organization of things.
Judge bizarro universe Neale instead.

View File

@ -1,54 +0,0 @@
# Web Server
There is one web server,
which provides static content,
and a single entrypoint for dynamic state information.
The static content is some HTML and JavaScript,
which the browser runs to pull the dynamic state,
and update the page with current status of everything.
# Workers
There are at least two Workers:
a Reader and an Encoder.
Each Worker runs in its own thread,
and can do its job without interfering with another Worker.
## Readers
Readers monitor a device for media.
Right now, those devices are always CD-ROM drives.
As soon as media is inserted,
a MediaHandler is created to scan and then copy it.
## Encoders
Encoders wait for jobs to show up,
and then they re-invoke a MediaHandler to encode everything in that job.
# MediaHandlers
MediaHandlers have a work directory,
where they store all their stuff.
They have the following stages of execution:
1. *scan* the media to figure out its title, list of tracks, and other metadata
2. *copy* the media to the work directory
3. *encode* the work directory into the desired format (eg. MP3, MKV)
4. *clean* the work directory
Before each step,
state is read out of the work directory.
During each step,
a MediaHandler continually updates its Worker with a completion percentage.
This is passed up to the Web Server's dynamic state.
After each step,
a MediaHandler updates its state,
which is stored on disk.
The only way to communicate state between execution stages is by writing to disk.
This provides some tolerance of job interruption, power loss, etc.

128
src/cd.py
View File

@ -13,7 +13,7 @@ SECOND = 1
MINUTE = 60 * SECOND MINUTE = 60 * SECOND
HOUR = 60 * MINUTE HOUR = 60 * MINUTE
def scan(state, device): def read(device, status):
# Get disc ID # Get disc ID
p = subprocess.run( p = subprocess.run(
[ [
@ -23,9 +23,8 @@ def scan(state, device):
encoding="utf-8", encoding="utf-8",
capture_output=True, capture_output=True,
) )
discid = p.stdout.strip() discid = p.stdout
state["discid"] = discid status["discid"] = discid
cddb_id = discid.split()[0]
# Look it up in cddb # Look it up in cddb
email = os.environ.get("EMAIL") # You should really set this variable, tho email = os.environ.get("EMAIL") # You should really set this variable, tho
@ -43,23 +42,22 @@ def scan(state, device):
# We're expected to be automatic here, # We're expected to be automatic here,
# so just use the first one. # so just use the first one.
for k in ("title", "artist", "genre", "year", "tracks"): for k in ("title", "artist", "genre", "year", "tracks"):
state[k] = disc[k] status[k] = disc[k]
else: else:
now = time.strftime("%Y-%m-%dT%H%M%S")
num_tracks = int(discid.split()[1]) num_tracks = int(discid.split()[1])
state["title"] = "Unknown CD - %s" % cddb_id status["title"] = "Unknown CD - %s" % now
state["tracks"] = ["Track %02d" % (i+1) for i in range(num_tracks)] status["tracks"] = [""] * num_tracks
def rip(device, status, directory):
def copy(state, device, directory):
# cdparanoia reports completion in samples # cdparanoia reports completion in samples
# use discid duration to figure out total number of samples # use discid duration to figure out total number of samples
duration = int(state["discid"].split()[-1]) * SECOND # disc duration in seconds duration = int(status["discid"].split()[-1]) * SECOND # disc duration in seconds
total_samples = duration * (75 / SECOND) * 1176 # 75 sectors per second, 1176 samples per sector total_samples = duration * (75 / SECOND) * 1176 # 75 sectors per second, 1176 samples per sector
state["total_samples"] = total_samples
track_num = 1 track_num = 1
for track_name in state["tracks"]: for track_name in status["tracks"]:
logging.debug("Ripping track %d of %d", track_num, len(state["tracks"])) logging.debug("Ripping track %d of %d", track_num, len(status["tracks"]))
p = subprocess.Popen( p = subprocess.Popen(
[ [
"cdparanoia", "cdparanoia",
@ -77,110 +75,56 @@ def copy(state, device, directory):
line = line.strip() line = line.strip()
if line.startswith("##: -2"): if line.startswith("##: -2"):
samples = int(line.split()[-1]) samples = int(line.split()[-1])
yield samples / total_samples status["complete"] = samples / total_samples
track_num += 1 track_num += 1
def encode(status, directory):
def encode(state, directory): # Encode the tracks
track_num = 1 track_num = 1
total_tracks = len(state["tracks"]) for track_name in status["tracks"]:
durations = [int(d) for d in state["discid"].split()[2:-1]]
total_duration = sum(durations)
encoded_duration = 0
tag_script = io.StringIO()
tag_script.write("#! /bin/sh\n")
tag_script.write("\n")
tag_script.write("ALBUM=%s\n" % state["title"])
tag_script.write("ARTIST=%s\n" % state.get("artist", ""))
tag_script.write("GENRE=%s\n" % state.get("genre", ""))
tag_script.write("YEAR=%s\n" % state.get("year", ""))
tag_script.write("\n")
for track_name in state["tracks"]:
logging.debug("Encoding track %d (%s)" % (track_num, track_name))
duration = durations[track_num-1]
argv = [ argv = [
"lame", "lame",
"--brief",
"--nohist",
"--disptime", "1",
"--preset", "standard", "--preset", "standard",
"--tl", state["title"], "-tl", status["title"],
"--tn", "%d/%d" % (track_num, total_tracks), "--tn", "%d/%d" % (track_num, len(status["tracks"])),
] ]
tag_script.write("id3v2") if status["artist"]:
tag_script.write(" --album \"$ALBUM\"") argv.extend(["-ta", status["artist"]])
tag_script.write(" --artist \"$ARTIST\"") if status["genre"]:
tag_script.write(" --genre \"$GENRE\"") argv.extend(["-tg", status["genre"]])
tag_script.write(" --year \"$YEAR\"") if status["year"]:
if state.get("artist"): argv.extend(["-ty", status["year"]])
argv.extend(["--ta", state["artist"]])
if state.get("genre"):
argv.extend(["--tg", state["genre"]])
if state.get("year"):
argv.extend(["--ty", state["year"]])
if track_name: if track_name:
argv.extend(["--tt", track_name]) argv.extend(["-tt", track_name])
tag_script.write(" --song \"%s\"" % track_name) outfn = "%d - %s.mp3" % (track_num, track_name)
outfn = "%02d - %s.mp3" % (track_num, track_name)
else: else:
outfn = "%02d.mp3" % track_num outfn = "%d.mp3" % track_num
argv.append("track%02d.cdda.wav" % track_num) argv.append("track%02d.cdda.wav" % track_num)
argv.append(outfn) argv.append(outfn)
tag_script.write("\\\n ")
tag_script.write(" --track %d/%d" % (track_num, total_tracks))
tag_script.write(" \"%s\"\n" % outfn)
p = subprocess.Popen( p = subprocess.Popen(
argv, argv,
cwd = directory, cwd = directory,
stderr = subprocess.PIPE, stdin = subprocess.PIPE,
encoding = "utf-8", encoding = "utf-8",
) )
for line in p.stderr: p.communicate(input=track_name)
line = line.strip()
if "%)" in line:
p = line.split("(")[1]
p = p.split("%")[0]
pct = int(p) / 100
yield (encoded_duration + (duration * pct)) / total_duration
encoded_duration += duration
track_num += 1 track_num += 1
with open(os.path.join(directory, "tag.sh"), "w") as f:
f.write(tag_script.getvalue())
def clean(state, directory):
for fn in os.listdir(directory):
if fn.endswith(".wav"):
os.remove(os.path.join(directory, fn))
if __name__ == "__main__": if __name__ == "__main__":
import pprint import pprint
import sys
import json
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
status = {}
read("/dev/sr0", status)
pprint.pprint(status)
state = {} directory = os.path.join(".", status["title"])
scan(state, "/dev/sr0")
pprint.pprint(state)
directory = os.path.join(".", state["title"])
os.makedirs(directory, exist_ok=True) os.makedirs(directory, exist_ok=True)
with open(os.path.join(directory, "state.json"), "w") as f: rip("/dev/sr0", status, directory)
json.dump(f, state) pprint.pprint(status)
for pct in copy(state, "/dev/sr0", directory): encode(status, directory)
sys.stdout.write("Copying: %3d%%\r" % (pct*100)) pprint.pprint(status)
pprint.pprint(state)
for pct in encode(state, directory):
sys.stdout.write("Encoding: %3d%%\r" % (pct*100))
pprint.pprint(state)
# vi: sw=4 ts=4 et ai # vi: sw=4 ts=4 et ai

View File

@ -10,29 +10,37 @@ SECOND = 1
MINUTE = 60 * SECOND MINUTE = 60 * SECOND
HOUR = 60 * MINUTE HOUR = 60 * MINUTE
def collect(collection, track): class Copier:
def __init__(self, device, status):
self.device = device
self.status = status
self.scan()
def collect(self, track):
newCollection = [] newCollection = []
for t in collection: for t in self.collection:
if t["length"] == track["length"]: if t["length"] == track["length"]:
# If the length is exactly the same, # If the length is exactly the same,
# assume it's the same track, # assume it's the same track,
# and pick the one with the most stuff. # and pick the one with the most stuff.
if len(track["audio"]) < len(t["audio"]): if len(track["audio"]) < len(t["audio"]):
return collection return
elif len(track["subp"]) < len(t["subp"]): elif len(track["subp"]) < len(t["subp"]):
return collection return
newCollection.append(t) newCollection.append(t)
newCollection.append(track) newCollection.append(track)
return newCollection self.collection = newCollection
def scan(self):
self.status["state"] = "scanning"
def scan(state, device): self.collection = []
p = subprocess.run( p = subprocess.run(
[ [
"lsdvd", "lsdvd",
"-Oy", "-Oy",
"-x", "-x",
device, self.device,
], ],
encoding="utf-8", encoding="utf-8",
capture_output=True, capture_output=True,
@ -42,8 +50,9 @@ def scan(state, device):
if title in ('No', 'unknown'): if title in ('No', 'unknown'):
title = lsdvd["provider_id"] title = lsdvd["provider_id"]
if title == "$PACKAGE_STRING": if title == "$PACKAGE_STRING":
now = time.strftime(r"%Y-%m-%dT%H:%M:%S") title = "DVD"
title = "DVD %s" % (title, now) now = time.strftime("%Y-%m-%dT%H%M%S")
title = "%s %s" % (title, now)
# Go through all the tracks, looking for the largest referenced sector. # Go through all the tracks, looking for the largest referenced sector.
max_sector = 0 max_sector = 0
@ -62,23 +71,25 @@ def scan(state, device):
# * A feature, which has one track much longer than any other # * A feature, which has one track much longer than any other
# * A collection of shows, which has several long tracks, more or less the same lengths # * A collection of shows, which has several long tracks, more or less the same lengths
# * Something else # * Something else
collection = []
for track in tracks: for track in tracks:
if track["length"] / max_length > 0.80: if track["length"] / max_length > 0.80:
collection = collect(collection, track) self.collect(track)
if (max_length < 20 * MINUTE) and (len(collection) < len(track) * 0.6): if (max_length < 20 * MINUTE) and (len(self.collection) < len(track) * 0.6):
collection = tracks self.collection = tracks
state["title"] = title self.status["title"] = title
state["size"] = max_sector * 2048 # DVD sector size = 2048 self.status["size"] = max_sector * 2048 # DVD sector size = 2048
state["tracks"] = [(t["ix"], t["length"]) for t in collection] self.status["tracks"] = [(t["ix"], t["length"]) for t in self.collection]
def copy(self, directory):
self.status["state"] = "copying"
def copy(state, device, directory):
p = subprocess.Popen( p = subprocess.Popen(
[ [
"dvdbackup", "dvdbackup",
"--input=" + device, "--input=" + self.device,
"--name=" + state["title"], "--name=" + self.status["title"],
"--mirror", "--mirror",
"--progress", "--progress",
], ],
@ -99,25 +110,30 @@ def copy(state, device, directory):
if titleSize < lastTitleSize: if titleSize < lastTitleSize:
totalBytes += lastTitleSize totalBytes += lastTitleSize
lastTitleSize = titleSize lastTitleSize = titleSize
yield (totalBytes + titleSize) / state["size"] self.status["complete"] = (totalBytes + titleSize) / self.status["size"]
def encode(state, directory): class Encoder:
title = state["title"] def __init__(self, basedir, status):
logging.info("encoding: %s (%s)" % (title, directory)) self.basedir = basedir
self.status = status
total_length = sum(t[1] for t in state["tracks"]) def encode(self, obj):
title = obj["title"]
logging.info("encoding: %s (%s)" % (title, self.basedir))
total_length = sum(t[1] for t in obj["tracks"])
finished_length = 0 finished_length = 0
for track, length in state["tracks"]: for track, length in obj["tracks"]:
outfn = "%s-%d.mkv" % (title, track) outfn = "%s-%d.mkv" % (title, track)
tmppath = os.path.join(directory, outfn) tmppath = os.path.join(self.basedir, outfn)
outpath = os.path.join(directory, "..", outfn) outpath = os.path.join(self.basedir, "..", outfn)
p = subprocess.Popen( p = subprocess.Popen(
[ [
"nice", "nice",
"HandBrakeCLI", "HandBrakeCLI",
"--json", "--json",
"--input", "%s/%s/VIDEO_TS" % (directory, state["title"]), "--input", "%s/VIDEO_TS" % self.basedir,
"--output", tmppath, "--output", tmppath,
"--title", str(track), "--title", str(track),
"--native-language", "eng", "--native-language", "eng",
@ -143,7 +159,8 @@ def encode(state, directory):
m = progressRe.search(line) m = progressRe.search(line)
if m: if m:
progress = float(m[1]) progress = float(m[1])
yield (finished_length + progress*length) / total_length complete = (finished_length + progress*length) / total_length
self.status["complete"] = complete
finished_length += length finished_length += length
os.rename( os.rename(
@ -153,9 +170,6 @@ def encode(state, directory):
logging.info("Finished track %d; length %d" % (track, length)) logging.info("Finished track %d; length %d" % (track, length))
def clean(state, directory):
os.removedirs(directory)
if __name__ == "__main__": if __name__ == "__main__":
import pprint import pprint
vts = Video(".") vts = Video(".")

View File

@ -1,6 +1,7 @@
#! /usr/bin/python3 #! /usr/bin/python3
import os import os
import threading
import subprocess import subprocess
import glob import glob
import os import os
@ -12,47 +13,40 @@ import re
import logging import logging
import dvd import dvd
import cd import cd
import traceback
import worker
class Encoder(worker.Worker): class Encoder(threading.Thread):
def __init__(self, directory=None): def __init__(self, directory=None, **kwargs):
self.status = {} self.status = {}
self.directory = directory self.directory = directory
return super().__init__(directory) return super().__init__(**kwargs)
def run(self): def run(self):
while True: while True:
wait = True wait = True
self.status = {"type": "encoder", "state": "idle"} self.status = {"type": "encoder", "state": "idle"}
for fn in glob.glob(self.workdir("*", "sucker.json")): for fn in glob.glob(os.path.join(self.directory, "*", "sucker.json")):
directory = os.path.dirname(fn) fdir = os.path.dirname(fn)
state = self.read_state(directory) with open(fn) as f:
try: obj = json.load(f)
self.encode(directory, state) self.encode(fdir, obj)
except Exception as e:
logging.error("Error encoding %s: %s" % (directory, e))
logging.error(traceback.format_exc())
wait = False wait = False
if wait: if wait:
time.sleep(12) time.sleep(12)
def encode(self, directory, state): def encode(self, fdir, obj):
self.status["state"] = "encoding" self.status["state"] = "encoding"
self.status["title"] = state["title"] self.status["title"] = obj["title"]
if obj["type"] == "audio":
if state["video"]: self.encode_audio(fdir, obj)
media = dvd
else: else:
media = cd self.encode_video(fdir, obj)
shutil.rmtree(fdir)
logging.info("Encoding %s (%s)" % (directory, state["title"])) def encode_audio(self, fdir, obj):
for pct in media.encode(state, directory): cd.encode(obj, fdir)
self.status["complete"] = pct
media.clean(state, directory) def encode_video(self, fdir, obj):
self.clear_state(directory) enc = dvd.Encoder(fdir, self.status)
enc.encode(obj)
logging.info("Finished encoding")
# vi: sw=4 ts=4 et ai # vi: sw=4 ts=4 et ai

View File

@ -1,6 +1,7 @@
#! /usr/bin/python3 #! /usr/bin/python3
import os import os
import threading
import subprocess import subprocess
import time import time
import re import re
@ -8,10 +9,8 @@ import fcntl
import traceback import traceback
import json import json
import logging import logging
import slugify
import dvd import dvd
import cd import cd
import worker
CDROM_DRIVE_STATUS = 0x5326 CDROM_DRIVE_STATUS = 0x5326
CDS_NO_INFO = 0 CDS_NO_INFO = 0
@ -29,21 +28,25 @@ CDS_DATA_2 = 102
CDROM_LOCKDOOR = 0x5329 CDROM_LOCKDOOR = 0x5329
CDROM_EJECT = 0x5309 CDROM_EJECT = 0x5309
class Reader(worker.Worker): class Reader(threading.Thread):
def __init__(self, device, directory): def __init__(self, device, directory=None, **kwargs):
super().__init__(directory)
self.device = device self.device = device
self.status["type"] = "reader" self.directory = directory
self.status["device"] = device self.status = {
"type": "reader",
"state": "idle",
"device": self.device,
}
self.complete = 0 self.complete = 0
self.staleness = 0 self.staleness = 0
self.drive = None self.drive = None
logging.info("Starting reader on %s" % self.device) logging.info("Starting reader on %s" % self.device)
return super().__init__(**kwargs)
def reopen(self): def reopen(self):
if (self.staleness > 15) or not self.drive: if (self.staleness > 15) or not self.drive:
if self.drive: if self.drive:
os.close(self.drive) self.drive.close()
self.drive = None self.drive = None
try: try:
self.drive = os.open(self.device, os.O_RDONLY | os.O_NONBLOCK) self.drive = os.open(self.device, os.O_RDONLY | os.O_NONBLOCK)
@ -66,22 +69,24 @@ class Reader(worker.Worker):
rv = fcntl.ioctl(self.drive, CDROM_DISC_STATUS) rv = fcntl.ioctl(self.drive, CDROM_DISC_STATUS)
try: try:
if rv == CDS_AUDIO: if rv == CDS_AUDIO:
self.handle(False) self.handle_audio()
elif rv in [CDS_DATA_1, CDS_DATA_2]: elif rv in [CDS_DATA_1, CDS_DATA_2]:
self.handle(True) self.handle_data()
else: else:
logging.info("Can't handle disc type %d" % rv) logging.info("Can't handle disc type %d" % rv)
except Exception as e: except Exception as e:
logging.error("Error in disc handler: %s" % e) logging.error("Error in disc handler: %s" % e)
logging.error(traceback.format_exc()) logging.error(traceback.format_exc())
self.eject() self.eject()
elif rv in (CDS_TRAY_OPEN, CDS_NO_DISC, CDS_DRIVE_NOT_READ): elif rv in (CDS_TRAY_OPEN, CDS_NO_DISC):
time.sleep(3) time.sleep(3)
else: else:
logging.info("CDROM_DRIVE_STATUS: %d (%s)" % (rv, CDS_STR[rv])) logging.info("CDROM_DRIVE_STATUS: %d (%s)" % (rv, CDS_STR[rv]))
time.sleep(3) time.sleep(3)
def eject(self): def eject(self):
self.status["state"] = "ejecting"
for i in range(20): for i in range(20):
try: try:
fcntl.ioctl(self.drive, CDROM_LOCKDOOR, 0) fcntl.ioctl(self.drive, CDROM_LOCKDOOR, 0)
@ -91,28 +96,32 @@ class Reader(worker.Worker):
logging.error("Ejecting: %v" % e) logging.error("Ejecting: %v" % e)
time.sleep(i * 5) time.sleep(i * 5)
def handle(self, video): # XXX: rename this to something like "write_status"
self.status["video"] = video def finished(self, **kwargs):
self.status["state"] = "finished read"
fn = os.path.join(self.directory, self.status["title"], "sucker.json")
newfn = fn + ".new"
with open(newfn, "w") as fout:
json.dump(obj=self.status, fp=fout)
os.rename(src=newfn, dst=fn)
def handle_audio(self):
self.status["video"] = False
self.status["state"] = "reading" self.status["state"] = "reading"
cd.read(self.device, self.status)
state = {} directory = os.path.join(self.directory, status["title"])
state["video"] = video os.makedirs(directory, exist_ok=True)
if video:
media = dvd
else:
media = cd
media.scan(state, self.device)
self.status["title"] = state["title"]
subdir = slugify.slugify(state["title"])
workdir = self.workdir(subdir)
os.makedirs(workdir, exist_ok=True)
self.status["state"] = "copying" self.status["state"] = "copying"
for pct in media.copy(state, self.device, workdir): cd.copy(self.device, self.status, self.directory)
self.status["complete"] = pct self.finished() # XXX: rename this to something like "write_status"
self.write_state(subdir, state)
def handle_data(self):
self.status["video"] = True
src = dvd.Copier(self.device, self.status)
src.copy(self.directory)
self.finished()
# vi: sw=4 ts=4 et ai # vi: sw=4 ts=4 et ai

View File

@ -7,17 +7,17 @@ import time
import os import os
class Statuser(threading.Thread): class Statuser(threading.Thread):
def __init__(self, workers, directory): def __init__(self, workers, directory=None, **kwargs):
self.workers = workers self.workers = workers
self.directory = directory self.directory = directory
self.status = {} self.status = {}
super().__init__(daemon=True) super().__init__(**kwargs)
def run(self): def run(self):
while True: while True:
self.status["finished"] = { self.status["finished"] = {
"video": glob.glob(os.path.join(self.directory, "*.mkv")), "video": glob.glob(os.path.join(self.directory, "*.mkv")),
"audio": glob.glob(os.path.join(self.directory, "*/*.mp3")), "audio": glob.glob(os.path.join(self.directory, "*/*/*.mp3")),
} }
self.status["workers"] = [w.status for w in self.workers] self.status["workers"] = [w.status for w in self.workers]
time.sleep(12) time.sleep(12)

View File

@ -33,9 +33,13 @@ def main():
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
readers = [reader.Reader(d, args.incoming) for d in args.drive] readers = []
encoders = [encoder.Encoder(args.incoming) for i in range(1)] for d in args.drive:
st = statuser.Statuser(readers + encoders, args.incoming) 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)
[w.start() for w in readers + encoders] [w.start() for w in readers + encoders]
st.start() st.start()

View File

@ -1,32 +0,0 @@
import threading
import os
import json
import logging
class Worker(threading.Thread):
def __init__(self, directory, **kwargs):
self.directory = directory
self.status = {
"state": "idle",
}
kwargs["daemon"] = True
return super().__init__(**kwargs)
def workdir(self, *path):
return os.path.join(self.directory, *path)
def write_state(self, subdir, state):
logging.debug("Writing state: %s" % repr(state))
statefn = self.workdir(subdir, "sucker.json")
newstatefn = statefn + ".new"
with open(newstatefn, "w") as f:
json.dump(state, f)
os.rename(newstatefn, statefn)
def read_state(self, subdir):
with open(self.workdir(subdir, "sucker.json")) as f:
return json.load(f)
def clear_state(self, subdir):
os.unlink(self.workdir(subdir, "sucker.json"))