* morituri/rip/cd.py:
Add asserts for comparing id's between the simple toc and the full table. Create the output directory before ripping the htoa. Ignore data tracks for now. Don't fail if we have no AccurateRip responses. * morituri/image/table.py: Add a session ivar to Track. Factor in session leadin when calculating track length of last track in a session. add getMusicBrainzSubmitURL() add _getSessionGap() because the session gap size is different for session 2 and all following. Use it in merge() to get offsets right. Fix getAccurateRipURL by only using the audio tracks for the 'length in tracks' number Temporarily disable writing out data tracks to a .cue file, since it's not implemented yet. Add canCue to see if we can write a .cue file from the given table, and debug why not if not. * morituri/program/cdrdao.py: Rework to rip each session separately instead of using session 9. This fixes session 9 read-toc missing the pregap. Add a simple LineParser for handling output from disk-info. Count tracks relatively for the session, because the output for session 2 for track numbers picks up where session 1 left off. Don't set leadout from TOC printing since for the same reason session 2's leadout is absolute, not relative to start of session. Add a DiscInfoTask. Convert Table and Toc reading tasks to multitasks, first getting the number of sessions, then reading table/toc for each session. * morituri/test/test_image_table.py: Fix up MusicBrainz disc id for my Ladyhawke disc. Add AccurateRip URL verification, compared against EAC's. * morituri/test/test_image_toc.py: Use two separate session read-toc output files to verify the case of Das Capital. Verify musicbrainz URL.
This commit is contained in:
42
ChangeLog
42
ChangeLog
@@ -1,3 +1,45 @@
|
||||
2009-05-25 Thomas Vander Stichele <thomas at apestaart dot org>
|
||||
|
||||
* morituri/rip/cd.py:
|
||||
Add asserts for comparing id's between the simple toc and
|
||||
the full table.
|
||||
Create the output directory before ripping the htoa.
|
||||
Ignore data tracks for now.
|
||||
Don't fail if we have no AccurateRip responses.
|
||||
* morituri/image/table.py:
|
||||
Add a session ivar to Track.
|
||||
Factor in session leadin when calculating track length
|
||||
of last track in a session.
|
||||
add getMusicBrainzSubmitURL()
|
||||
add _getSessionGap() because the session gap size is different
|
||||
for session 2 and all following.
|
||||
Use it in merge() to get offsets right.
|
||||
Fix getAccurateRipURL by only using the audio tracks for the
|
||||
'length in tracks' number
|
||||
Temporarily disable writing out data tracks to a .cue file,
|
||||
since it's not implemented yet.
|
||||
Add canCue to see if we can write a .cue file from the given table,
|
||||
and debug why not if not.
|
||||
* morituri/program/cdrdao.py:
|
||||
Rework to rip each session separately instead of using session 9.
|
||||
This fixes session 9 read-toc missing the pregap.
|
||||
Add a simple LineParser for handling output from disk-info.
|
||||
Count tracks relatively for the session, because the output for
|
||||
session 2 for track numbers picks up where session 1 left off.
|
||||
Don't set leadout from TOC printing since for the same reason
|
||||
session 2's leadout is absolute, not relative to start of session.
|
||||
Add a DiscInfoTask.
|
||||
Convert Table and Toc reading tasks to multitasks, first getting the
|
||||
number of sessions, then reading table/toc for each session.
|
||||
|
||||
* morituri/test/test_image_table.py:
|
||||
Fix up MusicBrainz disc id for my Ladyhawke disc.
|
||||
Add AccurateRip URL verification, compared against EAC's.
|
||||
* morituri/test/test_image_toc.py:
|
||||
Use two separate session read-toc output files to verify
|
||||
the case of Das Capital.
|
||||
Verify musicbrainz URL.
|
||||
|
||||
2009-05-25 Thomas Vander Stichele <thomas at apestaart dot org>
|
||||
|
||||
* morituri/common/task.py:
|
||||
|
||||
@@ -25,6 +25,8 @@ Wrap Table of Contents.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import urllib
|
||||
import urlparse
|
||||
|
||||
from morituri.common import task, common, log
|
||||
|
||||
@@ -65,11 +67,12 @@ class Track:
|
||||
indexes = None
|
||||
isrc = None
|
||||
cdtext = None
|
||||
session = None
|
||||
|
||||
def __repr__(self):
|
||||
return '<Track %02d>' % self.number
|
||||
|
||||
def __init__(self, number, audio=True):
|
||||
def __init__(self, number, audio=True, session=None):
|
||||
self.number = number
|
||||
self.audio = audio
|
||||
self.indexes = {}
|
||||
@@ -123,7 +126,6 @@ class Table(object, log.Loggable):
|
||||
@type tracks: list of L{Track}
|
||||
@ivar catalog: catalog number
|
||||
@type catalog: str
|
||||
@ivar version: version number of the object and its API.
|
||||
"""
|
||||
|
||||
tracks = None # list of Track
|
||||
@@ -131,7 +133,7 @@ class Table(object, log.Loggable):
|
||||
catalog = None # catalog number; FIXME: is this UPC ?
|
||||
cdtext = None
|
||||
|
||||
version = 1
|
||||
classVersion = 1
|
||||
|
||||
def __init__(self, tracks=None):
|
||||
if not tracks:
|
||||
@@ -139,6 +141,10 @@ class Table(object, log.Loggable):
|
||||
|
||||
self.tracks = tracks
|
||||
self.cdtext = {}
|
||||
self.logName = "Table 0x%08X" % id(self)
|
||||
# done this way because just having a class-defined instance var
|
||||
# gets overridden when unpickling
|
||||
self.instanceVersion = self.classVersion
|
||||
|
||||
def getTrackStart(self, number):
|
||||
"""
|
||||
@@ -159,9 +165,20 @@ class Table(object, log.Loggable):
|
||||
@returns: the end of the given track number (ie index 1 of next track)
|
||||
@rtype: int
|
||||
"""
|
||||
# default to end of disc
|
||||
end = self.leadout - 1
|
||||
|
||||
# if not last track, calculate it from the next track
|
||||
if number < len(self.tracks):
|
||||
end = self.tracks[number].getIndex(1).absolute - 1
|
||||
|
||||
# if on a session border, subtract the session leadin
|
||||
this = self.tracks[number - 1]
|
||||
next = self.tracks[number]
|
||||
if next.session > this.session:
|
||||
gap = self._getSessionGap(next.session)
|
||||
end -= gap
|
||||
|
||||
return end
|
||||
|
||||
def getTrackLength(self, number):
|
||||
@@ -245,6 +262,8 @@ class Table(object, log.Loggable):
|
||||
@rtype: str
|
||||
@returns: the 28-character base64-encoded disc ID
|
||||
"""
|
||||
values = self._getMusicBrainzValues()
|
||||
|
||||
# MusicBrainz disc id does not take into account data tracks
|
||||
import sha
|
||||
import base64
|
||||
@@ -252,26 +271,17 @@ class Table(object, log.Loggable):
|
||||
sha1 = sha.new()
|
||||
|
||||
# number of first track
|
||||
sha1.update("%02X" % 1)
|
||||
sha1.update("%02X" % values[0])
|
||||
|
||||
# number of last track
|
||||
sha1.update("%02X" % self.getAudioTracks())
|
||||
sha1.update("%02X" % values[1])
|
||||
|
||||
leadout = self.leadout
|
||||
# if the disc is multi-session, last track is the data track,
|
||||
# and we should subtract 11250 + 150 from the last track's offset
|
||||
# for the leadout
|
||||
if self.hasDataTracks():
|
||||
assert not self.tracks[-1].audio
|
||||
leadout = self.tracks[-1].getIndex(1).absolute - 11250 - 150
|
||||
|
||||
# treat leadout offset as track 0 offset
|
||||
sha1.update("%08X" % (150 + leadout))
|
||||
sha1.update("%08X" % values[2])
|
||||
|
||||
# offsets of tracks
|
||||
for i in range(1, 100):
|
||||
try:
|
||||
offset = self.tracks[i - 1].getIndex(1).absolute + 150
|
||||
offset = values[2 + i]
|
||||
except IndexError:
|
||||
#print 'track', i - 1, '0 offset'
|
||||
offset = 0
|
||||
@@ -295,6 +305,71 @@ class Table(object, log.Loggable):
|
||||
|
||||
return result
|
||||
|
||||
def getMusicBrainzSubmitURL(self):
|
||||
host = 'mm.musicbrainz.org'
|
||||
|
||||
discid = self.getMusicBrainzDiscId()
|
||||
values = self._getMusicBrainzValues()
|
||||
|
||||
query = urllib.urlencode({
|
||||
'id': discid,
|
||||
'toc': ' '.join([str(v) for v in values]),
|
||||
'tracks': self.getAudioTracks()
|
||||
})
|
||||
|
||||
return urlparse.urlunparse((
|
||||
'http', host, '/bare/cdlookup.html', '', query, ''))
|
||||
|
||||
|
||||
def _getMusicBrainzValues(self):
|
||||
"""
|
||||
Get all MusicBrainz values needed to calculate disc id and submit URL.
|
||||
|
||||
This includes:
|
||||
- track number of first track
|
||||
- number of audio tracks
|
||||
- leadout of disc
|
||||
- offset of index 1 of each track
|
||||
|
||||
@rtype: list of int
|
||||
"""
|
||||
# MusicBrainz disc id does not take into account data tracks
|
||||
|
||||
result = []
|
||||
|
||||
# number of first track
|
||||
result.append(1)
|
||||
|
||||
# number of last audio track
|
||||
result.append(self.getAudioTracks())
|
||||
|
||||
leadout = self.leadout
|
||||
# if the disc is multi-session, last track is the data track,
|
||||
# and we should subtract 11250 + 150 from the last track's offset
|
||||
# for the leadout
|
||||
if self.hasDataTracks():
|
||||
assert not self.tracks[-1].audio
|
||||
leadout = self.tracks[-1].getIndex(1).absolute - 11250 - 150
|
||||
|
||||
# treat leadout offset as track 0 offset
|
||||
result.append(150 + leadout)
|
||||
|
||||
# offsets of tracks
|
||||
for i in range(1, 100):
|
||||
try:
|
||||
track = self.tracks[i - 1]
|
||||
if not track.audio:
|
||||
continue
|
||||
offset = track.getIndex(1).absolute + 150
|
||||
result.append(offset)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
|
||||
self.debug('Musicbrainz values: %r', result)
|
||||
return result
|
||||
|
||||
|
||||
def getAccurateRipIds(self):
|
||||
"""
|
||||
Calculate the two AccurateRip ID's.
|
||||
@@ -338,7 +413,7 @@ class Table(object, log.Loggable):
|
||||
return "http://www.accuraterip.com/accuraterip/" \
|
||||
"%s/%s/%s/dBAR-%.3d-%s-%s-%s.bin" % (
|
||||
discId1[-1], discId1[-2], discId1[-3],
|
||||
len(self.tracks), discId1, discId2, self.getCDDBDiscId())
|
||||
self.getAudioTracks(), discId1, discId2, self.getCDDBDiscId())
|
||||
|
||||
def cue(self, program='Morituri'):
|
||||
"""
|
||||
@@ -370,6 +445,10 @@ class Table(object, log.Loggable):
|
||||
lines.append('FILE "%s" WAVE' % path)
|
||||
|
||||
for i, track in enumerate(self.tracks):
|
||||
# FIXME: skip data tracks for now
|
||||
if not track.audio:
|
||||
continue
|
||||
|
||||
# if there is no index 0, but there is a new file, advance
|
||||
# FILE line here
|
||||
if not track.indexes.has_key(0):
|
||||
@@ -503,18 +582,7 @@ class Table(object, log.Loggable):
|
||||
|
||||
@type other: L{Table}
|
||||
"""
|
||||
# From cdrecord multi-session info:
|
||||
# For the first additional session this is 11250 sectors
|
||||
# lead-out/lead-in overhead + 150 sectors for the pre-gap of the first
|
||||
# track after the lead-in = 11400 sectos.
|
||||
|
||||
# For all further session this is 6750 sectors lead-out/lead-in
|
||||
# overhead + 150 sectors for the pre-gap of the first track after the
|
||||
# lead-in = 6900 sectors.
|
||||
|
||||
gap = 11400
|
||||
if session > 2:
|
||||
gap = 6900
|
||||
gap = self._getSessionGap(session)
|
||||
|
||||
trackCount = len(self.tracks)
|
||||
sourceCounter = self.tracks[-1].getLastIndex().counter
|
||||
@@ -522,6 +590,7 @@ class Table(object, log.Loggable):
|
||||
for track in other.tracks:
|
||||
t = copy.deepcopy(track)
|
||||
t.number = track.number + trackCount
|
||||
t.session = session
|
||||
for i in t.indexes.values():
|
||||
if i.absolute is not None:
|
||||
i.absolute += self.leadout + gap
|
||||
@@ -534,6 +603,22 @@ class Table(object, log.Loggable):
|
||||
self.tracks.append(t)
|
||||
|
||||
self.leadout += other.leadout + gap # FIXME
|
||||
self.debug('Fixing leadout, now %d', self.leadout)
|
||||
|
||||
def _getSessionGap(self, session):
|
||||
# From cdrecord multi-session info:
|
||||
# For the first additional session this is 11250 sectors
|
||||
# lead-out/lead-in overhead + 150 sectors for the pre-gap of the first
|
||||
# track after the lead-in = 11400 sectos.
|
||||
|
||||
# For all further session this is 6750 sectors lead-out/lead-in
|
||||
# overhead + 150 sectors for the pre-gap of the first track after the
|
||||
# lead-in = 6900 sectors.
|
||||
|
||||
gap = 11400
|
||||
if session > 2:
|
||||
gap = 6900
|
||||
return gap
|
||||
|
||||
### lookups
|
||||
def getNextTrackIndex(self, track, index):
|
||||
@@ -584,3 +669,20 @@ class Table(object, log.Loggable):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def canCue(self):
|
||||
"""
|
||||
Check if this table can be used to generate a .cue file
|
||||
"""
|
||||
if not self.hasTOC():
|
||||
self.debug('No TOC, cannot cue')
|
||||
return False
|
||||
|
||||
for t in self.tracks:
|
||||
for i in t.indexes.values():
|
||||
if i.relative is None:
|
||||
self.debug('Track %02d, Index %02d does not have relative',
|
||||
t.number, i.number)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -67,9 +67,41 @@ _POSITION_RE = re.compile(r"""
|
||||
(?P<ss>\d\d) # SS
|
||||
""", re.VERBOSE)
|
||||
|
||||
class LineParser(object, log.Loggable):
|
||||
"""
|
||||
Parse incoming bytes into lines
|
||||
Calls 'parse' on owner for each parsed line.
|
||||
"""
|
||||
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):
|
||||
def __init__(self, taskk, session=None):
|
||||
self._buffer = "" # accumulate characters
|
||||
self._lines = [] # accumulate lines
|
||||
self._errors = [] # accumulate error lines
|
||||
@@ -77,6 +109,8 @@ class OutputParser(object, log.Loggable):
|
||||
self._frames = None # number of frames
|
||||
self._track = None # 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
|
||||
|
||||
@@ -154,20 +188,21 @@ class OutputParser(object, log.Loggable):
|
||||
|
||||
m = _TRACK_RE.search(line)
|
||||
if m:
|
||||
self._tracks = int(m.group('track'))
|
||||
track = table.Track(self._tracks)
|
||||
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 track %d', self._tracks)
|
||||
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 leadout at offset %r', self._frames)
|
||||
self.table.leadout = self._frames
|
||||
self.info('%d tracks found', self._tracks)
|
||||
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):
|
||||
@@ -208,11 +243,17 @@ class CDRDAOTask(task.Task):
|
||||
self.runner.schedule(1.0, self._read, runner)
|
||||
|
||||
def _read(self, runner):
|
||||
ret = self._popen.recv()
|
||||
|
||||
if ret:
|
||||
self.log("read from stdout: %s", ret)
|
||||
self.readbytesout(ret)
|
||||
|
||||
ret = self._popen.recv_err()
|
||||
|
||||
if ret:
|
||||
self.log("read from stderr: %s", ret)
|
||||
self.readbytes(ret)
|
||||
self.readbyteserr(ret)
|
||||
|
||||
if self._popen.poll() is None:
|
||||
# not finished yet
|
||||
@@ -239,11 +280,17 @@ class CDRDAOTask(task.Task):
|
||||
os.kill(self._popen.pid, signal.SIGTERM)
|
||||
self.stop()
|
||||
|
||||
def readbytes(self, bytes):
|
||||
def readbytesout(self, bytes):
|
||||
"""
|
||||
Called when bytes have been read from stdout.
|
||||
"""
|
||||
pass
|
||||
|
||||
def readbyteserr(self, bytes):
|
||||
"""
|
||||
Called when bytes have been read from stderr.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
pass
|
||||
|
||||
def done(self):
|
||||
"""
|
||||
@@ -251,39 +298,86 @@ class CDRDAOTask(task.Task):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ReadTableTask(CDRDAOTask):
|
||||
class DiscInfoTask(CDRDAOTask):
|
||||
"""
|
||||
I am a task that reads all indexes of a CD.
|
||||
I am a task that reads information about a disc.
|
||||
|
||||
@ivar sessions: the number of sessions
|
||||
@type sessions: int
|
||||
"""
|
||||
|
||||
description = "Scanning disc..."
|
||||
table = None
|
||||
|
||||
def __init__(self, device=None):
|
||||
"""
|
||||
@param device: the device to rip from
|
||||
@type device: str
|
||||
"""
|
||||
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 parse(self, line):
|
||||
# called by parser
|
||||
if line.startswith('Sessions'):
|
||||
self.sessions = int (line[line.find(':') + 1:])
|
||||
self.debug('Found %d sessions', self.sessions)
|
||||
|
||||
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}
|
||||
"""
|
||||
|
||||
description = "Scanning indexes..."
|
||||
description = "Reading session"
|
||||
table = None
|
||||
extraOptions = None
|
||||
|
||||
def __init__(self, device=None):
|
||||
def __init__(self, session=None, device=None):
|
||||
"""
|
||||
@param device: the device to rip from
|
||||
@type device: str
|
||||
@param session: the session to read
|
||||
@type session: int
|
||||
@param device: the device to rip from
|
||||
@type device: str
|
||||
"""
|
||||
CDRDAOTask.__init__(self)
|
||||
self.parser = OutputParser(self)
|
||||
(fd, self._tocfilepath) = tempfile.mkstemp(suffix='.morituri')
|
||||
(fd, self._tocfilepath) = tempfile.mkstemp(
|
||||
suffix='.readtablesession.morituri')
|
||||
os.close(fd)
|
||||
os.unlink(self._tocfilepath)
|
||||
|
||||
self.options = ['read-toc', ]
|
||||
if device:
|
||||
self.options.extend(['--device', device, ])
|
||||
self.options.extend(['--session', '9', self._tocfilepath, ])
|
||||
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)
|
||||
|
||||
def readbytes(self, bytes):
|
||||
self.options.extend([self._tocfilepath, ])
|
||||
|
||||
def readbyteserr(self, bytes):
|
||||
self.parser.read(bytes)
|
||||
|
||||
def done(self):
|
||||
# FIXME: instead of reading only a TOC, output a complete Table
|
||||
# by merging the TOC info.
|
||||
self._tocfile = toc.TocFile(self._tocfilepath)
|
||||
self._tocfile.parse()
|
||||
@@ -302,12 +396,91 @@ class ReadTableTask(CDRDAOTask):
|
||||
# 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
|
||||
# self.table.leadout = self.parser.table.leadout
|
||||
|
||||
# we should have parsed it from the initial output
|
||||
assert self.table.leadout is not None
|
||||
|
||||
class ReadTOCTask(CDRDAOTask):
|
||||
|
||||
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}
|
||||
"""
|
||||
|
||||
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}
|
||||
"""
|
||||
|
||||
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}
|
||||
"""
|
||||
|
||||
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.tasks = [DiscInfoTask(device=device), ]
|
||||
|
||||
def stopped(self, taskk):
|
||||
# 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}
|
||||
"""
|
||||
|
||||
description = "Scanning indexes..."
|
||||
_readClass = ReadTableSessionTask
|
||||
|
||||
class ReadTOCTask(ReadAllSessionsTask):
|
||||
"""
|
||||
I am a task that reads the TOC of a CD, without pregaps.
|
||||
|
||||
@@ -316,32 +489,4 @@ class ReadTOCTask(CDRDAOTask):
|
||||
"""
|
||||
|
||||
description = "Reading TOC..."
|
||||
table = None
|
||||
|
||||
def __init__(self, device=None):
|
||||
"""
|
||||
@param device: the device to rip from
|
||||
@type device: str
|
||||
"""
|
||||
CDRDAOTask.__init__(self)
|
||||
self.parser = OutputParser(self)
|
||||
|
||||
(fd, self._toc) = tempfile.mkstemp(suffix='.morituri')
|
||||
os.close(fd)
|
||||
os.unlink(self._toc)
|
||||
|
||||
# Reading a non-existent session gives you output for all sessions
|
||||
# 9 should be a safe number
|
||||
self.options = ['read-toc', '--fast-toc', ]
|
||||
if device:
|
||||
self.options.extend(['--device', device, ])
|
||||
self.options.extend(['--session', '9', self._toc, ])
|
||||
|
||||
def readbytes(self, bytes):
|
||||
self.parser.read(bytes)
|
||||
|
||||
def done(self):
|
||||
os.unlink(self._toc)
|
||||
self.table = self.parser.table
|
||||
|
||||
assert self.table.hasTOC(), "This Table Index should be a TOC"
|
||||
_readClass = ReadTOCSessionTask
|
||||
|
||||
@@ -150,8 +150,12 @@ def getPath(outdir, template, metadata, i):
|
||||
v['A'] = filterForPath(metadata.artist)
|
||||
v['d'] = filterForPath(metadata.title)
|
||||
if i >= 0:
|
||||
v['a'] = filterForPath(metadata.tracks[i].artist)
|
||||
v['n'] = filterForPath(metadata.tracks[i].title)
|
||||
try:
|
||||
v['a'] = filterForPath(metadata.tracks[i - 1].artist)
|
||||
v['n'] = filterForPath(metadata.tracks[i - 1].title)
|
||||
except IndexError, e:
|
||||
print 'ERROR: no track %d found, %r' % (i, e)
|
||||
raise
|
||||
else:
|
||||
# htoa defaults to disc's artist
|
||||
v['a'] = filterForPath(metadata.artist)
|
||||
@@ -213,13 +217,12 @@ class Rip(logcommand.LogCommand):
|
||||
|
||||
# already show us some info based on this
|
||||
print "CDDB disc id", ittoc.getCDDBDiscId()
|
||||
print "MusicBrainz disc id", ittoc.getMusicBrainzDiscId()
|
||||
|
||||
metadata = musicbrainz(ittoc.getMusicBrainzDiscId())
|
||||
|
||||
url = ittoc.getAccurateRipURL()
|
||||
print "AccurateRip URL", url
|
||||
|
||||
cache = accurip.AccuCache()
|
||||
responses = cache.retrieve(url)
|
||||
if not metadata:
|
||||
print 'Submit this disc to MusicBrainz at:'
|
||||
print ittoc.getMusicBrainzSubmitURL()
|
||||
|
||||
# now, read the complete index table, which is slower
|
||||
path = os.path.join(os.path.expanduser('~'), '.morituri', 'cache',
|
||||
@@ -237,7 +240,12 @@ class Rip(logcommand.LogCommand):
|
||||
assert itable.getCDDBDiscId() == ittoc.getCDDBDiscId(), \
|
||||
"full table's id %s differs from toc id %s" % (
|
||||
itable.getCDDBDiscId(), ittoc.getCDDBDiscId())
|
||||
assert itable.getMusicBrainzDiscId() == ittoc.getMusicBrainzDiscId()
|
||||
assert itable.getMusicBrainzDiscId() == ittoc.getMusicBrainzDiscId(), \
|
||||
"full table's mb id %s differs from toc id mb %s" % (
|
||||
itable.getMusicBrainzDiscId(), ittoc.getMusicBrainzDiscId())
|
||||
assert itable.getAccurateRipURL() == ittoc.getAccurateRipURL(), \
|
||||
"full table's AR URL %s differs from toc AR URL %s" % (
|
||||
itable.getAccurateRipURL(), ittoc.getAccurateRipURL())
|
||||
|
||||
outdir = self.options.output_directory or os.getcwd()
|
||||
|
||||
@@ -257,6 +265,10 @@ class Rip(logcommand.LogCommand):
|
||||
|
||||
# rip it
|
||||
htoapath = getPath(outdir, self.options.track_template, metadata, -1) + '.wav'
|
||||
dirname = os.path.dirname(htoapath)
|
||||
if not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
|
||||
htoalength = stop - start
|
||||
if not os.path.exists(htoapath):
|
||||
print 'Ripping track %d: %s' % (0, os.path.basename(htoapath))
|
||||
@@ -265,7 +277,7 @@ class Rip(logcommand.LogCommand):
|
||||
offset=int(self.options.offset),
|
||||
device=self.parentCommand.options.device)
|
||||
function(runner, t)
|
||||
if t.checksum:
|
||||
if t.checksum is not None:
|
||||
print 'Checksums match for track %d' % 0
|
||||
else:
|
||||
print 'ERROR: checksums did not match for track %d' % 0
|
||||
@@ -274,6 +286,12 @@ class Rip(logcommand.LogCommand):
|
||||
|
||||
|
||||
for i, track in enumerate(itable.tracks):
|
||||
# FIXME: rip data tracks differently
|
||||
if not track.audio:
|
||||
# FIXME: make it work for now
|
||||
track.indexes[1].relative = 0
|
||||
continue
|
||||
|
||||
path = getPath(outdir, self.options.track_template, metadata, i) + '.wav'
|
||||
dirname = os.path.dirname(path)
|
||||
if not os.path.exists(dirname):
|
||||
@@ -305,6 +323,7 @@ class Rip(logcommand.LogCommand):
|
||||
os.makedirs(dirname)
|
||||
|
||||
# write .cue file
|
||||
assert itable.canCue()
|
||||
cuePath = '%s.cue' % discName
|
||||
handle = open(cuePath, 'w')
|
||||
handle.write(itable.cue())
|
||||
@@ -329,13 +348,12 @@ class Rip(logcommand.LogCommand):
|
||||
handle.close()
|
||||
|
||||
# verify using accuraterip
|
||||
print "CDDB disc id", itable.getCDDBDiscId()
|
||||
print "MusicBrainz disc id", itable.getMusicBrainzDiscId()
|
||||
url = itable.getAccurateRipURL()
|
||||
url = ittoc.getAccurateRipURL()
|
||||
print "AccurateRip URL", url
|
||||
|
||||
cache = accurip.AccuCache()
|
||||
responses = cache.retrieve(url)
|
||||
|
||||
if not responses:
|
||||
print 'Album not found in AccurateRip database'
|
||||
|
||||
@@ -364,7 +382,7 @@ class Rip(logcommand.LogCommand):
|
||||
confidence = None
|
||||
|
||||
# match against each response's checksum
|
||||
for j, r in enumerate(responses):
|
||||
for j, r in enumerate(responses or []):
|
||||
if "%08x" % csum == r.checksums[i]:
|
||||
if not response:
|
||||
response = r
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# -*- Mode: Python; test-case-name: morituri.test.test_image_table -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
from morituri.image import table
|
||||
|
||||
import unittest
|
||||
|
||||
from morituri.image import table
|
||||
from morituri.test import common
|
||||
|
||||
def h(i):
|
||||
return "0x%08x" % i
|
||||
@@ -39,13 +41,18 @@ class LadyhawkeTestCase(unittest.TestCase):
|
||||
self.assertEquals(self.table.getCDDBDiscId(), "c60af50d")
|
||||
|
||||
def testMusicBrainz(self):
|
||||
# track
|
||||
# output from mb-submit-disc:
|
||||
# http://mm.musicbrainz.org/bare/cdlookup.html?toc=1+12+195856+150+15687+31841+51016+66616+81352+99559+116070+133243+149997+161710+177832&tracks=12&id=KnpGsLhvH.lPrNc1PBL21lb9Bg4-
|
||||
# however, not (yet) in musicbrainz database
|
||||
|
||||
self.assertEquals(self.table.getMusicBrainzDiscId(),
|
||||
"qrJJkrLvXz5Nkvym3oZM4KI9U4A-")
|
||||
"KnpGsLhvH.lPrNc1PBL21lb9Bg4-")
|
||||
|
||||
def testAccurateRip(self):
|
||||
self.assertEquals(self.table.getAccurateRipIds(), (
|
||||
"0013bd5a", "00b8d489"))
|
||||
self.assertEquals(self.table.getAccurateRipURL(),
|
||||
"http://www.accuraterip.com/accuraterip/a/5/d/dBAR-012-0013bd5a-00b8d489-c60af50d.bin")
|
||||
|
||||
class MusicBrainzTestCase(unittest.TestCase):
|
||||
# example taken from http://musicbrainz.org/doc/DiscIDCalculation
|
||||
|
||||
@@ -179,24 +179,6 @@ class LadyhawkeTestCase(unittest.TestCase):
|
||||
# c60af50d 13 150 15687 31841 51016 66616 81352 99559 116070 133243
|
||||
# 149997 161710 177832 207256 2807
|
||||
|
||||
# Das Capital has a htoa and a data track
|
||||
# the fast.toc was generated with cdrdao read-toc --fast-toc --session 9
|
||||
class CapitalTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.toc = toc.TocFile(os.path.join(os.path.dirname(__file__),
|
||||
'capital.fast.toc'))
|
||||
self.toc.parse()
|
||||
self.assertEquals(len(self.toc.table.tracks), 12)
|
||||
#import code; code.interact(local=locals())
|
||||
self.failIf(self.toc.table.tracks[-1].audio)
|
||||
|
||||
def testCDDBId(self):
|
||||
self.toc.table.absolutize()
|
||||
#self.assertEquals(self.toc.table.getCDDBDiscId(), 'b910140c')
|
||||
# output from cd-discid:
|
||||
# b910140c 12 24320 44855 64090 77885 88095 104020 118245 129255 141765 164487 181780 209250 4440
|
||||
testCDDBId.skip = 'not implemented yet'
|
||||
|
||||
class CapitalMergeTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.toc1 = toc.TocFile(os.path.join(os.path.dirname(__file__),
|
||||
@@ -220,5 +202,9 @@ class CapitalMergeTestCase(unittest.TestCase):
|
||||
self.assertEquals(self.table.getCDDBDiscId(), 'b910140c')
|
||||
# output from cd-discid:
|
||||
# b910140c 12 24320 44855 64090 77885 88095 104020 118245 129255 141765 164487 181780 209250 4440
|
||||
testCDDBId.skip = 'not implemented yet'
|
||||
|
||||
def testMusicBrainz(self):
|
||||
# URL to submit: http://mm.musicbrainz.org/bare/cdlookup.html?toc=1+11+197850+24320+44855+64090+77885+88095+104020+118245+129255+141765+164487+181780&tracks=11&id=MAj3xXf6QMy7G.BIFOyHyq4MySE-
|
||||
self.assertEquals(self.table.getMusicBrainzDiscId(),
|
||||
"MAj3xXf6QMy7G.BIFOyHyq4MySE-")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user