Refactor. CD maybe working?
This commit is contained in:
parent
2860a1405c
commit
91a332ca53
|
@ -10,6 +10,7 @@
|
||||||
"gnudb",
|
"gnudb",
|
||||||
"newfn",
|
"newfn",
|
||||||
"RDONLY",
|
"RDONLY",
|
||||||
|
"cdparanoia",
|
||||||
"TTITLE"
|
"TTITLE"
|
||||||
]
|
]
|
||||||
}
|
}
|
13
Dockerfile
13
Dockerfile
|
@ -7,24 +7,17 @@ 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 \
|
||||||
cowsay \
|
python3-slugify \
|
||||||
&& 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/
|
||||||
|
|
|
@ -14,6 +14,13 @@ 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.~~ A rewrite broke this; I plan to fix it soon.
|
* ~~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.
|
||||||
* Rip video DVDs, transcode them to mkv
|
* Rip video DVDs, transcode them to mkv
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
* HandBrakeCLI
|
||||||
|
* cdparanoia
|
||||||
|
* cd-discid
|
||||||
|
*
|
||||||
|
|
||||||
## How To Run This
|
## How To Run This
|
||||||
|
|
||||||
You need a place to store your stuff.
|
You need a place to store your stuff.
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
# 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.
|
86
src/cd.py
86
src/cd.py
|
@ -13,7 +13,7 @@ SECOND = 1
|
||||||
MINUTE = 60 * SECOND
|
MINUTE = 60 * SECOND
|
||||||
HOUR = 60 * MINUTE
|
HOUR = 60 * MINUTE
|
||||||
|
|
||||||
def read(device, status):
|
def scan(state, device):
|
||||||
# Get disc ID
|
# Get disc ID
|
||||||
p = subprocess.run(
|
p = subprocess.run(
|
||||||
[
|
[
|
||||||
|
@ -24,7 +24,7 @@ def read(device, status):
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
)
|
)
|
||||||
discid = p.stdout.strip()
|
discid = p.stdout.strip()
|
||||||
status["discid"] = discid
|
state["discid"] = discid
|
||||||
|
|
||||||
# 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
|
||||||
|
@ -42,23 +42,24 @@ def read(device, status):
|
||||||
# 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"):
|
||||||
status[k] = disc[k]
|
state[k] = disc[k]
|
||||||
else:
|
else:
|
||||||
now = time.strftime("%Y-%m-%dT%H%M%S")
|
now = time.strftime("%Y-%m-%dT%H%M%S")
|
||||||
num_tracks = int(discid.split()[1])
|
num_tracks = int(discid.split()[1])
|
||||||
status["title"] = "Unknown CD - %s" % now
|
state["title"] = "Unknown CD - %s" % now
|
||||||
status["tracks"] = ["Track %02d" % i for i in range(num_tracks)]
|
state["tracks"] = ["Track %02d" % i for i in range(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(status["discid"].split()[-1]) * SECOND # disc duration in seconds
|
duration = int(state["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
|
||||||
status["total_samples"] = total_samples
|
state["total_samples"] = total_samples
|
||||||
|
|
||||||
track_num = 1
|
track_num = 1
|
||||||
for track_name in status["tracks"]:
|
for track_name in state["tracks"]:
|
||||||
logging.debug("Ripping track %d of %d", track_num, len(status["tracks"]))
|
logging.debug("Ripping track %d of %d", track_num, len(state["tracks"]))
|
||||||
p = subprocess.Popen(
|
p = subprocess.Popen(
|
||||||
[
|
[
|
||||||
"cdparanoia",
|
"cdparanoia",
|
||||||
|
@ -76,16 +77,17 @@ def rip(device, status, 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])
|
||||||
status["complete"] = samples / total_samples
|
yield samples / total_samples
|
||||||
|
|
||||||
track_num += 1
|
track_num += 1
|
||||||
|
|
||||||
def encode(status, directory):
|
|
||||||
|
def encode(state, directory):
|
||||||
track_num = 1
|
track_num = 1
|
||||||
durations = [int(d) for d in status["discid"].split()[2:-1]]
|
durations = [int(d) for d in state["discid"].split()[2:-1]]
|
||||||
total_duration = sum(durations)
|
total_duration = sum(durations)
|
||||||
encoded_duration = 0
|
encoded_duration = 0
|
||||||
for track_name in status["tracks"]:
|
for track_name in state["tracks"]:
|
||||||
logging.debug("Encoding track %d (%s)" % (track_num, track_name))
|
logging.debug("Encoding track %d (%s)" % (track_num, track_name))
|
||||||
duration = durations[track_num-1]
|
duration = durations[track_num-1]
|
||||||
argv = [
|
argv = [
|
||||||
|
@ -94,15 +96,15 @@ def encode(status, directory):
|
||||||
"--nohist",
|
"--nohist",
|
||||||
"--disptime", "1",
|
"--disptime", "1",
|
||||||
"--preset", "standard",
|
"--preset", "standard",
|
||||||
"--tl", status["title"],
|
"--tl", state["title"],
|
||||||
"--tn", "%d/%d" % (track_num, len(status["tracks"])),
|
"--tn", "%d/%d" % (track_num, len(state["tracks"])),
|
||||||
]
|
]
|
||||||
if status.get("artist"):
|
if state.get("artist"):
|
||||||
argv.extend(["--ta", status["artist"]])
|
argv.extend(["--ta", state["artist"]])
|
||||||
if status.get("genre"):
|
if state.get("genre"):
|
||||||
argv.extend(["--tg", status["genre"]])
|
argv.extend(["--tg", state["genre"]])
|
||||||
if status.get("year"):
|
if state.get("year"):
|
||||||
argv.extend(["--ty", status["year"]])
|
argv.extend(["--ty", state["year"]])
|
||||||
if track_name:
|
if track_name:
|
||||||
argv.extend(["--tt", track_name])
|
argv.extend(["--tt", track_name])
|
||||||
outfn = "%02d - %s.mp3" % (track_num, track_name)
|
outfn = "%02d - %s.mp3" % (track_num, track_name)
|
||||||
|
@ -122,34 +124,38 @@ def encode(status, directory):
|
||||||
p = line.split("(")[1]
|
p = line.split("(")[1]
|
||||||
p = p.split("%")[0]
|
p = p.split("%")[0]
|
||||||
pct = int(p) / 100
|
pct = int(p) / 100
|
||||||
status["complete"] = (encoded_duration + (duration * pct)) / total_duration
|
yield (encoded_duration + (duration * pct)) / total_duration
|
||||||
print(status["complete"])
|
|
||||||
|
|
||||||
encoded_duration += duration
|
encoded_duration += duration
|
||||||
track_num += 1
|
track_num += 1
|
||||||
|
|
||||||
|
|
||||||
|
def clean(state, directory):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import pprint
|
import pprint
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
|
|
||||||
if len(sys.argv) > 1:
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
directory = sys.argv[1]
|
|
||||||
fn = os.path.join(directory, "status.json")
|
|
||||||
f = open(fn)
|
|
||||||
status = json.load(f)
|
|
||||||
else:
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
status = {}
|
|
||||||
read("/dev/sr0", status)
|
|
||||||
pprint.pprint(status)
|
|
||||||
|
|
||||||
directory = os.path.join(".", status["title"])
|
state = {}
|
||||||
os.makedirs(directory, exist_ok=True)
|
scan(state, "/dev/sr0")
|
||||||
rip("/dev/sr0", status, directory)
|
pprint.pprint(state)
|
||||||
pprint.pprint(status)
|
|
||||||
|
|
||||||
encode(status, directory)
|
directory = os.path.join(".", state["title"])
|
||||||
pprint.pprint(status)
|
os.makedirs(directory, exist_ok=True)
|
||||||
|
with open(os.path.join(directory, "state.json"), "w") as f:
|
||||||
|
json.dump(f, state)
|
||||||
|
|
||||||
|
for pct in copy(state, "/dev/sr0", directory):
|
||||||
|
sys.stdout.write("Copying: %3d%%\r" % (pct*100))
|
||||||
|
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
|
||||||
|
|
277
src/dvd.py
277
src/dvd.py
|
@ -10,164 +10,153 @@ SECOND = 1
|
||||||
MINUTE = 60 * SECOND
|
MINUTE = 60 * SECOND
|
||||||
HOUR = 60 * MINUTE
|
HOUR = 60 * MINUTE
|
||||||
|
|
||||||
class Copier:
|
def collect(collection, track):
|
||||||
def __init__(self, device, status):
|
newCollection = []
|
||||||
self.device = device
|
for t in collection:
|
||||||
self.status = status
|
if t["length"] == track["length"]:
|
||||||
self.scan()
|
# If the length is exactly the same,
|
||||||
|
# assume it's the same track,
|
||||||
def collect(self, track):
|
# and pick the one with the most stuff.
|
||||||
newCollection = []
|
if len(track["audio"]) < len(t["audio"]):
|
||||||
for t in self.collection:
|
return collection
|
||||||
if t["length"] == track["length"]:
|
elif len(track["subp"]) < len(t["subp"]):
|
||||||
# If the length is exactly the same,
|
return collection
|
||||||
# assume it's the same track,
|
newCollection.append(t)
|
||||||
# and pick the one with the most stuff.
|
newCollection.append(track)
|
||||||
if len(track["audio"]) < len(t["audio"]):
|
return newCollection
|
||||||
return
|
|
||||||
elif len(track["subp"]) < len(t["subp"]):
|
|
||||||
return
|
|
||||||
newCollection.append(t)
|
|
||||||
newCollection.append(track)
|
|
||||||
self.collection = newCollection
|
|
||||||
|
|
||||||
def scan(self):
|
|
||||||
self.status["state"] = "scanning"
|
|
||||||
|
|
||||||
self.collection = []
|
|
||||||
p = subprocess.run(
|
|
||||||
[
|
|
||||||
"lsdvd",
|
|
||||||
"-Oy",
|
|
||||||
"-x",
|
|
||||||
self.device,
|
|
||||||
],
|
|
||||||
encoding="utf-8",
|
|
||||||
capture_output=True,
|
|
||||||
)
|
|
||||||
lsdvd = eval(p.stdout[8:]) # s/^lsdvd = //
|
|
||||||
title = lsdvd["title"]
|
|
||||||
if title in ('No', 'unknown'):
|
|
||||||
title = lsdvd["provider_id"]
|
|
||||||
if title == "$PACKAGE_STRING":
|
|
||||||
title = "DVD"
|
|
||||||
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.
|
|
||||||
max_sector = 0
|
|
||||||
max_length = 0
|
|
||||||
tracks = lsdvd["track"]
|
|
||||||
for track in tracks:
|
|
||||||
max_length = max(track["length"], max_length)
|
|
||||||
for cell in track["cell"]:
|
|
||||||
max_sector = max(cell["last_sector"], max_sector)
|
|
||||||
if max_sector == 0:
|
|
||||||
logging.info("Media size = 0; aborting")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Make a guess about what's on this DVD.
|
|
||||||
# We will categories into three types:
|
|
||||||
# * 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
|
|
||||||
# * Something else
|
|
||||||
for track in tracks:
|
|
||||||
if track["length"] / max_length > 0.80:
|
|
||||||
self.collect(track)
|
|
||||||
if (max_length < 20 * MINUTE) and (len(self.collection) < len(track) * 0.6):
|
|
||||||
self.collection = tracks
|
|
||||||
|
|
||||||
self.status["title"] = title
|
|
||||||
self.status["size"] = max_sector * 2048 # DVD sector size = 2048
|
|
||||||
self.status["tracks"] = [(t["ix"], t["length"]) for t in self.collection]
|
|
||||||
|
|
||||||
|
|
||||||
def copy(self, directory):
|
def scan(state, device):
|
||||||
self.status["state"] = "copying"
|
p = subprocess.run(
|
||||||
|
[
|
||||||
|
"lsdvd",
|
||||||
|
"-Oy",
|
||||||
|
"-x",
|
||||||
|
device,
|
||||||
|
],
|
||||||
|
encoding="utf-8",
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
lsdvd = eval(p.stdout[8:]) # s/^lsdvd = //
|
||||||
|
title = lsdvd["title"]
|
||||||
|
if title in ('No', 'unknown'):
|
||||||
|
title = lsdvd["provider_id"]
|
||||||
|
if title == "$PACKAGE_STRING":
|
||||||
|
title = "DVD"
|
||||||
|
now = time.strftime(r"%Y-%m-%dT%H%M%S")
|
||||||
|
title = "%s %s" % (title, now)
|
||||||
|
|
||||||
|
# Go through all the tracks, looking for the largest referenced sector.
|
||||||
|
max_sector = 0
|
||||||
|
max_length = 0
|
||||||
|
tracks = lsdvd["track"]
|
||||||
|
for track in tracks:
|
||||||
|
max_length = max(track["length"], max_length)
|
||||||
|
for cell in track["cell"]:
|
||||||
|
max_sector = max(cell["last_sector"], max_sector)
|
||||||
|
if max_sector == 0:
|
||||||
|
logging.info("Media size = 0; aborting")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Make a guess about what's on this DVD.
|
||||||
|
# We will categories into three types:
|
||||||
|
# * 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
|
||||||
|
# * Something else
|
||||||
|
collection = []
|
||||||
|
for track in tracks:
|
||||||
|
if track["length"] / max_length > 0.80:
|
||||||
|
collection = collect(track)
|
||||||
|
if (max_length < 20 * MINUTE) and (len(collection) < len(track) * 0.6):
|
||||||
|
collection = tracks
|
||||||
|
|
||||||
|
state["title"] = title
|
||||||
|
state["size"] = max_sector * 2048 # DVD sector size = 2048
|
||||||
|
state["tracks"] = [(t["ix"], t["length"]) for t in collection]
|
||||||
|
|
||||||
|
def copy(state, device, directory):
|
||||||
|
p = subprocess.Popen(
|
||||||
|
[
|
||||||
|
"dvdbackup",
|
||||||
|
"--input=" + device,
|
||||||
|
"--name=" + state["title"],
|
||||||
|
"--mirror",
|
||||||
|
"--progress",
|
||||||
|
],
|
||||||
|
encoding="utf-8",
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
cwd=directory,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
yield (totalBytes + titleSize) / state["size"]
|
||||||
|
|
||||||
|
|
||||||
|
def encode(state, directory):
|
||||||
|
title = state["title"]
|
||||||
|
logging.info("encoding: %s (%s)" % (title, directory))
|
||||||
|
|
||||||
|
total_length = sum(t[1] for t in state["tracks"])
|
||||||
|
finished_length = 0
|
||||||
|
for track, length in state["tracks"]:
|
||||||
|
outfn = "%s-%d.mkv" % (title, track)
|
||||||
|
tmppath = os.path.join(directory, outfn)
|
||||||
|
outpath = os.path.join(directory, "..", outfn)
|
||||||
p = subprocess.Popen(
|
p = subprocess.Popen(
|
||||||
[
|
[
|
||||||
"dvdbackup",
|
"nice",
|
||||||
"--input=" + self.device,
|
"HandBrakeCLI",
|
||||||
"--name=" + self.status["title"],
|
"--json",
|
||||||
"--mirror",
|
"--input", "%s/VIDEO_TS" % directory,
|
||||||
"--progress",
|
"--output", tmppath,
|
||||||
|
"--title", str(track),
|
||||||
|
"--native-language", "eng",
|
||||||
|
"--markers",
|
||||||
|
"--loose-anamorphic",
|
||||||
|
"--all-subtitles",
|
||||||
|
"--all-audio",
|
||||||
|
"--aencoder", "copy",
|
||||||
|
"--audio-copy-mask", "aac,ac3,mp3",
|
||||||
|
"--audio-fallback", "aac",
|
||||||
],
|
],
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.STDOUT,
|
stderr=None,
|
||||||
cwd=directory,
|
|
||||||
)
|
)
|
||||||
totalBytes = titleSize = lastTitleSize = 0
|
|
||||||
progressRe = re.compile(r"^Copying.*([0-9.]+)/[0-9.]+ (MiB|KiB)")
|
# 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:
|
for line in p.stdout:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
m = progressRe.search(line)
|
m = progressRe.search(line)
|
||||||
if m and m[2] == "MiB":
|
if m:
|
||||||
titleSize = float(m[1]) * 1024 * 1024
|
progress = float(m[1])
|
||||||
elif m and m[2] == "KiB":
|
complete = (finished_length + progress*length) / total_length
|
||||||
titleSize = float(m[1]) * 1024
|
state["complete"] = complete
|
||||||
if titleSize < lastTitleSize:
|
|
||||||
totalBytes += lastTitleSize
|
finished_length += length
|
||||||
lastTitleSize = titleSize
|
os.rename(
|
||||||
self.status["complete"] = (totalBytes + titleSize) / self.status["size"]
|
src=tmppath,
|
||||||
|
dst=outpath,
|
||||||
|
)
|
||||||
|
logging.info("Finished track %d; length %d" % (track, length))
|
||||||
|
|
||||||
|
|
||||||
class Encoder:
|
def clean(state, directory):
|
||||||
def __init__(self, basedir, status):
|
pass
|
||||||
self.basedir = basedir
|
|
||||||
self.status = status
|
|
||||||
|
|
||||||
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
|
|
||||||
for track, length in obj["tracks"]:
|
|
||||||
outfn = "%s-%d.mkv" % (title, track)
|
|
||||||
tmppath = os.path.join(self.basedir, outfn)
|
|
||||||
outpath = os.path.join(self.basedir, "..", outfn)
|
|
||||||
p = subprocess.Popen(
|
|
||||||
[
|
|
||||||
"nice",
|
|
||||||
"HandBrakeCLI",
|
|
||||||
"--json",
|
|
||||||
"--input", "%s/VIDEO_TS" % self.basedir,
|
|
||||||
"--output", tmppath,
|
|
||||||
"--title", str(track),
|
|
||||||
"--native-language", "eng",
|
|
||||||
"--markers",
|
|
||||||
"--loose-anamorphic",
|
|
||||||
"--all-subtitles",
|
|
||||||
"--all-audio",
|
|
||||||
"--aencoder", "copy",
|
|
||||||
"--audio-copy-mask", "aac,ac3,mp3",
|
|
||||||
"--audio-fallback", "aac",
|
|
||||||
],
|
|
||||||
encoding="utf-8",
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 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])
|
|
||||||
complete = (finished_length + progress*length) / total_length
|
|
||||||
self.status["complete"] = complete
|
|
||||||
|
|
||||||
finished_length += length
|
|
||||||
os.rename(
|
|
||||||
src=tmppath,
|
|
||||||
dst=outpath,
|
|
||||||
)
|
|
||||||
logging.info("Finished track %d; length %d" % (track, length))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#! /usr/bin/python3
|
#! /usr/bin/python3
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import threading
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
|
@ -13,40 +12,41 @@ import re
|
||||||
import logging
|
import logging
|
||||||
import dvd
|
import dvd
|
||||||
import cd
|
import cd
|
||||||
|
import worker
|
||||||
|
|
||||||
class Encoder(threading.Thread):
|
class Encoder(worker.Worker):
|
||||||
def __init__(self, directory=None, **kwargs):
|
def __init__(self, directory=None):
|
||||||
self.status = {}
|
self.status = {}
|
||||||
self.directory = directory
|
self.directory = directory
|
||||||
return super().__init__(**kwargs)
|
return super().__init__(directory)
|
||||||
|
|
||||||
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(os.path.join(self.directory, "*", "sucker.json")):
|
for fn in glob.glob(self.workdir("*", "state.json")):
|
||||||
fdir = os.path.dirname(fn)
|
self.encode(os.path.dirname(fn), obj)
|
||||||
with open(fn) as f:
|
|
||||||
obj = json.load(f)
|
|
||||||
self.encode(fdir, obj)
|
|
||||||
wait = False
|
wait = False
|
||||||
if wait:
|
if wait:
|
||||||
time.sleep(12)
|
time.sleep(12)
|
||||||
|
|
||||||
def encode(self, fdir, obj):
|
def encode(self, directory, obj):
|
||||||
self.status["state"] = "encoding"
|
self.status["state"] = "encoding"
|
||||||
self.status["title"] = obj["title"]
|
|
||||||
if obj["type"] == "audio":
|
state = self.read_state(directory)
|
||||||
self.encode_audio(fdir, obj)
|
self.status["title"] = state["title"]
|
||||||
|
|
||||||
|
if state["video"]:
|
||||||
|
media = dvd
|
||||||
else:
|
else:
|
||||||
self.encode_video(fdir, obj)
|
media = cd
|
||||||
shutil.rmtree(fdir)
|
|
||||||
|
|
||||||
def encode_audio(self, fdir, obj):
|
logging.info("Encoding %s (%s)" % (directory, state["title"]))
|
||||||
cd.encode(obj, fdir)
|
for pct in media.encode(state, directory):
|
||||||
|
self.status["complete"] = pct
|
||||||
|
|
||||||
def encode_video(self, fdir, obj):
|
media.clean(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
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
class MediaHandler:
|
||||||
|
def __init__(self, basedir, state):
|
||||||
|
self.basedir = basedir
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
def
|
|
@ -1,7 +1,6 @@
|
||||||
#! /usr/bin/python3
|
#! /usr/bin/python3
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import threading
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
|
@ -9,8 +8,10 @@ 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
|
||||||
|
@ -28,25 +29,21 @@ CDS_DATA_2 = 102
|
||||||
CDROM_LOCKDOOR = 0x5329
|
CDROM_LOCKDOOR = 0x5329
|
||||||
CDROM_EJECT = 0x5309
|
CDROM_EJECT = 0x5309
|
||||||
|
|
||||||
class Reader(threading.Thread):
|
class Reader(worker.Worker):
|
||||||
def __init__(self, device, directory=None, **kwargs):
|
def __init__(self, device, directory):
|
||||||
|
super().__init__(device)
|
||||||
self.device = device
|
self.device = device
|
||||||
self.directory = directory
|
self.status["type"] = "reader"
|
||||||
self.status = {
|
self.status["device"] = device
|
||||||
"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:
|
||||||
self.drive.close()
|
os.close(self.drive)
|
||||||
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)
|
||||||
|
@ -69,9 +66,9 @@ class Reader(threading.Thread):
|
||||||
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_audio()
|
self.handle(false)
|
||||||
elif rv in [CDS_DATA_1, CDS_DATA_2]:
|
elif rv in [CDS_DATA_1, CDS_DATA_2]:
|
||||||
self.handle_data()
|
self.handle(true)
|
||||||
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:
|
||||||
|
@ -96,32 +93,26 @@ class Reader(threading.Thread):
|
||||||
logging.error("Ejecting: %v" % e)
|
logging.error("Ejecting: %v" % e)
|
||||||
time.sleep(i * 5)
|
time.sleep(i * 5)
|
||||||
|
|
||||||
# XXX: rename this to something like "write_status"
|
def handle(self, video):
|
||||||
def finished(self, **kwargs):
|
self.status["video"] = video
|
||||||
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)
|
|
||||||
|
|
||||||
directory = os.path.join(self.directory, status["title"])
|
state = {}
|
||||||
os.makedirs(directory, exist_ok=True)
|
state["video"] = video
|
||||||
|
if video:
|
||||||
|
media = cd
|
||||||
|
else:
|
||||||
|
media = dvd
|
||||||
|
|
||||||
|
media.scan(state, self.device)
|
||||||
|
self.status["title"] = state["title"]
|
||||||
|
subdir = slugify.slugify(state["title"])
|
||||||
|
|
||||||
self.status["state"] = "copying"
|
self.status["state"] = "copying"
|
||||||
cd.copy(self.device, self.status, self.directory)
|
for pct in media.copy(device, self.workdir(subdir)):
|
||||||
self.finished() # XXX: rename this to something like "write_status"
|
self.status["complete"] = pct
|
||||||
|
|
||||||
|
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
|
||||||
|
|
25
src/state.py
25
src/state.py
|
@ -1,25 +0,0 @@
|
||||||
#! /usr/bin/python3
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
class State(dict):
|
|
||||||
def __init__(self, path):
|
|
||||||
super().__init__()
|
|
||||||
self.path = path
|
|
||||||
self.read()
|
|
||||||
|
|
||||||
def read(self):
|
|
||||||
try:
|
|
||||||
f = open(self.path)
|
|
||||||
except FileNotFoundError:
|
|
||||||
return
|
|
||||||
obj = json.load(f)
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
for k in obj:
|
|
||||||
self[k] = obj[k]
|
|
||||||
|
|
||||||
def write(self):
|
|
||||||
f = open(self.path, "w")
|
|
||||||
json.dump(self, f)
|
|
||||||
f.close()
|
|
|
@ -7,17 +7,17 @@ import time
|
||||||
import os
|
import os
|
||||||
|
|
||||||
class Statuser(threading.Thread):
|
class Statuser(threading.Thread):
|
||||||
def __init__(self, workers, directory=None, **kwargs):
|
def __init__(self, workers, directory):
|
||||||
self.workers = workers
|
self.workers = workers
|
||||||
self.directory = directory
|
self.directory = directory
|
||||||
self.status = {}
|
self.status = {}
|
||||||
super().__init__(**kwargs)
|
super().__init__(daemon=True)
|
||||||
|
|
||||||
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)
|
||||||
|
|
|
@ -33,13 +33,9 @@ def main():
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
readers = []
|
readers = [reader.Reader(d, args.incoming) for d in args.drive]
|
||||||
for d in args.drive:
|
encoders = [encoder.Encoder(args.incoming) for i in range(1)]
|
||||||
readers.append(reader.Reader(d, directory=args.incoming, daemon=True))
|
st = statuser.Statuser(readers + encoders, args.incoming)
|
||||||
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()
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import threading
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
class Worker(threading.Thread):
|
||||||
|
def __init__(self, directory, **kwargs):
|
||||||
|
self.directory = directory
|
||||||
|
self.status = {
|
||||||
|
"state": "idle",
|
||||||
|
"directory": directory,
|
||||||
|
}
|
||||||
|
|
||||||
|
kwargs["daemon"] = True
|
||||||
|
return super().__init__(**kwargs)
|
||||||
|
|
||||||
|
def workdir(self, *path):
|
||||||
|
return os.path.join(self.directory, *path)
|
||||||
|
|
||||||
|
def write_state(self, subdir, state):
|
||||||
|
with open(self.workdir(subdir, "state.json"), "w") as f:
|
||||||
|
json.dump(f, state)
|
||||||
|
|
||||||
|
def read_state(self, subdir):
|
||||||
|
with open(self.workdir(subdir, "state.json")) as f:
|
||||||
|
return json.load(f)
|
Loading…
Reference in New Issue