Skip to content

Commit 3e79032

Browse files
jtl999JoeLametta
authored andcommitted
WIP: Refactor cdrdao toc/table functions into Task and provide progress output (#345)
* Begin work on moving cdrdao to a task * Add code to start cdrdao task * Allow cdrdao output to be asynchronously parsable * Provide progress of cdrdao read toc/table to console * Flake8 fixes, Freso's advices
1 parent 752b485 commit 3e79032

File tree

3 files changed

+163
-90
lines changed

3 files changed

+163
-90
lines changed

whipper/command/cd.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ def do(self):
9494
utils.unmount_device(self.device)
9595

9696
# first, read the normal TOC, which is fast
97-
logger.info("reading TOC...")
9897
self.ittoc = self.program.getFastToc(self.runner, self.device)
9998

10099
# already show us some info based on this

whipper/common/program.py

Lines changed: 18 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@
2727
import os
2828
import time
2929

30-
from whipper.common import accurip, cache, checksum, common, mbngs, path
30+
from whipper.common import accurip, checksum, common, mbngs, path
3131
from whipper.program import cdrdao, cdparanoia
3232
from whipper.image import image
3333
from whipper.extern import freedb
3434
from whipper.extern.task import task
35+
from whipper.result import result
3536

3637
import logging
3738
logger = logging.getLogger(__name__)
@@ -63,7 +64,6 @@ def __init__(self, config, record=False):
6364
@param record: whether to record results of API calls for playback.
6465
"""
6566
self._record = record
66-
self._cache = cache.ResultCache()
6767
self._config = config
6868

6969
d = {}
@@ -95,42 +95,31 @@ def getFastToc(self, runner, device):
9595
if V(version) < V('1.2.3rc2'):
9696
logger.warning('cdrdao older than 1.2.3 has a pre-gap length bug.'
9797
' See http://sourceforge.net/tracker/?func=detail&aid=604751&group_id=2171&atid=102171') # noqa: E501
98-
toc = cdrdao.ReadTOCTask(device).table
98+
99+
t = cdrdao.ReadTOC_Task(device)
100+
runner.run(t)
101+
toc = t.toc.table
102+
99103
assert toc.hasTOC()
100104
return toc
101105

102106
def getTable(self, runner, cddbdiscid, mbdiscid, device, offset,
103-
out_path):
107+
toc_path):
104108
"""
105-
Retrieve the Table either from the cache or the drive.
109+
Retrieve the Table from the drive.
106110
107111
@rtype: L{table.Table}
108112
"""
109-
tcache = cache.TableCache()
110-
ptable = tcache.get(cddbdiscid, mbdiscid)
111113
itable = None
112114
tdict = {}
113115

114-
# Ignore old cache, since we do not know what offset it used.
115-
if isinstance(ptable.object, dict):
116-
tdict = ptable.object
117-
118-
if offset in tdict:
119-
itable = tdict[offset]
120-
121-
if not itable:
122-
logger.debug('getTable: cddbdiscid %s, mbdiscid %s not in cache '
123-
'for offset %s, reading table', cddbdiscid, mbdiscid,
124-
offset)
125-
t = cdrdao.ReadTableTask(device, out_path)
126-
itable = t.table
127-
tdict[offset] = itable
128-
ptable.persist(tdict)
129-
logger.debug('getTable: read table %r', itable)
130-
else:
131-
logger.debug('getTable: cddbdiscid %s, mbdiscid %s in cache '
132-
'for offset %s', cddbdiscid, mbdiscid, offset)
133-
logger.debug('getTable: loaded table %r', itable)
116+
t = cdrdao.ReadTOC_Task(device)
117+
t.description = "Reading table"
118+
t.toc_path = toc_path
119+
runner.run(t)
120+
itable = t.toc.table
121+
tdict[offset] = itable
122+
logger.debug('getTable: read table %r' % itable)
134123

135124
assert itable.hasTOC()
136125

@@ -142,21 +131,15 @@ def getTable(self, runner, cddbdiscid, mbdiscid, device, offset,
142131

143132
def getRipResult(self, cddbdiscid):
144133
"""
145-
Retrieve the persistable RipResult either from our cache (from a
146-
previous, possibly aborted rip), or return a new one.
134+
Return a RipResult object.
147135
148136
@rtype: L{result.RipResult}
149137
"""
150138
assert self.result is None
151-
152-
self._presult = self._cache.getRipResult(cddbdiscid)
153-
self.result = self._presult.object
139+
self.result = result.RipResult()
154140

155141
return self.result
156142

157-
def saveRipResult(self):
158-
self._presult.persist()
159-
160143
def addDisambiguation(self, template_part, metadata):
161144
"Add disambiguation to template path part string."
162145
if metadata.catalogNumber:

whipper/program/cdrdao.py

Lines changed: 145 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,58 +2,163 @@
22
import re
33
import shutil
44
import tempfile
5+
import subprocess
56
from subprocess import Popen, PIPE
67

7-
from whipper.common.common import EjectError, truncate_filename
8+
from whipper.common.common import truncate_filename
89
from whipper.image.toc import TocFile
10+
from whipper.extern.task import task
11+
from whipper.extern import asyncsub
912

1013
import logging
1114
logger = logging.getLogger(__name__)
1215

1316
CDRDAO = 'cdrdao'
1417

18+
_TRACK_RE = re.compile(r"^Analyzing track (?P<track>[0-9]*) \(AUDIO\): start (?P<start>[0-9]*:[0-9]*:[0-9]*), length (?P<length>[0-9]*:[0-9]*:[0-9]*)") # noqa: E501
19+
_CRC_RE = re.compile(
20+
r"Found (?P<channels>[0-9]*) Q sub-channels with CRC errors")
21+
_BEGIN_CDRDAO_RE = re.compile(r"-" * 60)
22+
_LAST_TRACK_RE = re.compile(r"^(?P<track>[0-9]*)")
23+
_LEADOUT_RE = re.compile(
24+
r"^Leadout AUDIO\s*[0-9]\s*[0-9]*:[0-9]*:[0-9]*\([0-9]*\)")
1525

16-
def read_toc(device, fast_toc=False, toc_path=None):
26+
27+
class ProgressParser:
28+
tracks = 0
29+
currentTrack = 0
30+
oldline = '' # for leadout/final track number detection
31+
32+
def parse(self, line):
33+
cdrdao_m = _BEGIN_CDRDAO_RE.match(line)
34+
35+
if cdrdao_m:
36+
logger.debug("RE: Begin cdrdao toc-read")
37+
38+
leadout_m = _LEADOUT_RE.match(line)
39+
40+
if leadout_m:
41+
logger.debug("RE: Reached leadout")
42+
last_track_m = _LAST_TRACK_RE.match(self.oldline)
43+
if last_track_m:
44+
self.tracks = last_track_m.group('track')
45+
46+
track_s = _TRACK_RE.search(line)
47+
if track_s:
48+
logger.debug("RE: Began reading track: %d",
49+
int(track_s.group('track')))
50+
self.currentTrack = int(track_s.group('track'))
51+
52+
crc_s = _CRC_RE.search(line)
53+
if crc_s:
54+
print("Track %d finished, "
55+
"found %d Q sub-channels with CRC errors" %
56+
(self.currentTrack, int(crc_s.group('channels'))))
57+
58+
self.oldline = line
59+
60+
61+
class ReadTOCTask(task.Task):
1762
"""
18-
Return cdrdao-generated table of contents for 'device'.
63+
Task that reads the TOC of the disc using cdrdao
1964
"""
20-
# cdrdao MUST be passed a non-existing filename as its last argument
21-
# to write the TOC to; it does not support writing to stdout or
22-
# overwriting an existing file, nor does linux seem to support
23-
# locking a non-existant file. Thus, this race-condition introducing
24-
# hack is carried from morituri to whipper and will be removed when
25-
# cdrdao is fixed.
26-
fd, tocfile = tempfile.mkstemp(suffix=u'.cdrdao.read-toc.whipper')
27-
os.close(fd)
28-
os.unlink(tocfile)
29-
30-
cmd = [CDRDAO, 'read-toc'] + (['--fast-toc'] if fast_toc else []) + [
31-
'--device', device, tocfile]
32-
# PIPE is the closest to >/dev/null we can get
33-
logger.debug("executing %r", cmd)
34-
p = Popen(cmd, stdout=PIPE, stderr=PIPE)
35-
_, stderr = p.communicate()
36-
if p.returncode != 0:
37-
msg = 'cdrdao read-toc failed: return code is non-zero: ' + \
38-
str(p.returncode)
39-
logger.critical(msg)
40-
# Gracefully handle missing disc
41-
if "ERROR: Unit not ready, giving up." in stderr:
42-
raise EjectError(device, "no disc detected")
43-
raise IOError(msg)
44-
45-
toc = TocFile(tocfile)
46-
toc.parse()
47-
if toc_path is not None:
48-
t_comp = os.path.abspath(toc_path).split(os.sep)
49-
t_dirn = os.sep.join(t_comp[:-1])
50-
# If the output path doesn't exist, make it recursively
51-
if not os.path.isdir(t_dirn):
52-
os.makedirs(t_dirn)
53-
t_dst = truncate_filename(os.path.join(t_dirn, t_comp[-1] + '.toc'))
54-
shutil.copy(tocfile, os.path.join(t_dirn, t_dst))
55-
os.unlink(tocfile)
56-
return toc
65+
description = "Reading TOC"
66+
toc = None
67+
68+
def __init__(self, device, fast_toc=False, toc_path=None):
69+
"""
70+
Read the TOC for 'device'.
71+
72+
@param device: block device to read TOC from
73+
@type device: str
74+
@param fast_toc: If to use fast-toc cdrdao mode
75+
@type fast_toc: bool
76+
@param toc_path: Where to save TOC if wanted.
77+
@type toc_path: str
78+
"""
79+
80+
self.device = device
81+
self.fast_toc = fast_toc
82+
self.toc_path = toc_path
83+
self._buffer = "" # accumulate characters
84+
self._parser = ProgressParser()
85+
86+
self.fd, self.tocfile = tempfile.mkstemp(
87+
suffix=u'.cdrdao.read-toc.whipper.task')
88+
89+
def start(self, runner):
90+
task.Task.start(self, runner)
91+
os.close(self.fd)
92+
os.unlink(self.tocfile)
93+
94+
cmd = ([CDRDAO, 'read-toc']
95+
+ (['--fast-toc'] if self.fast_toc else [])
96+
+ ['--device', self.device, self.tocfile])
97+
98+
self._popen = asyncsub.Popen(cmd,
99+
bufsize=1024,
100+
stdin=subprocess.PIPE,
101+
stdout=subprocess.PIPE,
102+
stderr=subprocess.PIPE,
103+
close_fds=True)
104+
105+
self.schedule(0.01, self._read, runner)
106+
107+
def _read(self, runner):
108+
ret = self._popen.recv_err()
109+
if not ret:
110+
if self._popen.poll() is not None:
111+
self._done()
112+
return
113+
self.schedule(0.01, self._read, runner)
114+
return
115+
self._buffer += ret
116+
117+
# parse buffer into lines if possible, and parse them
118+
if "\n" in self._buffer:
119+
lines = self._buffer.split('\n')
120+
if lines[-1] != "\n":
121+
# last line didn't end yet
122+
self._buffer = lines[-1]
123+
del lines[-1]
124+
else:
125+
self._buffer = ""
126+
for line in lines:
127+
self._parser.parse(line)
128+
if (self._parser.currentTrack is not 0 and
129+
self._parser.tracks is not 0):
130+
progress = (float('%d' % self._parser.currentTrack) /
131+
float(self._parser.tracks))
132+
if progress < 1.0:
133+
self.setProgress(progress)
134+
135+
# 0 does not give us output before we complete, 1.0 gives us output
136+
# too late
137+
self.schedule(0.01, self._read, runner)
138+
139+
def _poll(self, runner):
140+
if self._popen.poll() is None:
141+
self.schedule(1.0, self._poll, runner)
142+
return
143+
144+
self._done()
145+
146+
def _done(self):
147+
self.setProgress(1.0)
148+
self.toc = TocFile(self.tocfile)
149+
self.toc.parse()
150+
if self.toc_path is not None:
151+
t_comp = os.path.abspath(self.toc_path).split(os.sep)
152+
t_dirn = os.sep.join(t_comp[:-1])
153+
# If the output path doesn't exist, make it recursively
154+
if not os.path.isdir(t_dirn):
155+
os.makedirs(t_dirn)
156+
t_dst = truncate_filename(
157+
os.path.join(t_dirn, t_comp[-1] + '.toc'))
158+
shutil.copy(self.tocfile, os.path.join(t_dirn, t_dst))
159+
os.unlink(self.tocfile)
160+
self.stop()
161+
return
57162

58163

59164
def DetectCdr(device):
@@ -88,20 +193,6 @@ def version():
88193
return m.group('version')
89194

90195

91-
def ReadTOCTask(device):
92-
"""
93-
stopgap morituri-insanity compatibility layer
94-
"""
95-
return read_toc(device, fast_toc=True)
96-
97-
98-
def ReadTableTask(device, toc_path=None):
99-
"""
100-
stopgap morituri-insanity compatibility layer
101-
"""
102-
return read_toc(device, toc_path=toc_path)
103-
104-
105196
def getCDRDAOVersion():
106197
"""
107198
stopgap morituri-insanity compatibility layer

0 commit comments

Comments
 (0)