replace cdrdao.py with much simpler version (#52)

* replace cdrdao.py with much simpler version
* more pythonic syntax for cdrdao.read_toc(fast_toc=) switching
* fix silly typo
This commit is contained in:
Samantha Baldwin
2016-10-22 04:32:18 -04:00
committed by JoeLametta
parent 8721ba1caf
commit d7f8557426
5 changed files with 60 additions and 566 deletions

View File

@@ -126,26 +126,14 @@ class Program(log.Loggable):
ptoc = cache.Persister(toc_pickle or None)
if not ptoc.object:
tries = 0
while True:
tries += 1
t = cdrdao.ReadTOCTask(device=device)
try:
function(runner, t)
break
except:
if tries > 3:
raise
self.debug('failed to read TOC after %d tries, retrying' % tries)
version = t.tasks[1].parser.version
from pkg_resources import parse_version as V
# we've built a cdrdao 1.2.3rc2 modified package with the patch
if V(version) < V('1.2.3rc2p1'):
version = cdrdao.getCDRDAOVersion()
if V(version) < V('1.2.3rc2'):
self.stdout.write('Warning: cdrdao older than 1.2.3 has a '
'pre-gap length bug.\n'
'See http://sourceforge.net/tracker/?func=detail'
'&aid=604751&group_id=2171&atid=102171\n')
t = cdrdao.ReadTOCTask(device)
ptoc.persist(t.table)
toc = ptoc.object
assert toc.hasTOC()
@@ -173,8 +161,7 @@ class Program(log.Loggable):
self.debug('getTable: cddbdiscid %s, mbdiscid %s not in cache for offset %s, '
'reading table' % (
cddbdiscid, mbdiscid, offset))
t = cdrdao.ReadTableTask(device=device)
runner.run(t)
t = cdrdao.ReadTableTask(device)
itable = t.table
tdict[offset] = itable
ptable.persist(tdict)

View File

@@ -1,522 +1,73 @@
# -*- Mode: Python; test-case-name:morituri.test.test_program_cdrdao -*-
# vi:si:et:sw=4:sts=4:ts=4
# Morituri - for those about to RIP
# Copyright (C) 2009 Thomas Vander Stichele
# This file is part of morituri.
#
# morituri is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# morituri is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with morituri. If not, see <http://www.gnu.org/licenses/>.
import re
import logging
import os
import re
import tempfile
from subprocess import check_call, Popen, PIPE, CalledProcessError
from morituri.common import log, common
from morituri.image import toc, table
from morituri.common import task as ctask
from morituri.image.toc import TocFile
from morituri.extern.task import task
CDRDAO = 'cdrdao'
class ProgramError(Exception):
def read_toc(device, fast_toc=False):
"""
The program had a fatal error.
Return cdrdao-generated table of contents for 'device'.
"""
# cdrdao MUST be passed a non-existing filename as its last argument
# to write the TOC to; it does not support writing to stdout or
# overwriting an existing file, nor does linux seem to support
# locking a non-existant file. Thus, this race-condition introducing
# hack is carried from morituri to whipper and will be removed when
# cdrdao is fixed.
fd, tocfile = tempfile.mkstemp(suffix=u'.cdrdao.read-toc.whipper')
os.close(fd)
os.unlink(tocfile)
def __init__(self, errorMessage):
self.args = (errorMessage, )
self.errorMessage = errorMessage
cmd = [CDRDAO, 'read-toc'] + (['--fast-toc'] if fast_toc else []) + [
'--device', device, tocfile]
# PIPE is the closest to >/dev/null we can get
try:
check_call(cmd, stdout=PIPE, stderr=PIPE)
except CalledProcessError, e:
logging.warning('cdrdao read-toc failed: return code is non-zero: ' +
str(e.returncode))
raise e
toc = TocFile(tocfile)
toc.parse()
os.unlink(tocfile)
return toc
states = ['START', 'TRACK', 'LEADOUT', 'DONE']
_VERSION_RE = re.compile(r'^Cdrdao version (?P<version>.*) - \(C\)')
_ANALYZING_RE = re.compile(r'^Analyzing track (?P<track>\d+).*')
_TRACK_RE = re.compile(r"""
^(?P<track>[\d\s]{2})\s+ # Track
(?P<mode>\w+)\s+ # Mode; AUDIO
\d\s+ # Flags
\d\d:\d\d:\d\d # Start in HH:MM:FF
\((?P<start>.+)\)\s+ # Start in frames
\d\d:\d\d:\d\d # Length in HH:MM:FF
\((?P<length>.+)\) # Length in frames
""", re.VERBOSE)
_LEADOUT_RE = re.compile(r"""
^Leadout\s
\w+\s+ # Mode
\d\s+ # Flags
\d\d:\d\d:\d\d # Start in HH:MM:FF
\((?P<start>.+)\) # Start in frames
""", re.VERBOSE)
_POSITION_RE = re.compile(r"""
^(?P<hh>\d\d): # HH
(?P<mm>\d\d): # MM
(?P<ss>\d\d) # SS
""", re.VERBOSE)
_ERROR_RE = re.compile(r"""^ERROR: (?P<error>.*)""")
class LineParser(object, log.Loggable):
def version():
"""
Parse incoming bytes into lines
Calls 'parse' on owner for each parsed line.
Return cdrdao version as a string.
"""
cdrdao = Popen(CDRDAO, stderr=PIPE)
out, err = cdrdao.communicate()
if cdrdao.returncode != 1:
logging.warning("cdrdao version detection failed: "
"return code is " + str(cdrdao.returncode))
return None
m = re.compile(r'^Cdrdao version (?P<version>.*) - \(C\)').search(
err.decode('utf-8'))
if not m:
logging.warning("cdrdao version detection failed: "
+ "could not find version")
return None
return m.group('version')
def __init__(self, owner):
self._buffer = "" # accumulate characters
self._lines = [] # accumulate lines
self._owner = owner
def read(self, bytes):
self.log('received %d bytes', len(bytes))
self._buffer += bytes
# parse buffer into lines if possible, and parse them
if "\n" in self._buffer:
self.log('buffer has newline, splitting')
lines = self._buffer.split('\n')
if lines[-1] != "\n":
# last line didn't end yet
self.log('last line still in progress')
self._buffer = lines[-1]
del lines[-1]
else:
self.log('last line finished, resetting buffer')
self._buffer = ""
for line in lines:
self.log('Parsing %s', line)
self._owner.parse(line)
self._lines.extend(lines)
class OutputParser(object, log.Loggable):
def __init__(self, taskk, session=None):
self._buffer = "" # accumulate characters
self._lines = [] # accumulate lines
self._state = 'START'
self._frames = None # number of frames
self.track = 0 # which track are we analyzing?
self._task = taskk
self.tracks = 0 # count of tracks, relative to session
self._session = session
self.table = table.Table() # the index table for the TOC
self.version = None # cdrdao version
def read(self, bytes):
self.log('received %d bytes in state %s', len(bytes), self._state)
self._buffer += bytes
# find counter in LEADOUT state; only when we read full toc
self.log('state: %s, buffer bytes: %d', self._state, len(self._buffer))
if self._buffer and self._state == 'LEADOUT':
# split on lines that end in \r, which reset cursor to counter
# start
# this misses the first one, but that's ok:
# length 03:40:71...\n00:01:00
times = self._buffer.split('\r')
# counter ends in \r, so the last one would be empty
if not times[-1]:
del times[-1]
position = ""
m = None
while times and not m:
position = times.pop()
m = _POSITION_RE.search(position)
# we need both a position reported and an Analyzing line
# to have been parsed to report progress
if m and self.track is not None:
track = self.table.tracks[self.track - 1]
frame = (track.getIndex(1).absolute or 0) \
+ int(m.group('hh')) * 60 * common.FRAMES_PER_SECOND \
+ int(m.group('mm')) * common.FRAMES_PER_SECOND \
+ int(m.group('ss'))
self.log('at frame %d of %d', frame, self._frames)
self._task.setProgress(float(frame) / self._frames)
# parse buffer into lines if possible, and parse them
if "\n" in self._buffer:
self.log('buffer has newline, splitting')
lines = self._buffer.split('\n')
if lines[-1] != "\n":
# last line didn't end yet
self.log('last line still in progress')
self._buffer = lines[-1]
del lines[-1]
else:
self.log('last line finished, resetting buffer')
self._buffer = ""
for line in lines:
self.log('Parsing %s', line)
m = _ERROR_RE.search(line)
if m:
error = m.group('error')
self._task.errors.append(error)
self.debug('Found ERROR: output: %s', error)
self._task.exception = ProgramError(error)
self._task.abort()
return
self._parse(lines)
self._lines.extend(lines)
def _parse(self, lines):
for line in lines:
#print 'parsing', len(line), line
methodName = "_parse_" + self._state
getattr(self, methodName)(line)
def _parse_START(self, line):
if line.startswith('Cdrdao version'):
m = _VERSION_RE.search(line)
self.version = m.group('version')
if line.startswith('Track'):
self.debug('Found possible track line')
if line == "Track Mode Flags Start Length":
self.debug('Found track line, moving to TRACK state')
self._state = 'TRACK'
return
m = _ERROR_RE.search(line)
if m:
error = m.group('error')
self._task.errors.append(error)
def _parse_TRACK(self, line):
if line.startswith('---'):
return
m = _TRACK_RE.search(line)
if m:
t = int(m.group('track'))
self.tracks += 1
track = table.Track(self.tracks, session=self._session)
track.index(1, absolute=int(m.group('start')))
self.table.tracks.append(track)
self.debug('Found absolute track %d, session-relative %d', t,
self.tracks)
m = _LEADOUT_RE.search(line)
if m:
self.debug('Found leadout line, moving to LEADOUT state')
self._state = 'LEADOUT'
self._frames = int(m.group('start'))
self.debug('Found absolute leadout at offset %r', self._frames)
self.info('%d tracks found for this session', self.tracks)
return
def _parse_LEADOUT(self, line):
m = _ANALYZING_RE.search(line)
if m:
self.debug('Found analyzing line')
track = int(m.group('track'))
self.description = 'Analyzing track %d...' % track
self.track = track
# FIXME: handle errors
class CDRDAOTask(ctask.PopenTask):
def ReadTOCTask(device):
"""
I am a task base class that runs CDRDAO.
stopgap morituri-insanity compatibility layer
"""
return read_toc(device, fast_toc=True)
logCategory = 'CDRDAOTask'
description = "Reading TOC..."
options = None
def __init__(self):
self.errors = []
self.debug('creating CDRDAOTask')
def start(self, runner):
self.debug('Starting cdrdao with options %r', self.options)
self.command = ['cdrdao', ] + self.options
ctask.PopenTask.start(self, runner)
def commandMissing(self):
raise common.MissingDependencyException('cdrdao')
def failed(self):
if self.errors:
raise DeviceOpenException("\n".join(self.errors))
else:
raise ProgramFailedException(self._popen.returncode)
class DiscInfoTask(CDRDAOTask):
def ReadTableTask(device):
"""
I am a task that reads information about a disc.
@ivar sessions: the number of sessions
@type sessions: int
stopgap morituri-insanity compatibility layer
"""
logCategory = 'DiscInfoTask'
description = "Scanning disc..."
table = None
sessions = None
def __init__(self, device=None):
"""
@param device: the device to rip from
@type device: str
"""
self.debug('creating DiscInfoTask for device %r', device)
CDRDAOTask.__init__(self)
self.options = ['disk-info', ]
if device:
self.options.extend(['--device', device, ])
self.parser = LineParser(self)
def readbytesout(self, bytes):
self.parser.read(bytes)
def readbyteserr(self, bytes):
self.parser.read(bytes)
def parse(self, line):
# called by parser
if line.startswith('Sessions'):
self.sessions = int(line[line.find(':') + 1:])
self.debug('Found %d sessions', self.sessions)
m = _ERROR_RE.search(line)
if m:
error = m.group('error')
self.errors.append(error)
def done(self):
pass
# Read stuff for one session
class ReadSessionTask(CDRDAOTask):
"""
I am a task that reads things for one session.
@ivar table: the index table
@type table: L{table.Table}
"""
logCategory = 'ReadSessionTask'
description = "Reading session"
table = None
extraOptions = None
def __init__(self, session=None, device=None):
"""
@param session: the session to read
@type session: int
@param device: the device to rip from
@type device: str
"""
self.debug('Creating ReadSessionTask for session %d on device %r',
session, device)
CDRDAOTask.__init__(self)
self.parser = OutputParser(self)
(fd, self._tocfilepath) = tempfile.mkstemp(
suffix=u'.readtablesession.morituri')
os.close(fd)
os.unlink(self._tocfilepath)
self.options = ['read-toc', ]
if device:
self.options.extend(['--device', device, ])
if session:
self.options.extend(['--session', str(session)])
self.description = "%s of session %d..." % (
self.description, session)
if self.extraOptions:
self.options.extend(self.extraOptions)
self.options.extend([self._tocfilepath, ])
def readbyteserr(self, bytes):
self.parser.read(bytes)
if self.parser.tracks > 0:
self.setProgress(float(self.parser.track - 1) / self.parser.tracks)
def done(self):
# by merging the TOC info.
self._tocfile = toc.TocFile(self._tocfilepath)
self._tocfile.parse()
os.unlink(self._tocfilepath)
self.table = self._tocfile.table
# we know the .toc file represents a single wav rip, so all offsets
# are absolute since beginning of disc
self.table.absolutize()
# we unset relative since there is no real file backing this toc
for t in self.table.tracks:
for i in t.indexes.values():
#i.absolute = i.relative
i.relative = None
# copy the leadout from the parser's table
# FIXME: how do we get the length of the last audio track in the case
# of a data track ?
# self.table.leadout = self.parser.table.leadout
# we should have parsed it from the initial output
assert self.table.leadout is not None
class ReadTableSessionTask(ReadSessionTask):
"""
I am a task that reads all indexes of a CD for a session.
@ivar table: the index table
@type table: L{table.Table}
"""
logCategory = 'ReadTableSessionTask'
description = "Scanning indexes"
class ReadTOCSessionTask(ReadSessionTask):
"""
I am a task that reads the TOC of a CD, without pregaps.
@ivar table: the index table that matches the TOC.
@type table: L{table.Table}
"""
logCategory = 'ReadTOCSessTask'
description = "Reading TOC"
extraOptions = ['--fast-toc', ]
def done(self):
ReadSessionTask.done(self)
assert self.table.hasTOC(), "This Table Index should be a TOC"
# read all sessions
class ReadAllSessionsTask(task.MultiSeparateTask):
"""
I am a base class for tasks that need to read all sessions.
@ivar table: the index table
@type table: L{table.Table}
"""
logCategory = 'ReadAllSessionsTask'
table = None
_readClass = None
def __init__(self, device=None):
"""
@param device: the device to rip from
@type device: str
"""
task.MultiSeparateTask.__init__(self)
self._device = device
self.debug('Starting ReadAllSessionsTask')
self.tasks = [DiscInfoTask(device=device), ]
def stopped(self, taskk):
if not taskk.exception:
# After first task, schedule additional ones
if taskk == self.tasks[0]:
for i in range(taskk.sessions):
self.tasks.append(self._readClass(session=i + 1,
device=self._device))
if self._task == len(self.tasks):
self.table = self.tasks[1].table
if len(self.tasks) > 2:
for i, t in enumerate(self.tasks[2:]):
self.table.merge(t.table, i + 2)
assert self.table.leadout is not None
task.MultiSeparateTask.stopped(self, taskk)
class ReadTableTask(ReadAllSessionsTask):
"""
I am a task that reads all indexes of a CD for all sessions.
@ivar table: the index table
@type table: L{table.Table}
"""
logCategory = 'ReadTableTask'
description = "Scanning indexes..."
_readClass = ReadTableSessionTask
class ReadTOCTask(ReadAllSessionsTask):
"""
I am a task that reads the TOC of a CD, without pregaps.
@ivar table: the index table that matches the TOC.
@type table: L{table.Table}
"""
logCategory = 'ReadTOCTask'
description = "Reading TOC..."
_readClass = ReadTOCSessionTask
class DeviceOpenException(Exception):
def __init__(self, msg):
self.msg = msg
self.args = (msg, )
class ProgramFailedException(Exception):
def __init__(self, code):
self.code = code
self.args = (code, )
_VERSION_RE = re.compile(
"^Cdrdao version (?P<version>.+) -")
return read_toc(device)
def getCDRDAOVersion():
getter = common.VersionGetter('cdrdao',
["cdrdao"],
_VERSION_RE,
"%(version)s")
return getter.get()
"""
stopgap morituri-insanity compatibility layer
"""
return version()

View File

@@ -10,7 +10,6 @@ from morituri.common import log, logcommand, common, config, directory
from morituri.configure import configure
from morituri.extern.command import command
from morituri.extern.task import task
from morituri.program import cdrdao
from morituri.rip import cd, offset, drive, image, accurip, debug
@@ -33,13 +32,6 @@ def main():
sys.stderr.write('rip: error: missing dependency "%s"\n' %
e.exception.dependency)
return 255
# FIXME: move this exception
if isinstance(e.exception, cdrdao.DeviceOpenException):
sys.stderr.write("""rip: error: cannot read CD from drive.
cdrdao says:
%s
""" % e.exception.msg)
return 255
if isinstance(e.exception, common.EmptyError):
log.debug('main',

View File

@@ -100,14 +100,7 @@ CD in the AccurateRip database."""
prog.unmountDevice(device)
# first get the Table Of Contents of the CD
t = cdrdao.ReadTOCTask(device=device)
try:
runner.run(t)
except cdrdao.DeviceOpenException, e:
self.error(e.msg)
return 3
t = cdrdao.ReadTOCTask(device)
table = t.table
self.debug("CDDB disc id: %r", table.getCDDBDiscId())

View File

@@ -7,38 +7,9 @@ from morituri.program import cdrdao
from morituri.test import common
class FakeTask:
def setProgress(self, value):
pass
class ParseTestCase(common.TestCase):
def setUp(self):
path = os.path.join(os.path.dirname(__file__),
'cdrdao.readtoc.progress')
self._parser = cdrdao.OutputParser(FakeTask())
self._handle = open(path)
def testParse(self):
# FIXME: we should be testing splitting in byte blocks, not lines
for line in self._handle.readlines():
self._parser.read(line)
for i, offset in enumerate(
[0, 13864, 31921, 48332, 61733, 80961,
100219, 116291, 136188, 157504, 175275]):
track = self._parser.table.tracks[i]
self.assertEquals(track.getIndex(1).absolute, offset)
self.assertEquals(self._parser.version, '1.2.2')
# TODO: Current test architecture makes testing cdrdao difficult. Revisit.
class VersionTestCase(common.TestCase):
def testGetVersion(self):
v = cdrdao.getCDRDAOVersion()
self.failUnless(v)