Revert "Convert docstrings to reStructuredText"
This reverts commit 3b1bd242d0.
This commit is contained in:
2
setup.py
2
setup.py
@@ -13,6 +13,6 @@ setup(
|
|||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
'whipper = whipper.command.main:main'
|
'whipper = whipper.command.main:main'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class BaseCommand():
|
class BaseCommand():
|
||||||
"""A base command class for whipper commands.
|
"""
|
||||||
|
A base command class for whipper commands.
|
||||||
|
|
||||||
Creates an argparse.ArgumentParser.
|
Creates an argparse.ArgumentParser.
|
||||||
Override add_arguments() and handle_arguments() to register
|
Override add_arguments() and handle_arguments() to register
|
||||||
@@ -45,7 +46,6 @@ class BaseCommand():
|
|||||||
arguments, the current options namespace, and the full command path
|
arguments, the current options namespace, and the full command path
|
||||||
name.
|
name.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
device_option = False
|
device_option = False
|
||||||
no_add_help = False # for rip.main.Whipper
|
no_add_help = False # for rip.main.Whipper
|
||||||
formatter_class = argparse.RawDescriptionHelpFormatter
|
formatter_class = argparse.RawDescriptionHelpFormatter
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ class _CD(BaseCommand):
|
|||||||
try:
|
try:
|
||||||
self.program.result.cdparanoiaDefeatsCache = \
|
self.program.result.cdparanoiaDefeatsCache = \
|
||||||
self.config.getDefeatsCache(*info)
|
self.config.getDefeatsCache(*info)
|
||||||
except KeyError as e:
|
except KeyError, e:
|
||||||
logger.debug('Got key error: %r' % (e, ))
|
logger.debug('Got key error: %r' % (e, ))
|
||||||
self.program.result.artist = self.program.metadata \
|
self.program.result.artist = self.program.metadata \
|
||||||
and self.program.metadata.artist \
|
and self.program.metadata.artist \
|
||||||
@@ -408,7 +408,7 @@ Log files will log the path to tracks relative to this directory.
|
|||||||
len(self.itable.tracks),
|
len(self.itable.tracks),
|
||||||
extra))
|
extra))
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception, e:
|
||||||
logger.debug('Got exception %r on try %d',
|
logger.debug('Got exception %r on try %d',
|
||||||
e, tries)
|
e, tries)
|
||||||
|
|
||||||
|
|||||||
@@ -118,9 +118,9 @@ class ResultCache(BaseCommand):
|
|||||||
description = summary
|
description = summary
|
||||||
|
|
||||||
subcommands = {
|
subcommands = {
|
||||||
'cue': RCCue,
|
'cue': RCCue,
|
||||||
'list': RCList,
|
'list': RCList,
|
||||||
'log': RCLog,
|
'log': RCLog,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -291,10 +291,10 @@ class Debug(BaseCommand):
|
|||||||
description = "debug internals"
|
description = "debug internals"
|
||||||
|
|
||||||
subcommands = {
|
subcommands = {
|
||||||
'checksum': Checksum,
|
'checksum': Checksum,
|
||||||
'encode': Encode,
|
'encode': Encode,
|
||||||
'tag': Tag,
|
'tag': Tag,
|
||||||
'musicbrainzngs': MusicBrainzNGS,
|
'musicbrainzngs': MusicBrainzNGS,
|
||||||
'resultcache': ResultCache,
|
'resultcache': ResultCache,
|
||||||
'version': Version,
|
'version': Version,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,20 +38,20 @@ def main():
|
|||||||
try:
|
try:
|
||||||
cmd = Whipper(sys.argv[1:], os.path.basename(sys.argv[0]), None)
|
cmd = Whipper(sys.argv[1:], os.path.basename(sys.argv[0]), None)
|
||||||
ret = cmd.do()
|
ret = cmd.do()
|
||||||
except SystemError as e:
|
except SystemError, e:
|
||||||
sys.stderr.write('whipper: error: %s\n' % e)
|
sys.stderr.write('whipper: error: %s\n' % e)
|
||||||
if (type(e) is common.EjectError and
|
if (type(e) is common.EjectError and
|
||||||
cmd.options.eject in ('failure', 'always')):
|
cmd.options.eject in ('failure', 'always')):
|
||||||
eject_device(e.device)
|
eject_device(e.device)
|
||||||
return 255
|
return 255
|
||||||
except RuntimeError as e:
|
except RuntimeError, e:
|
||||||
print(e)
|
print(e)
|
||||||
return 1
|
return 1
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
return 2
|
return 2
|
||||||
except ImportError as e:
|
except ImportError, e:
|
||||||
raise
|
raise
|
||||||
except task.TaskException as e:
|
except task.TaskException, e:
|
||||||
if isinstance(e.exception, ImportError):
|
if isinstance(e.exception, ImportError):
|
||||||
raise ImportError(e.exception)
|
raise ImportError(e.exception)
|
||||||
elif isinstance(e.exception, common.MissingDependencyException):
|
elif isinstance(e.exception, common.MissingDependencyException):
|
||||||
@@ -80,11 +80,11 @@ You can get help on subcommands by using the -h option to the subcommand.
|
|||||||
no_add_help = True
|
no_add_help = True
|
||||||
subcommands = {
|
subcommands = {
|
||||||
'accurip': accurip.AccuRip,
|
'accurip': accurip.AccuRip,
|
||||||
'cd': cd.CD,
|
'cd': cd.CD,
|
||||||
'debug': debug.Debug,
|
'debug': debug.Debug,
|
||||||
'drive': drive.Drive,
|
'drive': drive.Drive,
|
||||||
'offset': offset.Offset,
|
'offset': offset.Offset,
|
||||||
'image': image.Image
|
'image': image.Image
|
||||||
}
|
}
|
||||||
|
|
||||||
def add_arguments(self):
|
def add_arguments(self):
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ CD in the AccurateRip database."""
|
|||||||
sys.stdout.write('Trying read offset %d ...\n' % offset)
|
sys.stdout.write('Trying read offset %d ...\n' % offset)
|
||||||
try:
|
try:
|
||||||
archecksums = self._arcs(runner, table, 1, offset)
|
archecksums = self._arcs(runner, table, 1, offset)
|
||||||
except task.TaskException as e:
|
except task.TaskException, e:
|
||||||
|
|
||||||
# let MissingDependency fall through
|
# let MissingDependency fall through
|
||||||
if isinstance(e.exception,
|
if isinstance(e.exception,
|
||||||
@@ -152,7 +152,7 @@ CD in the AccurateRip database."""
|
|||||||
for track in range(2, (len(table.tracks) + 1) - 1):
|
for track in range(2, (len(table.tracks) + 1) - 1):
|
||||||
try:
|
try:
|
||||||
archecksums = self._arcs(runner, table, track, offset)
|
archecksums = self._arcs(runner, table, track, offset)
|
||||||
except task.TaskException as e:
|
except task.TaskException, e:
|
||||||
if isinstance(e.exception, cdparanoia.FileSizeError):
|
if isinstance(e.exception, cdparanoia.FileSizeError):
|
||||||
sys.stdout.write(
|
sys.stdout.write(
|
||||||
'WARNING: cannot rip with offset %d...\n' %
|
'WARNING: cannot rip with offset %d...\n' %
|
||||||
@@ -195,11 +195,11 @@ CD in the AccurateRip database."""
|
|||||||
runner.run(t)
|
runner.run(t)
|
||||||
|
|
||||||
v1 = arc.accuraterip_checksum(
|
v1 = arc.accuraterip_checksum(
|
||||||
path, track, len(table.tracks), wave=True, v2=False
|
path, track, len(table.tracks), wave=True, v2=False
|
||||||
)
|
)
|
||||||
v2 = arc.accuraterip_checksum(
|
v2 = arc.accuraterip_checksum(
|
||||||
path, track, len(table.tracks), wave=True, v2=True
|
path, track, len(table.tracks), wave=True, v2=True
|
||||||
)
|
)
|
||||||
|
|
||||||
os.unlink(path)
|
os.unlink(path)
|
||||||
return ("%08x" % v1, "%08x" % v2)
|
return ("%08x" % v1, "%08x" % v2)
|
||||||
|
|||||||
@@ -41,8 +41,7 @@ class EntryNotFound(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class _AccurateRipResponse(object):
|
class _AccurateRipResponse(object):
|
||||||
"""I represent an AccurateRip response with its metadata.
|
"""
|
||||||
|
|
||||||
An AccurateRip response contains a collection of metadata identifying a
|
An AccurateRip response contains a collection of metadata identifying a
|
||||||
particular digital audio compact disc.
|
particular digital audio compact disc.
|
||||||
|
|
||||||
@@ -53,14 +52,14 @@ class _AccurateRipResponse(object):
|
|||||||
the disc index, which excludes any audio hidden in track pre-gaps (such as
|
the disc index, which excludes any audio hidden in track pre-gaps (such as
|
||||||
HTOA).
|
HTOA).
|
||||||
|
|
||||||
The checksums and confidences arrays are indexed by relative track
|
|
||||||
position, so track 1 will have array index 0, track 2 will have array
|
|
||||||
index 1, and so forth. HTOA and other hidden tracks are not included.
|
|
||||||
|
|
||||||
The response is stored as a packed binary structure.
|
The response is stored as a packed binary structure.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, data):
|
def __init__(self, data):
|
||||||
|
"""
|
||||||
|
The checksums and confidences arrays are indexed by relative track
|
||||||
|
position, so track 1 will have array index 0, track 2 will have array
|
||||||
|
index 1, and so forth. HTOA and other hidden tracks are not included.
|
||||||
|
"""
|
||||||
self.num_tracks = struct.unpack("B", data[0])[0]
|
self.num_tracks = struct.unpack("B", data[0])[0]
|
||||||
self.discId1 = "%08x" % struct.unpack("<L", data[1:5])[0]
|
self.discId1 = "%08x" % struct.unpack("<L", data[1:5])[0]
|
||||||
self.discId2 = "%08x" % struct.unpack("<L", data[5:9])[0]
|
self.discId2 = "%08x" % struct.unpack("<L", data[5:9])[0]
|
||||||
@@ -97,17 +96,13 @@ def _split_responses(raw_entry):
|
|||||||
|
|
||||||
|
|
||||||
def calculate_checksums(track_paths):
|
def calculate_checksums(track_paths):
|
||||||
"""Calculate ARv1 and ARv2 checksums of the given tracks.
|
"""
|
||||||
|
|
||||||
Return ARv1 and ARv2 checksums as two arrays of character strings in a
|
Return ARv1 and ARv2 checksums as two arrays of character strings in a
|
||||||
dictionary: {'v1': ['deadbeef', ...], 'v2': [...]}
|
dictionary: {'v1': ['deadbeef', ...], 'v2': [...]}
|
||||||
|
|
||||||
Return None instead of checksum string for unchecksummable tracks.
|
Return None instead of checksum string for unchecksummable tracks.
|
||||||
|
|
||||||
HTOA checksums are not included in the database and are not calculated.
|
HTOA checksums are not included in the database and are not calculated.
|
||||||
|
|
||||||
:param track_paths:
|
|
||||||
:type track_paths:
|
|
||||||
"""
|
"""
|
||||||
track_count = len(track_paths)
|
track_count = len(track_paths)
|
||||||
v1_checksums = []
|
v1_checksums = []
|
||||||
@@ -116,23 +111,23 @@ def calculate_checksums(track_paths):
|
|||||||
# This is done sequentially because it is very fast.
|
# This is done sequentially because it is very fast.
|
||||||
for i, path in enumerate(track_paths):
|
for i, path in enumerate(track_paths):
|
||||||
v1_sum = accuraterip_checksum(
|
v1_sum = accuraterip_checksum(
|
||||||
path, i + 1, track_count, wave=True, v2=False
|
path, i+1, track_count, wave=True, v2=False
|
||||||
)
|
)
|
||||||
if not v1_sum:
|
if not v1_sum:
|
||||||
logger.error(
|
logger.error(
|
||||||
'could not calculate AccurateRip v1 checksum for track %d %r' %
|
'could not calculate AccurateRip v1 checksum for track %d %r' %
|
||||||
(i + 1, path)
|
(i+1, path)
|
||||||
)
|
)
|
||||||
v1_checksums.append(None)
|
v1_checksums.append(None)
|
||||||
else:
|
else:
|
||||||
v1_checksums.append("%08x" % v1_sum)
|
v1_checksums.append("%08x" % v1_sum)
|
||||||
v2_sum = accuraterip_checksum(
|
v2_sum = accuraterip_checksum(
|
||||||
path, i + 1, track_count, wave=True, v2=True
|
path, i+1, track_count, wave=True, v2=True
|
||||||
)
|
)
|
||||||
if not v2_sum:
|
if not v2_sum:
|
||||||
logger.error(
|
logger.error(
|
||||||
'could not calculate AccurateRip v2 checksum for track %d %r' %
|
'could not calculate AccurateRip v2 checksum for track %d %r' %
|
||||||
(i + 1, path)
|
(i+1, path)
|
||||||
)
|
)
|
||||||
v2_checksums.append(None)
|
v2_checksums.append(None)
|
||||||
else:
|
else:
|
||||||
@@ -161,7 +156,7 @@ def _save_entry(raw_entry, path):
|
|||||||
# XXX: os.makedirs(exist_ok=True) in py3
|
# XXX: os.makedirs(exist_ok=True) in py3
|
||||||
try:
|
try:
|
||||||
makedirs(dirname(path))
|
makedirs(dirname(path))
|
||||||
except OSError as e:
|
except OSError, e:
|
||||||
if e.errno != EEXIST:
|
if e.errno != EEXIST:
|
||||||
logger.error('could not save entry to %s: %r' % (path, str(e)))
|
logger.error('could not save entry to %s: %r' % (path, str(e)))
|
||||||
return
|
return
|
||||||
@@ -169,16 +164,11 @@ def _save_entry(raw_entry, path):
|
|||||||
|
|
||||||
|
|
||||||
def get_db_entry(path):
|
def get_db_entry(path):
|
||||||
"""Retrieve cached AccurateRip disc entry.
|
"""
|
||||||
|
Retrieve cached AccurateRip disc entry as array of _AccurateRipResponses.
|
||||||
(As array of _AccurateRipResponses).
|
|
||||||
|
|
||||||
Downloads entry from accuraterip.com on cache fault.
|
Downloads entry from accuraterip.com on cache fault.
|
||||||
|
|
||||||
``path`` is in the format of the output of table.accuraterip_path().
|
`path' is in the format of the output of table.accuraterip_path().
|
||||||
|
|
||||||
:param path:
|
|
||||||
:type path:
|
|
||||||
"""
|
"""
|
||||||
cached_path = join(_CACHE_DIR, path)
|
cached_path = join(_CACHE_DIR, path)
|
||||||
if exists(cached_path):
|
if exists(cached_path):
|
||||||
@@ -205,17 +195,12 @@ def _assign_checksums_and_confidences(tracks, checksums, responses):
|
|||||||
|
|
||||||
|
|
||||||
def _match_responses(tracks, responses):
|
def _match_responses(tracks, responses):
|
||||||
"""Match and save track AccurateRip response checksums.
|
"""
|
||||||
|
Match and save track accuraterip response checksums against
|
||||||
The checksum are matched against all non-hidden tracks.
|
all non-hidden tracks.
|
||||||
|
|
||||||
Returns True if every track has a match for every entry for either
|
Returns True if every track has a match for every entry for either
|
||||||
AccurateRip version.
|
AccurateRip version.
|
||||||
|
|
||||||
:param tracks:
|
|
||||||
:type tracks:
|
|
||||||
:param responses:
|
|
||||||
:type responses:
|
|
||||||
"""
|
"""
|
||||||
for r in responses:
|
for r in responses:
|
||||||
for i, track in enumerate(tracks):
|
for i, track in enumerate(tracks):
|
||||||
@@ -237,16 +222,9 @@ def _match_responses(tracks, responses):
|
|||||||
|
|
||||||
|
|
||||||
def verify_result(result, responses, checksums):
|
def verify_result(result, responses, checksums):
|
||||||
"""Verify track AccurateRip checksums against database responses.
|
"""
|
||||||
|
Verify track AccurateRip checksums against database responses.
|
||||||
Stores track checksums and database values on result.
|
Stores track checksums and database values on result.
|
||||||
|
|
||||||
:param result:
|
|
||||||
:type result:
|
|
||||||
:param responses:
|
|
||||||
:type responses:
|
|
||||||
:param checksums:
|
|
||||||
:type checksums:
|
|
||||||
"""
|
"""
|
||||||
if not (result and responses and checksums):
|
if not (result and responses and checksums):
|
||||||
return False
|
return False
|
||||||
@@ -261,10 +239,8 @@ def verify_result(result, responses, checksums):
|
|||||||
|
|
||||||
|
|
||||||
def print_report(result):
|
def print_report(result):
|
||||||
"""Print AccurateRip verification results to stdout.
|
"""
|
||||||
|
Print AccurateRip verification results to stdout.
|
||||||
:param result:
|
|
||||||
:type result:
|
|
||||||
"""
|
"""
|
||||||
for i, track in enumerate(result.tracks):
|
for i, track in enumerate(result.tracks):
|
||||||
status = 'rip NOT accurate'
|
status = 'rip NOT accurate'
|
||||||
|
|||||||
@@ -32,39 +32,35 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class Persister:
|
class Persister:
|
||||||
"""I wrap an optional pickle to persist an object to disk.
|
"""
|
||||||
|
I wrap an optional pickle to persist an object to disk.
|
||||||
|
|
||||||
Instantiate me with a path to automatically unpickle the object.
|
Instantiate me with a path to automatically unpickle the object.
|
||||||
Call persist to store the object to disk; it will get stored if it
|
Call persist to store the object to disk; it will get stored if it
|
||||||
changed from the on-disk object.
|
changed from the on-disk object.
|
||||||
|
|
||||||
If path is not given, the object will not be persisted.
|
@ivar object: the persistent object
|
||||||
This allows code to transparently deal with both persisted and
|
|
||||||
non-persisted objects, since the persist method will just end up
|
|
||||||
doing nothing.
|
|
||||||
|
|
||||||
:ivar object: the persistent object.
|
|
||||||
:vartype object:
|
|
||||||
:ivar path:
|
|
||||||
:vartype path:
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, path=None, default=None):
|
def __init__(self, path=None, default=None):
|
||||||
|
"""
|
||||||
|
If path is not given, the object will not be persisted.
|
||||||
|
This allows code to transparently deal with both persisted and
|
||||||
|
non-persisted objects, since the persist method will just end up
|
||||||
|
doing nothing.
|
||||||
|
"""
|
||||||
self._path = path
|
self._path = path
|
||||||
self.object = None
|
self.object = None
|
||||||
|
|
||||||
self._unpickle(default)
|
self._unpickle(default)
|
||||||
|
|
||||||
def persist(self, obj=None):
|
def persist(self, obj=None):
|
||||||
"""Persist the given object.
|
"""
|
||||||
|
Persist the given object, if we have a persistence path and the
|
||||||
If we have a persistence path and the object changed.
|
object changed.
|
||||||
|
|
||||||
If object is not given, re-persist our object, always.
|
If object is not given, re-persist our object, always.
|
||||||
If object is given, only persist if it was changed.
|
If object is given, only persist if it was changed.
|
||||||
|
|
||||||
:param obj: (Default value = None).
|
|
||||||
:type obj:
|
|
||||||
"""
|
"""
|
||||||
# don't pickle if it's already ok
|
# don't pickle if it's already ok
|
||||||
if obj and obj == self.object:
|
if obj and obj == self.object:
|
||||||
@@ -121,7 +117,9 @@ class Persister:
|
|||||||
|
|
||||||
|
|
||||||
class PersistedCache:
|
class PersistedCache:
|
||||||
"""I wrap a directory of persisted objects."""
|
"""
|
||||||
|
I wrap a directory of persisted objects.
|
||||||
|
"""
|
||||||
|
|
||||||
path = None
|
path = None
|
||||||
|
|
||||||
@@ -129,7 +127,7 @@ class PersistedCache:
|
|||||||
self.path = path
|
self.path = path
|
||||||
try:
|
try:
|
||||||
os.makedirs(self.path)
|
os.makedirs(self.path)
|
||||||
except OSError as e:
|
except OSError, e:
|
||||||
if e.errno != 17: # FIXME
|
if e.errno != 17: # FIXME
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@@ -137,10 +135,8 @@ class PersistedCache:
|
|||||||
return os.path.join(self.path, '%s.pickle' % key)
|
return os.path.join(self.path, '%s.pickle' % key)
|
||||||
|
|
||||||
def get(self, key):
|
def get(self, key):
|
||||||
"""Return the persister for the given key.
|
"""
|
||||||
|
Returns the persister for the given key.
|
||||||
:param key:
|
|
||||||
:type key:
|
|
||||||
"""
|
"""
|
||||||
persister = Persister(self._getPath(key))
|
persister = Persister(self._getPath(key))
|
||||||
if persister.object:
|
if persister.object:
|
||||||
@@ -164,16 +160,11 @@ class ResultCache:
|
|||||||
self._pcache = PersistedCache(self._path)
|
self._pcache = PersistedCache(self._path)
|
||||||
|
|
||||||
def getRipResult(self, cddbdiscid, create=True):
|
def getRipResult(self, cddbdiscid, create=True):
|
||||||
"""Retrieve the persistable RipResult.
|
"""
|
||||||
|
Retrieve the persistable RipResult either from our cache (from a
|
||||||
|
previous, possibly aborted rip), or return a new one.
|
||||||
|
|
||||||
The RipResult is retrieved either from our cache (from a previous,
|
@rtype: L{Persistable} for L{result.RipResult}
|
||||||
possibly aborted rip), or a new one is returned.
|
|
||||||
|
|
||||||
:param cddbdiscid:
|
|
||||||
:type cddbdiscid:
|
|
||||||
:param create: (Default value = True).
|
|
||||||
:type create:
|
|
||||||
:rtype: L{Persistable} for L{result.RipResult}
|
|
||||||
"""
|
"""
|
||||||
presult = self._pcache.get(cddbdiscid)
|
presult = self._pcache.get(cddbdiscid)
|
||||||
|
|
||||||
@@ -199,7 +190,9 @@ class ResultCache:
|
|||||||
|
|
||||||
|
|
||||||
class TableCache:
|
class TableCache:
|
||||||
"""I read and write entries to and from the cache of tables.
|
|
||||||
|
"""
|
||||||
|
I read and write entries to and from the cache of tables.
|
||||||
|
|
||||||
If no path is specified, the cache will write to the current cache
|
If no path is specified, the cache will write to the current cache
|
||||||
directory and read from all possible cache directories (to allow for
|
directory and read from all possible cache directories (to allow for
|
||||||
|
|||||||
@@ -37,26 +37,28 @@ BYTES_PER_FRAME = SAMPLES_PER_FRAME * 4
|
|||||||
|
|
||||||
|
|
||||||
class EjectError(SystemError):
|
class EjectError(SystemError):
|
||||||
"""Possibly ejects the drive in command.main.
|
"""
|
||||||
|
Possibly ejects the drive in command.main.
|
||||||
ivar args: is a tuple used by BaseException.__str__.
|
|
||||||
:vartype args:
|
|
||||||
ivar: device is the device path to eject.
|
|
||||||
:vartype device:
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, device, *args):
|
def __init__(self, device, *args):
|
||||||
|
"""
|
||||||
|
args is a tuple used by BaseException.__str__
|
||||||
|
device is the device path to eject
|
||||||
|
"""
|
||||||
self.args = args
|
self.args = args
|
||||||
self.device = device
|
self.device = device
|
||||||
|
|
||||||
|
|
||||||
def msfToFrames(msf):
|
def msfToFrames(msf):
|
||||||
"""Convert a string value in MM:SS:FF to frames.
|
"""
|
||||||
|
Converts a string value in MM:SS:FF to frames.
|
||||||
|
|
||||||
:param msf: the MM:SS:FF value to convert.
|
@param msf: the MM:SS:FF value to convert
|
||||||
:type msf: str
|
@type msf: str
|
||||||
:returns: number of frames.
|
|
||||||
:rtype: int
|
@rtype: int
|
||||||
|
@returns: number of frames
|
||||||
"""
|
"""
|
||||||
if ':' not in msf:
|
if ':' not in msf:
|
||||||
return int(msf)
|
return int(msf)
|
||||||
@@ -92,19 +94,22 @@ def framesToHMSF(frames):
|
|||||||
|
|
||||||
|
|
||||||
def formatTime(seconds, fractional=3):
|
def formatTime(seconds, fractional=3):
|
||||||
"""Nicely format time in a human-readable format, like HH:MM:SS.mmm.
|
"""
|
||||||
|
Nicely format time in a human-readable format, like
|
||||||
|
HH:MM:SS.mmm
|
||||||
|
|
||||||
If fractional is zero, no seconds will be shown.
|
If fractional is zero, no seconds will be shown.
|
||||||
If it is greater than 0, we will show seconds and fractions of seconds.
|
If it is greater than 0, we will show seconds and fractions of seconds.
|
||||||
As a side consequence, there is no way to show seconds without fractions.
|
As a side consequence, there is no way to show seconds without fractions.
|
||||||
|
|
||||||
:param seconds: the time in seconds to format.
|
@param seconds: the time in seconds to format.
|
||||||
:type seconds: int or float
|
@type seconds: int or float
|
||||||
:param fractional: how many digits to show for the fractional part of
|
@param fractional: how many digits to show for the fractional part of
|
||||||
seconds. (Default value = 3)
|
seconds.
|
||||||
:type fractional: int
|
@type fractional: int
|
||||||
:returns: a nicely formatted time string.
|
|
||||||
:rtype: str
|
@rtype: string
|
||||||
|
@returns: a nicely formatted time string.
|
||||||
"""
|
"""
|
||||||
chunks = []
|
chunks = []
|
||||||
|
|
||||||
@@ -142,18 +147,16 @@ class EmptyError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class MissingFrames(Exception):
|
class MissingFrames(Exception):
|
||||||
"""Less frames decoded than expected."""
|
"""
|
||||||
|
Less frames decoded than expected.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def shrinkPath(path):
|
def shrinkPath(path):
|
||||||
"""Shrink a full path to a shorter version.
|
"""
|
||||||
|
Shrink a full path to a shorter version.
|
||||||
Used to handle ENAMETOOLONG
|
Used to handle ENAMETOOLONG
|
||||||
|
|
||||||
:param path:
|
|
||||||
:type path:
|
|
||||||
"""
|
"""
|
||||||
parts = list(os.path.split(path))
|
parts = list(os.path.split(path))
|
||||||
length = len(parts[-1])
|
length = len(parts[-1])
|
||||||
@@ -183,16 +186,16 @@ def shrinkPath(path):
|
|||||||
|
|
||||||
|
|
||||||
def getRealPath(refPath, filePath):
|
def getRealPath(refPath, filePath):
|
||||||
"""Translate a .cue or .toc's FILE argument to an existing path.
|
"""
|
||||||
|
Translate a .cue or .toc's FILE argument to an existing path.
|
||||||
Does Windows path translation.
|
Does Windows path translation.
|
||||||
Will look for the given file name, but with .flac and .wav as extensions.
|
Will look for the given file name, but with .flac and .wav as extensions.
|
||||||
|
|
||||||
:param refPath: path to the file from which the track is referenced;
|
@param refPath: path to the file from which the track is referenced;
|
||||||
for example, path to the .cue file in the same directory
|
for example, path to the .cue file in the same directory
|
||||||
:type refPath: unicode
|
@type refPath: unicode
|
||||||
:param filePath:
|
|
||||||
:type filePath: unicode
|
@type filePath: unicode
|
||||||
"""
|
"""
|
||||||
assert type(filePath) is unicode, "%r is not unicode" % filePath
|
assert type(filePath) is unicode, "%r is not unicode" % filePath
|
||||||
|
|
||||||
@@ -238,14 +241,11 @@ def getRealPath(refPath, filePath):
|
|||||||
|
|
||||||
|
|
||||||
def getRelativePath(targetPath, collectionPath):
|
def getRelativePath(targetPath, collectionPath):
|
||||||
"""Get a relative path from the directory of collectionPath to targetPath.
|
"""
|
||||||
|
Get a relative path from the directory of collectionPath to
|
||||||
|
targetPath.
|
||||||
|
|
||||||
Used to determine the path to use in .cue/.m3u files
|
Used to determine the path to use in .cue/.m3u files
|
||||||
|
|
||||||
:param targetPath:
|
|
||||||
:type targetPath:
|
|
||||||
:param collectionPath:
|
|
||||||
:type collectionPath:
|
|
||||||
"""
|
"""
|
||||||
logger.debug('getRelativePath: target %r, collection %r' % (
|
logger.debug('getRelativePath: target %r, collection %r' % (
|
||||||
targetPath, collectionPath))
|
targetPath, collectionPath))
|
||||||
@@ -266,22 +266,21 @@ def getRelativePath(targetPath, collectionPath):
|
|||||||
|
|
||||||
|
|
||||||
class VersionGetter(object):
|
class VersionGetter(object):
|
||||||
"""I get the version of a program by looking for it in command output.
|
"""
|
||||||
|
I get the version of a program by looking for it in command output
|
||||||
(Through a RegExp).
|
according to a regexp.
|
||||||
|
|
||||||
:ivar dependency: name of the dependency providing the program
|
|
||||||
:vartype dependency:
|
|
||||||
:ivar args: the arguments to invoke to show the version
|
|
||||||
:vartype args: list of str
|
|
||||||
:ivar regexp: the regular expression to get the version
|
|
||||||
:vartype regexp:
|
|
||||||
:ivar expander: the expansion string for the version using the
|
|
||||||
regexp group dict
|
|
||||||
:vartype expander:
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, dependency, args, regexp, expander):
|
def __init__(self, dependency, args, regexp, expander):
|
||||||
|
"""
|
||||||
|
@param dependency: name of the dependency providing the program
|
||||||
|
@param args: the arguments to invoke to show the version
|
||||||
|
@type args: list of str
|
||||||
|
@param regexp: the regular expression to get the version
|
||||||
|
@param expander: the expansion string for the version using the
|
||||||
|
regexp group dict
|
||||||
|
"""
|
||||||
|
|
||||||
self._dep = dependency
|
self._dep = dependency
|
||||||
self._args = args
|
self._args = args
|
||||||
self._regexp = regexp
|
self._regexp = regexp
|
||||||
@@ -299,7 +298,7 @@ class VersionGetter(object):
|
|||||||
vre = self._regexp.search(output)
|
vre = self._regexp.search(output)
|
||||||
if vre:
|
if vre:
|
||||||
version = self._expander % vre.groupdict()
|
version = self._expander % vre.groupdict()
|
||||||
except OSError as e:
|
except OSError, e:
|
||||||
import errno
|
import errno
|
||||||
if e.errno == errno.ENOENT:
|
if e.errno == errno.ENOENT:
|
||||||
raise MissingDependencyException(self._dep)
|
raise MissingDependencyException(self._dep)
|
||||||
|
|||||||
@@ -85,27 +85,18 @@ class Config:
|
|||||||
# drive sections
|
# drive sections
|
||||||
|
|
||||||
def setReadOffset(self, vendor, model, release, offset):
|
def setReadOffset(self, vendor, model, release, offset):
|
||||||
"""Set a read offset for the given drive.
|
"""
|
||||||
|
Set a read offset for the given drive.
|
||||||
|
|
||||||
Strips the given strings of leading and trailing whitespace.
|
Strips the given strings of leading and trailing whitespace.
|
||||||
|
|
||||||
:param vendor:
|
|
||||||
:param model:
|
|
||||||
:param release:
|
|
||||||
:param offset:
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
section = self._findOrCreateDriveSection(vendor, model, release)
|
section = self._findOrCreateDriveSection(vendor, model, release)
|
||||||
self._parser.set(section, 'read_offset', str(offset))
|
self._parser.set(section, 'read_offset', str(offset))
|
||||||
self.write()
|
self.write()
|
||||||
|
|
||||||
def getReadOffset(self, vendor, model, release):
|
def getReadOffset(self, vendor, model, release):
|
||||||
"""Get a read offset for the given drive.
|
"""
|
||||||
|
Get a read offset for the given drive.
|
||||||
:param vendor:
|
|
||||||
:param model:
|
|
||||||
:param release:
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
section = self._findDriveSection(vendor, model, release)
|
section = self._findDriveSection(vendor, model, release)
|
||||||
|
|
||||||
@@ -116,15 +107,10 @@ class Config:
|
|||||||
vendor, model, release))
|
vendor, model, release))
|
||||||
|
|
||||||
def setDefeatsCache(self, vendor, model, release, defeat):
|
def setDefeatsCache(self, vendor, model, release, defeat):
|
||||||
"""Set whether the drive defeats the cache.
|
"""
|
||||||
|
Set whether the drive defeats the cache.
|
||||||
|
|
||||||
Strips the given strings of leading and trailing whitespace.
|
Strips the given strings of leading and trailing whitespace.
|
||||||
|
|
||||||
:param vendor:
|
|
||||||
:param model:
|
|
||||||
:param release:
|
|
||||||
:param defeat:
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
section = self._findOrCreateDriveSection(vendor, model, release)
|
section = self._findOrCreateDriveSection(vendor, model, release)
|
||||||
self._parser.set(section, 'defeats_cache', str(defeat))
|
self._parser.set(section, 'defeats_cache', str(defeat))
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with whipper. If not, see <http://www.gnu.org/licenses/>.
|
# along with whipper. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""Handles communication with the MusicBrainz server using NGS."""
|
"""
|
||||||
|
Handles communication with the MusicBrainz server using NGS.
|
||||||
|
"""
|
||||||
|
|
||||||
import urllib2
|
import urllib2
|
||||||
|
|
||||||
@@ -52,38 +54,15 @@ class TrackMetadata(object):
|
|||||||
|
|
||||||
|
|
||||||
class DiscMetadata(object):
|
class DiscMetadata(object):
|
||||||
"""I represent all relevant disc metadata information found in MBz.
|
|
||||||
|
|
||||||
:cvar artist: artist(s) name.
|
|
||||||
:vartype artist:
|
|
||||||
:cvar sortName: album artist sort name.
|
|
||||||
:vartype sortName:
|
|
||||||
:cvar title: title of the disc (with disambiguation).
|
|
||||||
:vartype title:
|
|
||||||
:cvar various:
|
|
||||||
:vartype various:
|
|
||||||
:cvar tracks:
|
|
||||||
:vartype tracks:
|
|
||||||
:cvar release: earliest release date, in YYYY-MM-DD.
|
|
||||||
:vartype release: unicode
|
|
||||||
:cvar releaseTitle: title of the release (without disambiguation).
|
|
||||||
:vartype releaseTitle:
|
|
||||||
:cvar releaseType:
|
|
||||||
:vartype releaseType:
|
|
||||||
:cvar mbid:
|
|
||||||
:vartype mbid:
|
|
||||||
:cvar mbidArtist:
|
|
||||||
:vartype mbidArtist:
|
|
||||||
:cvar url:
|
|
||||||
:vartype url:
|
|
||||||
:cvar catalogNumber:
|
|
||||||
:vartype catalogNumber:
|
|
||||||
:cvar barcode:
|
|
||||||
:vartype barcode:
|
|
||||||
:ivar tracks:
|
|
||||||
:vartype tracks:
|
|
||||||
"""
|
"""
|
||||||
|
@param artist: artist(s) name
|
||||||
|
@param sortName: album artist sort name
|
||||||
|
@param release: earliest release date, in YYYY-MM-DD
|
||||||
|
@type release: unicode
|
||||||
|
@param title: title of the disc (with disambiguation)
|
||||||
|
@param releaseTitle: title of the release (without disambiguation)
|
||||||
|
@type tracks: C{list} of L{TrackMetadata}
|
||||||
|
"""
|
||||||
artist = None
|
artist = None
|
||||||
sortName = None
|
sortName = None
|
||||||
title = None
|
title = None
|
||||||
@@ -133,7 +112,10 @@ def _record(record, which, name, what):
|
|||||||
|
|
||||||
|
|
||||||
class _Credit(list):
|
class _Credit(list):
|
||||||
"""Representation of an artist-credit in MusicBrainz for a disc/track."""
|
"""
|
||||||
|
I am a representation of an artist-credit in MusicBrainz for a disc
|
||||||
|
or track.
|
||||||
|
"""
|
||||||
|
|
||||||
def joiner(self, attributeGetter, joinString=None):
|
def joiner(self, attributeGetter, joinString=None):
|
||||||
res = []
|
res = []
|
||||||
@@ -162,19 +144,12 @@ class _Credit(list):
|
|||||||
|
|
||||||
|
|
||||||
def _getMetadata(releaseShort, release, discid, country=None):
|
def _getMetadata(releaseShort, release, discid, country=None):
|
||||||
"""Get disc's metadata using MusicBrainz.
|
"""
|
||||||
|
@type release: C{dict}
|
||||||
|
@param release: a release dict as returned in the value for key release
|
||||||
|
from get_release_by_id
|
||||||
|
|
||||||
:param releaseShort:
|
@rtype: L{DiscMetadata} or None
|
||||||
:type releaseShort:
|
|
||||||
:param release: a release dict as returned in the value for key release
|
|
||||||
from get_release_by_id.
|
|
||||||
:type release: C{dict}
|
|
||||||
:param discid:
|
|
||||||
:type discid:
|
|
||||||
:param country: (Default value = None).
|
|
||||||
:type country:
|
|
||||||
:returns:
|
|
||||||
:rtype: L{DiscMetadata} or None
|
|
||||||
"""
|
"""
|
||||||
logger.debug('getMetadata for release id %r',
|
logger.debug('getMetadata for release id %r',
|
||||||
release['id'])
|
release['id'])
|
||||||
@@ -281,18 +256,15 @@ def _getMetadata(releaseShort, release, discid, country=None):
|
|||||||
|
|
||||||
|
|
||||||
def musicbrainz(discid, country=None, record=False):
|
def musicbrainz(discid, country=None, record=False):
|
||||||
"""Get a list of DiscMetadata objects for the given MusicBrainz disc id.
|
"""
|
||||||
|
Based on a MusicBrainz disc id, get a list of DiscMetadata objects
|
||||||
|
for the given disc id.
|
||||||
|
|
||||||
Example disc id: Mj48G109whzEmAbPBoGvd4KyCS4-
|
Example disc id: Mj48G109whzEmAbPBoGvd4KyCS4-
|
||||||
|
|
||||||
:param discid:
|
@type discid: str
|
||||||
:type discid: str
|
|
||||||
:param country: (Default value = None).
|
@rtype: list of L{DiscMetadata}
|
||||||
:type country:
|
|
||||||
:param record: (Default value = False).
|
|
||||||
:type record:
|
|
||||||
:returns:
|
|
||||||
:rtype: list of L{DiscMetadata}
|
|
||||||
"""
|
"""
|
||||||
logger.debug('looking up results for discid %r', discid)
|
logger.debug('looking up results for discid %r', discid)
|
||||||
import musicbrainzngs
|
import musicbrainzngs
|
||||||
@@ -302,7 +274,7 @@ def musicbrainz(discid, country=None, record=False):
|
|||||||
try:
|
try:
|
||||||
result = musicbrainzngs.get_releases_by_discid(
|
result = musicbrainzngs.get_releases_by_discid(
|
||||||
discid, includes=["artists", "recordings", "release-groups"])
|
discid, includes=["artists", "recordings", "release-groups"])
|
||||||
except musicbrainzngs.ResponseError as e:
|
except musicbrainzngs.ResponseError, e:
|
||||||
if isinstance(e.cause, urllib2.HTTPError):
|
if isinstance(e.cause, urllib2.HTTPError):
|
||||||
if e.cause.code == 404:
|
if e.cause.code == 404:
|
||||||
raise NotFoundException(e)
|
raise NotFoundException(e)
|
||||||
|
|||||||
@@ -22,19 +22,17 @@ import re
|
|||||||
|
|
||||||
|
|
||||||
class PathFilter(object):
|
class PathFilter(object):
|
||||||
"""I filter path components for safe storage on file systems.
|
"""
|
||||||
|
I filter path components for safe storage on file systems.
|
||||||
:ivar slashes: whether to convert slashes to dashes.
|
|
||||||
:vartype slashes:
|
|
||||||
:ivar quotes: whether to normalize quotes.
|
|
||||||
:vartype quotes:
|
|
||||||
:ivar fat: whether to strip characters illegal on FAT filesystems.
|
|
||||||
:vartype fat:
|
|
||||||
:ivar special: whether to strip special characters.
|
|
||||||
:vartype special:
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, slashes=True, quotes=True, fat=True, special=False):
|
def __init__(self, slashes=True, quotes=True, fat=True, special=False):
|
||||||
|
"""
|
||||||
|
@param slashes: whether to convert slashes to dashes
|
||||||
|
@param quotes: whether to normalize quotes
|
||||||
|
@param fat: whether to strip characters illegal on FAT filesystems
|
||||||
|
@param special: whether to strip special characters
|
||||||
|
"""
|
||||||
self._slashes = slashes
|
self._slashes = slashes
|
||||||
self._quotes = quotes
|
self._quotes = quotes
|
||||||
self._fat = fat
|
self._fat = fat
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with whipper. If not, see <http://www.gnu.org/licenses/>.
|
# along with whipper. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""Common functionality and class for all programs using whipper."""
|
"""
|
||||||
|
Common functionality and class for all programs using whipper.
|
||||||
|
"""
|
||||||
|
|
||||||
import musicbrainzngs
|
import musicbrainzngs
|
||||||
import re
|
import re
|
||||||
@@ -39,26 +41,15 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class Program:
|
class Program:
|
||||||
"""I maintain program state and functionality.
|
"""
|
||||||
|
I maintain program state and functionality.
|
||||||
|
|
||||||
:cvar cuePath:
|
@ivar metadata:
|
||||||
:vartype cuePath:
|
@type metadata: L{mbngs.DiscMetadata}
|
||||||
:cvar logPath:
|
@ivar result: the rip's result
|
||||||
:vartype logPath:
|
@type result: L{result.RipResult}
|
||||||
:cvar metadata:
|
@type outdir: unicode
|
||||||
:vartype metadata: L{mbngs.DiscMetadata}
|
@type config: L{whipper.common.config.Config}
|
||||||
:cvar outdir:
|
|
||||||
:vartype outdir: unicode
|
|
||||||
:cvar result: the rip's result
|
|
||||||
:vartype result: L{result.RipResult}
|
|
||||||
:ivar config:
|
|
||||||
:vartype config:
|
|
||||||
:ivar record: whether to record results of API calls for playback.
|
|
||||||
:vartype record:
|
|
||||||
:param stdout: (Default value = sys.stdout).
|
|
||||||
:type stdout:
|
|
||||||
:ivar cache:
|
|
||||||
:vartype cache:
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
cuePath = None
|
cuePath = None
|
||||||
@@ -70,6 +61,9 @@ class Program:
|
|||||||
_stdout = None
|
_stdout = None
|
||||||
|
|
||||||
def __init__(self, config, record=False, stdout=sys.stdout):
|
def __init__(self, config, record=False, stdout=sys.stdout):
|
||||||
|
"""
|
||||||
|
@param record: whether to record results of API calls for playback.
|
||||||
|
"""
|
||||||
self._record = record
|
self._record = record
|
||||||
self._cache = cache.ResultCache()
|
self._cache = cache.ResultCache()
|
||||||
self._stdout = stdout
|
self._stdout = stdout
|
||||||
@@ -97,7 +91,6 @@ class Program:
|
|||||||
|
|
||||||
def getFastToc(self, runner, device):
|
def getFastToc(self, runner, device):
|
||||||
"""Retrieve the normal TOC table from a toc pickle or the drive.
|
"""Retrieve the normal TOC table from a toc pickle or the drive.
|
||||||
|
|
||||||
Also retrieves the cdrdao version
|
Also retrieves the cdrdao version
|
||||||
"""
|
"""
|
||||||
def function(r, t):
|
def function(r, t):
|
||||||
@@ -118,20 +111,10 @@ class Program:
|
|||||||
return toc
|
return toc
|
||||||
|
|
||||||
def getTable(self, runner, cddbdiscid, mbdiscid, device, offset):
|
def getTable(self, runner, cddbdiscid, mbdiscid, device, offset):
|
||||||
"""Retrieve the Table either from the cache or the drive.
|
"""
|
||||||
|
Retrieve the Table either from the cache or the drive.
|
||||||
|
|
||||||
:param runner:
|
@rtype: L{table.Table}
|
||||||
:type runner:
|
|
||||||
:param cddbdiscid:
|
|
||||||
:type cddbdiscid:
|
|
||||||
:param mbdiscid:
|
|
||||||
:type mbdiscid:
|
|
||||||
:param device:
|
|
||||||
:type device:
|
|
||||||
:param offset:
|
|
||||||
:type offset:
|
|
||||||
:returns:
|
|
||||||
:rtype: L{table.Table}
|
|
||||||
"""
|
"""
|
||||||
tcache = cache.TableCache()
|
tcache = cache.TableCache()
|
||||||
ptable = tcache.get(cddbdiscid, mbdiscid)
|
ptable = tcache.get(cddbdiscid, mbdiscid)
|
||||||
@@ -168,15 +151,11 @@ class Program:
|
|||||||
return itable
|
return itable
|
||||||
|
|
||||||
def getRipResult(self, cddbdiscid):
|
def getRipResult(self, cddbdiscid):
|
||||||
"""Retrieve the persistable RipResult.
|
"""
|
||||||
|
Retrieve the persistable RipResult either from our cache (from a
|
||||||
|
previous, possibly aborted rip), or return a new one.
|
||||||
|
|
||||||
RipResult is either retrieved from our cache (from a previous,
|
@rtype: L{result.RipResult}
|
||||||
possibly aborted rip), or return a new one.
|
|
||||||
|
|
||||||
:param cddbdiscid:
|
|
||||||
:type cddbdiscid:
|
|
||||||
:returns:
|
|
||||||
:rtype: L{result.RipResult}
|
|
||||||
"""
|
"""
|
||||||
assert self.result is None
|
assert self.result is None
|
||||||
|
|
||||||
@@ -189,7 +168,7 @@ class Program:
|
|||||||
self._presult.persist()
|
self._presult.persist()
|
||||||
|
|
||||||
def addDisambiguation(self, template_part, metadata):
|
def addDisambiguation(self, template_part, metadata):
|
||||||
"""Add disambiguation to template path part string."""
|
"Add disambiguation to template path part string."
|
||||||
if metadata.catalogNumber:
|
if metadata.catalogNumber:
|
||||||
template_part += ' (%s)' % metadata.catalogNumber
|
template_part += ' (%s)' % metadata.catalogNumber
|
||||||
elif metadata.barcode:
|
elif metadata.barcode:
|
||||||
@@ -197,40 +176,29 @@ class Program:
|
|||||||
return template_part
|
return template_part
|
||||||
|
|
||||||
def getPath(self, outdir, template, mbdiscid, metadata, track_number=None):
|
def getPath(self, outdir, template, mbdiscid, metadata, track_number=None):
|
||||||
"""Return disc or track path relative to outdir according to template.
|
"""
|
||||||
|
Return disc or track path relative to outdir according to
|
||||||
Track paths do not include extension.
|
template. Track paths do not include extension.
|
||||||
|
|
||||||
Tracks are named according to the track template, filling in
|
Tracks are named according to the track template, filling in
|
||||||
the variables and adding the file extension. Variables
|
the variables and adding the file extension. Variables
|
||||||
exclusive to the track template are:
|
exclusive to the track template are:
|
||||||
* %t: track number.
|
- %t: track number
|
||||||
* %a: track artist.
|
- %a: track artist
|
||||||
* %n: track title.
|
- %n: track title
|
||||||
* %s: track sort name.
|
- %s: track sort name
|
||||||
|
|
||||||
Disc files (.cue, .log, .m3u) are named according to the disc
|
Disc files (.cue, .log, .m3u) are named according to the disc
|
||||||
template, filling in the variables and adding the file
|
template, filling in the variables and adding the file
|
||||||
extension. Variables for both disc and track template are:
|
extension. Variables for both disc and track template are:
|
||||||
* %A: album artist.
|
- %A: album artist
|
||||||
* %S: album sort name.
|
- %S: album sort name
|
||||||
* %d: disc title.
|
- %d: disc title
|
||||||
* %y: release year.
|
- %y: release year
|
||||||
* %r: release type, lowercase.
|
- %r: release type, lowercase
|
||||||
* %R: Release type, normal case.
|
- %R: Release type, normal case
|
||||||
* %x: audio extension, lowercase.
|
- %x: audio extension, lowercase
|
||||||
* %X: audio extension, uppercase.
|
- %X: audio extension, uppercase
|
||||||
|
|
||||||
:param outdir:
|
|
||||||
:type outdir:
|
|
||||||
:param template:
|
|
||||||
:type template:
|
|
||||||
:param mbdiscid:
|
|
||||||
:type mbdiscid:
|
|
||||||
:param metadata:
|
|
||||||
:type metadata:
|
|
||||||
:param track_number: (Default value = None)
|
|
||||||
:type track_number:
|
|
||||||
"""
|
"""
|
||||||
assert type(outdir) is unicode, "%r is not unicode" % outdir
|
assert type(outdir) is unicode, "%r is not unicode" % outdir
|
||||||
assert type(template) is unicode, "%r is not unicode" % template
|
assert type(template) is unicode, "%r is not unicode" % template
|
||||||
@@ -278,12 +246,10 @@ class Program:
|
|||||||
return os.path.join(outdir, template % v)
|
return os.path.join(outdir, template % v)
|
||||||
|
|
||||||
def getCDDB(self, cddbdiscid):
|
def getCDDB(self, cddbdiscid):
|
||||||
"""Query CDDB using the given cddbdiscid to find the disc title.
|
"""
|
||||||
|
@param cddbdiscid: list of id, tracks, offsets, seconds
|
||||||
|
|
||||||
:param cddbdiscid: list of id, tracks, offsets, seconds.
|
@rtype: str
|
||||||
:type cddbdiscid:
|
|
||||||
:returns:
|
|
||||||
:rtype: str
|
|
||||||
"""
|
"""
|
||||||
# FIXME: convert to nonblocking?
|
# FIXME: convert to nonblocking?
|
||||||
import CDDB
|
import CDDB
|
||||||
@@ -293,7 +259,7 @@ class Program:
|
|||||||
if code == 200:
|
if code == 200:
|
||||||
return md['title']
|
return md['title']
|
||||||
|
|
||||||
except IOError as e:
|
except IOError, e:
|
||||||
# FIXME: for some reason errno is a str ?
|
# FIXME: for some reason errno is a str ?
|
||||||
if e.errno == 'socket error':
|
if e.errno == 'socket error':
|
||||||
self._stdout.write("Warning: network error: %r\n" % (e, ))
|
self._stdout.write("Warning: network error: %r\n" % (e, ))
|
||||||
@@ -304,18 +270,8 @@ class Program:
|
|||||||
|
|
||||||
def getMusicBrainz(self, ittoc, mbdiscid, release=None, country=None,
|
def getMusicBrainz(self, ittoc, mbdiscid, release=None, country=None,
|
||||||
prompt=False):
|
prompt=False):
|
||||||
"""Look up disc on MusicBrainz and get the relevant metadata.
|
"""
|
||||||
|
@type ittoc: L{whipper.image.table.Table}
|
||||||
:param ittoc:
|
|
||||||
:type ittoc: L{whipper.image.table.Table}
|
|
||||||
:param mbdiscid:
|
|
||||||
:type mbdiscid:
|
|
||||||
:param release: (Default value = None)
|
|
||||||
:type release:
|
|
||||||
:param country: (Default value = None)
|
|
||||||
:type country:
|
|
||||||
:param prompt: (Default value = False)
|
|
||||||
:type prompt:
|
|
||||||
"""
|
"""
|
||||||
# look up disc on MusicBrainz
|
# look up disc on MusicBrainz
|
||||||
self._stdout.write('Disc duration: %s, %d audio tracks\n' % (
|
self._stdout.write('Disc duration: %s, %d audio tracks\n' % (
|
||||||
@@ -334,13 +290,13 @@ class Program:
|
|||||||
country=country,
|
country=country,
|
||||||
record=self._record)
|
record=self._record)
|
||||||
break
|
break
|
||||||
except mbngs.NotFoundException as e:
|
except mbngs.NotFoundException, e:
|
||||||
logger.warning("release not found: %r" % (e, ))
|
logger.warning("release not found: %r" % (e, ))
|
||||||
break
|
break
|
||||||
except musicbrainzngs.NetworkError as e:
|
except musicbrainzngs.NetworkError, e:
|
||||||
logger.warning("network error: %r" % (e, ))
|
logger.warning("network error: %r" % (e, ))
|
||||||
break
|
break
|
||||||
except mbngs.MusicBrainzException as e:
|
except mbngs.MusicBrainzException, e:
|
||||||
logger.warning("musicbrainz exception: %r" % (e, ))
|
logger.warning("musicbrainz exception: %r" % (e, ))
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
continue
|
continue
|
||||||
@@ -447,12 +403,13 @@ class Program:
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
def getTagList(self, number):
|
def getTagList(self, number):
|
||||||
"""Based on the metadata, get a dict of tags for the given track.
|
"""
|
||||||
|
Based on the metadata, get a dict of tags for the given track.
|
||||||
|
|
||||||
:param number: track number (0 for HTOA).
|
@param number: track number (0 for HTOA)
|
||||||
:type number: int
|
@type number: int
|
||||||
:returns:
|
|
||||||
:rtype: dict
|
@rtype: dict
|
||||||
"""
|
"""
|
||||||
trackArtist = u'Unknown Artist'
|
trackArtist = u'Unknown Artist'
|
||||||
albumArtist = u'Unknown Artist'
|
albumArtist = u'Unknown Artist'
|
||||||
@@ -474,7 +431,7 @@ class Program:
|
|||||||
title = track.title
|
title = track.title
|
||||||
mbidTrack = track.mbid
|
mbidTrack = track.mbid
|
||||||
mbidTrackArtist = track.mbidArtist
|
mbidTrackArtist = track.mbidArtist
|
||||||
except IndexError as e:
|
except IndexError, e:
|
||||||
print 'ERROR: no track %d found, %r' % (number, e)
|
print 'ERROR: no track %d found, %r' % (number, e)
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
@@ -507,9 +464,10 @@ class Program:
|
|||||||
return tags
|
return tags
|
||||||
|
|
||||||
def getHTOA(self):
|
def getHTOA(self):
|
||||||
"""Check if we have hidden track one audio.
|
"""
|
||||||
|
Check if we have hidden track one audio.
|
||||||
|
|
||||||
:returns: tuple of (start, stop), or None.
|
@returns: tuple of (start, stop), or None
|
||||||
"""
|
"""
|
||||||
track = self.result.table.tracks[0]
|
track = self.result.table.tracks[0]
|
||||||
try:
|
try:
|
||||||
@@ -527,7 +485,7 @@ class Program:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
runner.run(t)
|
runner.run(t)
|
||||||
except task.TaskException as e:
|
except task.TaskException, e:
|
||||||
if isinstance(e.exception, common.MissingFrames):
|
if isinstance(e.exception, common.MissingFrames):
|
||||||
logger.warning('missing frames for %r' % trackResult.filename)
|
logger.warning('missing frames for %r' % trackResult.filename)
|
||||||
return False
|
return False
|
||||||
@@ -542,25 +500,12 @@ class Program:
|
|||||||
|
|
||||||
def ripTrack(self, runner, trackResult, offset, device, taglist,
|
def ripTrack(self, runner, trackResult, offset, device, taglist,
|
||||||
overread, what=None):
|
overread, what=None):
|
||||||
""".
|
"""
|
||||||
|
|
||||||
Ripping the track may change the track's filename as stored in
|
Ripping the track may change the track's filename as stored in
|
||||||
trackResult.
|
trackResult.
|
||||||
|
|
||||||
:param trackResult: the object to store information in.
|
@param trackResult: the object to store information in.
|
||||||
:type trackResult: L{result.TrackResult}
|
@type trackResult: L{result.TrackResult}
|
||||||
:param runner:
|
|
||||||
:type runner:
|
|
||||||
:param offset:
|
|
||||||
:type offset:
|
|
||||||
:param device:
|
|
||||||
:type device:
|
|
||||||
:param taglist:
|
|
||||||
:type taglist:
|
|
||||||
:param overread:
|
|
||||||
:type overread:
|
|
||||||
:param what: (Default value = None)
|
|
||||||
:type what:
|
|
||||||
"""
|
"""
|
||||||
if trackResult.number == 0:
|
if trackResult.number == 0:
|
||||||
start, stop = self.getHTOA()
|
start, stop = self.getHTOA()
|
||||||
@@ -610,19 +555,14 @@ class Program:
|
|||||||
runner.run(t)
|
runner.run(t)
|
||||||
|
|
||||||
def verifyImage(self, runner, table):
|
def verifyImage(self, runner, table):
|
||||||
"""Verify table against accuraterip and cue_path track lengths.
|
"""
|
||||||
|
verify table against accuraterip and cue_path track lengths
|
||||||
Verify our image against the given AccurateRip responses.
|
Verify our image against the given AccurateRip responses.
|
||||||
|
|
||||||
Needs an initialized self.result.
|
Needs an initialized self.result.
|
||||||
Will set accurip and friends on each TrackResult.
|
Will set accurip and friends on each TrackResult.
|
||||||
|
|
||||||
Populates self.result.tracks with above TrackResults.
|
Populates self.result.tracks with above TrackResults.
|
||||||
|
|
||||||
:param runner:
|
|
||||||
:type runner:
|
|
||||||
:param table:
|
|
||||||
:type table:
|
|
||||||
"""
|
"""
|
||||||
cueImage = image.Image(self.cuePath)
|
cueImage = image.Image(self.cuePath)
|
||||||
# assigns track lengths
|
# assigns track lengths
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
"""Rename files on file system and inside metafiles in a resumable way."""
|
"""
|
||||||
|
Rename files on file system and inside metafiles in a resumable way.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Operator(object):
|
class Operator(object):
|
||||||
@@ -34,16 +36,14 @@ class Operator(object):
|
|||||||
self._resuming = False
|
self._resuming = False
|
||||||
|
|
||||||
def addOperation(self, operation):
|
def addOperation(self, operation):
|
||||||
"""Add an operation.
|
"""
|
||||||
|
Add an operation.
|
||||||
:param operation:
|
|
||||||
:type operation:
|
|
||||||
"""
|
"""
|
||||||
self._todo.append(operation)
|
self._todo.append(operation)
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
"""Load state from the given state path using the given key.
|
"""
|
||||||
|
Load state from the given state path using the given key.
|
||||||
Verifies the state.
|
Verifies the state.
|
||||||
"""
|
"""
|
||||||
todo = os.path.join(self._statePath, self._key + '.todo')
|
todo = os.path.join(self._statePath, self._key + '.todo')
|
||||||
@@ -68,7 +68,9 @@ class Operator(object):
|
|||||||
self._resuming = True
|
self._resuming = True
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""Save the state to the given state path using the given key."""
|
"""
|
||||||
|
Saves the state to the given state path using the given key.
|
||||||
|
"""
|
||||||
# only save todo first time
|
# only save todo first time
|
||||||
todo = os.path.join(self._statePath, self._key + '.todo')
|
todo = os.path.join(self._statePath, self._key + '.todo')
|
||||||
if not os.path.exists(todo):
|
if not os.path.exists(todo):
|
||||||
@@ -87,7 +89,9 @@ class Operator(object):
|
|||||||
handle.write('%s %s\n' % (name, data))
|
handle.write('%s %s\n' % (name, data))
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
"""Execute the operations."""
|
"""
|
||||||
|
Execute the operations
|
||||||
|
"""
|
||||||
|
|
||||||
def next(self):
|
def next(self):
|
||||||
operation = self._todo[len(self._done)]
|
operation = self._todo[len(self._done)]
|
||||||
@@ -104,50 +108,52 @@ class Operator(object):
|
|||||||
class FileRenamer(Operator):
|
class FileRenamer(Operator):
|
||||||
|
|
||||||
def addRename(self, source, destination):
|
def addRename(self, source, destination):
|
||||||
"""Add a rename operation.
|
"""
|
||||||
|
Add a rename operation.
|
||||||
|
|
||||||
:param source: source filename.
|
@param source: source filename
|
||||||
:type source: str
|
@type source: str
|
||||||
:param destination: destination filename.
|
@param destination: destination filename
|
||||||
:type destination: str
|
@type destination: str
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Operation(object):
|
class Operation(object):
|
||||||
|
|
||||||
def verify(self):
|
def verify(self):
|
||||||
"""Check if the operation will succeed in the current conditions.
|
"""
|
||||||
|
Check if the operation will succeed in the current conditions.
|
||||||
Consider this a pre-flight check.
|
Consider this a pre-flight check.
|
||||||
|
|
||||||
Does not eliminate the need to handle errors as they happen.
|
Does not eliminate the need to handle errors as they happen.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def do(self):
|
def do(self):
|
||||||
"""Perform the operation."""
|
"""
|
||||||
|
Perform the operation.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def redo(self):
|
def redo(self):
|
||||||
"""Perform the operation.
|
"""
|
||||||
|
Perform the operation, without knowing if it already has been
|
||||||
Without knowing if it already has been (partly) performed.
|
(partly) performed.
|
||||||
"""
|
"""
|
||||||
self.do()
|
self.do()
|
||||||
|
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
"""Serialize the operation.
|
"""
|
||||||
|
Serialize the operation.
|
||||||
|
The return value should bu usable with L{deserialize}
|
||||||
|
|
||||||
The return value should be usable with L{deserialize}.
|
@rtype: str
|
||||||
|
|
||||||
:returns:
|
|
||||||
:rtype: str
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def deserialize(cls, data):
|
def deserialize(cls, data):
|
||||||
"""Deserialize the operation with the given operation data.
|
"""
|
||||||
|
Deserialize the operation with the given operation data.
|
||||||
|
|
||||||
:param data:
|
@type data: str
|
||||||
:type data: str
|
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
deserialize = classmethod(deserialize)
|
deserialize = classmethod(deserialize)
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ class LoggableMultiSeparateTask(task.MultiSeparateTask):
|
|||||||
|
|
||||||
|
|
||||||
class PopenTask(task.Task):
|
class PopenTask(task.Task):
|
||||||
"""I am a task that runs a command using Popen."""
|
"""
|
||||||
|
I am a task that runs a command using Popen.
|
||||||
|
"""
|
||||||
|
|
||||||
logCategory = 'PopenTask'
|
logCategory = 'PopenTask'
|
||||||
bufsize = 1024
|
bufsize = 1024
|
||||||
@@ -42,7 +44,7 @@ class PopenTask(task.Task):
|
|||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
close_fds=True, cwd=self.cwd)
|
close_fds=True, cwd=self.cwd)
|
||||||
except OSError as e:
|
except OSError, e:
|
||||||
import errno
|
import errno
|
||||||
if e.errno == errno.ENOENT:
|
if e.errno == errno.ENOENT:
|
||||||
self.commandMissing()
|
self.commandMissing()
|
||||||
@@ -86,7 +88,7 @@ class PopenTask(task.Task):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self._done()
|
self._done()
|
||||||
except Exception as e:
|
except Exception, e:
|
||||||
logger.debug('exception during _read(): %r', str(e))
|
logger.debug('exception during _read(): %r', str(e))
|
||||||
self.setException(e)
|
self.setException(e)
|
||||||
self.stop()
|
self.stop()
|
||||||
@@ -116,29 +118,31 @@ class PopenTask(task.Task):
|
|||||||
# self.stop()
|
# self.stop()
|
||||||
|
|
||||||
def readbytesout(self, bytes):
|
def readbytesout(self, bytes):
|
||||||
"""Call when bytes have been read from stdout.
|
"""
|
||||||
|
Called when bytes have been read from stdout.
|
||||||
:param bytes:
|
|
||||||
:type bytes:
|
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def readbyteserr(self, bytes):
|
def readbyteserr(self, bytes):
|
||||||
"""Call when bytes have been read from stderr.
|
"""
|
||||||
|
Called when bytes have been read from stderr.
|
||||||
:param bytes:
|
|
||||||
:type bytes:
|
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def done(self):
|
def done(self):
|
||||||
"""Call when the command completed successfully."""
|
"""
|
||||||
|
Called when the command completed successfully.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def failed(self):
|
def failed(self):
|
||||||
"""Call when the command failed."""
|
"""
|
||||||
|
Called when the command failed.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def commandMissing(self):
|
def commandMissing(self):
|
||||||
"""Call when the command is missing."""
|
"""
|
||||||
|
Called when the command is missing.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|||||||
6
whipper/extern/asyncsub.py
vendored
6
whipper/extern/asyncsub.py
vendored
@@ -53,7 +53,7 @@ class Popen(subprocess.Popen):
|
|||||||
(errCode, written) = WriteFile(x, input)
|
(errCode, written) = WriteFile(x, input)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return self._close('stdin')
|
return self._close('stdin')
|
||||||
except (subprocess.pywintypes.error, Exception) as why:
|
except (subprocess.pywintypes.error, Exception), why:
|
||||||
if why[0] in (109, errno.ESHUTDOWN):
|
if why[0] in (109, errno.ESHUTDOWN):
|
||||||
return self._close('stdin')
|
return self._close('stdin')
|
||||||
raise
|
raise
|
||||||
@@ -74,7 +74,7 @@ class Popen(subprocess.Popen):
|
|||||||
(errCode, read) = ReadFile(x, nAvail, None)
|
(errCode, read) = ReadFile(x, nAvail, None)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return self._close(which)
|
return self._close(which)
|
||||||
except (subprocess.pywintypes.error, Exception) as why:
|
except (subprocess.pywintypes.error, Exception), why:
|
||||||
if why[0] in (109, errno.ESHUTDOWN):
|
if why[0] in (109, errno.ESHUTDOWN):
|
||||||
return self._close(which)
|
return self._close(which)
|
||||||
raise
|
raise
|
||||||
@@ -94,7 +94,7 @@ class Popen(subprocess.Popen):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
written = os.write(self.stdin.fileno(), input)
|
written = os.write(self.stdin.fileno(), input)
|
||||||
except OSError as why:
|
except OSError, why:
|
||||||
if why[0] == errno.EPIPE: # broken pipe
|
if why[0] == errno.EPIPE: # broken pipe
|
||||||
return self._close('stdin')
|
return self._close('stdin')
|
||||||
raise
|
raise
|
||||||
|
|||||||
195
whipper/extern/task/task.py
vendored
195
whipper/extern/task/task.py
vendored
@@ -24,7 +24,9 @@ import gobject
|
|||||||
|
|
||||||
|
|
||||||
class TaskException(Exception):
|
class TaskException(Exception):
|
||||||
"""I wrap an exception that happened during task execution."""
|
"""
|
||||||
|
I wrap an exception that happened during task execution.
|
||||||
|
"""
|
||||||
|
|
||||||
exception = None # original exception
|
exception = None # original exception
|
||||||
|
|
||||||
@@ -37,16 +39,9 @@ class TaskException(Exception):
|
|||||||
|
|
||||||
|
|
||||||
def _getExceptionMessage(exception, frame=-1, filename=None):
|
def _getExceptionMessage(exception, frame=-1, filename=None):
|
||||||
"""Return a short message based on an exception, useful for debugging.
|
"""
|
||||||
|
Return a short message based on an exception, useful for debugging.
|
||||||
Tries to find where the exception was triggered.
|
Tries to find where the exception was triggered.
|
||||||
|
|
||||||
:param exception:
|
|
||||||
:type exception:
|
|
||||||
:param frame: (Default value = -1)
|
|
||||||
:type frame:
|
|
||||||
:param filename: (Default value = None)
|
|
||||||
:type filename:
|
|
||||||
"""
|
"""
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
@@ -71,7 +66,9 @@ def _getExceptionMessage(exception, frame=-1, filename=None):
|
|||||||
|
|
||||||
|
|
||||||
class LogStub(object):
|
class LogStub(object):
|
||||||
"""I am a stub for a log interface."""
|
"""
|
||||||
|
I am a stub for a log interface.
|
||||||
|
"""
|
||||||
|
|
||||||
# log stubs
|
# log stubs
|
||||||
def log(self, message, *args):
|
def log(self, message, *args):
|
||||||
@@ -91,22 +88,19 @@ class LogStub(object):
|
|||||||
|
|
||||||
|
|
||||||
class Task(LogStub):
|
class Task(LogStub):
|
||||||
"""I wrap a task in an asynchronous interface.
|
"""
|
||||||
|
I wrap a task in an asynchronous interface.
|
||||||
I can be listened to for starting, stopping, description changes
|
I can be listened to for starting, stopping, description changes
|
||||||
and progress updates.
|
and progress updates.
|
||||||
|
|
||||||
I communicate an error by setting self.exception to an exception and
|
I communicate an error by setting self.exception to an exception and
|
||||||
stopping myself from running. The listener can then handle the
|
stopping myself from running.
|
||||||
Task.exception.
|
The listener can then handle the Task.exception.
|
||||||
|
|
||||||
:cvar description: what am I doing.
|
@ivar description: what am I doing
|
||||||
:vartype description:
|
@ivar exception: set if an exception happened during the task
|
||||||
:cvar exception: set if an exception happened during the task
|
execution. Will be raised through run() at the end.
|
||||||
execution. Will be raised through run() at the end.
|
|
||||||
:vartype exception:
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logCategory = 'Task'
|
logCategory = 'Task'
|
||||||
|
|
||||||
description = 'I am doing something.'
|
description = 'I am doing something.'
|
||||||
@@ -123,7 +117,8 @@ class Task(LogStub):
|
|||||||
|
|
||||||
# subclass methods
|
# subclass methods
|
||||||
def start(self, runner):
|
def start(self, runner):
|
||||||
"""Start the task.
|
"""
|
||||||
|
Start the task.
|
||||||
|
|
||||||
Subclasses should chain up to me at the beginning.
|
Subclasses should chain up to me at the beginning.
|
||||||
|
|
||||||
@@ -133,9 +128,6 @@ class Task(LogStub):
|
|||||||
|
|
||||||
If start doesn't raise an exception, the task should run until
|
If start doesn't raise an exception, the task should run until
|
||||||
complete, or setException and stop().
|
complete, or setException and stop().
|
||||||
|
|
||||||
:param runner:
|
|
||||||
:type runner:
|
|
||||||
"""
|
"""
|
||||||
self.debug('starting')
|
self.debug('starting')
|
||||||
self.setProgress(self.progress)
|
self.setProgress(self.progress)
|
||||||
@@ -144,8 +136,8 @@ class Task(LogStub):
|
|||||||
self._notifyListeners('started')
|
self._notifyListeners('started')
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Stop the task.
|
"""
|
||||||
|
Stop the task.
|
||||||
Also resets the runner on the task.
|
Also resets the runner on the task.
|
||||||
|
|
||||||
Subclasses should chain up to me at the end.
|
Subclasses should chain up to me at the end.
|
||||||
@@ -167,12 +159,9 @@ class Task(LogStub):
|
|||||||
|
|
||||||
# base class methods
|
# base class methods
|
||||||
def setProgress(self, value):
|
def setProgress(self, value):
|
||||||
"""Notify about progress changes bigger than the increment.
|
"""
|
||||||
|
Notify about progress changes bigger than the increment.
|
||||||
Called by subclass implementations as the task progresses.
|
Called by subclass implementations as the task progresses.
|
||||||
|
|
||||||
:param value:
|
|
||||||
:type value:
|
|
||||||
"""
|
"""
|
||||||
if (value - self.progress > self.increment or
|
if (value - self.progress > self.increment or
|
||||||
value >= 1.0 or value == 0.0):
|
value >= 1.0 or value == 0.0):
|
||||||
@@ -187,12 +176,9 @@ class Task(LogStub):
|
|||||||
|
|
||||||
# FIXME: unify?
|
# FIXME: unify?
|
||||||
def setExceptionAndTraceback(self, exception):
|
def setExceptionAndTraceback(self, exception):
|
||||||
"""Call this to set a synthetically created exception.
|
"""
|
||||||
|
Call this to set a synthetically created exception (and not one
|
||||||
Not an exception that was actually raised and caught.
|
that was actually raised and caught)
|
||||||
|
|
||||||
:param exception:
|
|
||||||
:type exception:
|
|
||||||
"""
|
"""
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
@@ -215,10 +201,8 @@ class Task(LogStub):
|
|||||||
setAndRaiseException = setExceptionAndTraceback
|
setAndRaiseException = setExceptionAndTraceback
|
||||||
|
|
||||||
def setException(self, exception):
|
def setException(self, exception):
|
||||||
"""Call this to set a caught exception on the task.
|
"""
|
||||||
|
Call this to set a caught exception on the task.
|
||||||
:param exception:
|
|
||||||
:type exception:
|
|
||||||
"""
|
"""
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
@@ -237,12 +221,10 @@ class Task(LogStub):
|
|||||||
self.runner.schedule(self, delta, callable, *args, **kwargs)
|
self.runner.schedule(self, delta, callable, *args, **kwargs)
|
||||||
|
|
||||||
def addListener(self, listener):
|
def addListener(self, listener):
|
||||||
"""Add a listener for task status changes.
|
"""
|
||||||
|
Add a listener for task status changes.
|
||||||
|
|
||||||
Listeners should implement started, stopped, and progressed.
|
Listeners should implement started, stopped, and progressed.
|
||||||
|
|
||||||
:param listener:
|
|
||||||
:type listener:
|
|
||||||
"""
|
"""
|
||||||
self.debug('Adding listener %r', listener)
|
self.debug('Adding listener %r', listener)
|
||||||
if not self._listeners:
|
if not self._listeners:
|
||||||
@@ -255,53 +237,43 @@ class Task(LogStub):
|
|||||||
method = getattr(l, methodName)
|
method = getattr(l, methodName)
|
||||||
try:
|
try:
|
||||||
method(self, *args, **kwargs)
|
method(self, *args, **kwargs)
|
||||||
except Exception as e:
|
except Exception, e:
|
||||||
self.setException(e)
|
self.setException(e)
|
||||||
|
|
||||||
|
|
||||||
# FIXME: should this become a real interface, like in zope ?
|
# FIXME: should this become a real interface, like in zope ?
|
||||||
class ITaskListener(object):
|
class ITaskListener(object):
|
||||||
"""I am an interface for objects listening to tasks."""
|
"""
|
||||||
|
I am an interface for objects listening to tasks.
|
||||||
|
"""
|
||||||
# listener callbacks
|
# listener callbacks
|
||||||
|
|
||||||
def progressed(self, task, value):
|
def progressed(self, task, value):
|
||||||
"""Implement me to be informed about progress.
|
|
||||||
|
|
||||||
:param value: progress, from 0.0 to 1.0.
|
|
||||||
:type value: float
|
|
||||||
:param task:
|
|
||||||
:type task:
|
|
||||||
"""
|
"""
|
||||||
pass
|
Implement me to be informed about progress.
|
||||||
|
|
||||||
|
@type value: float
|
||||||
|
@param value: progress, from 0.0 to 1.0
|
||||||
|
"""
|
||||||
|
|
||||||
def described(self, task, description):
|
def described(self, task, description):
|
||||||
"""Implement me to be informed about description changes.
|
|
||||||
|
|
||||||
:param description:
|
|
||||||
:type description: str
|
|
||||||
:param task:
|
|
||||||
:type task:
|
|
||||||
"""
|
"""
|
||||||
pass
|
Implement me to be informed about description changes.
|
||||||
|
|
||||||
|
@type description: str
|
||||||
|
@param description: description
|
||||||
|
"""
|
||||||
|
|
||||||
def started(self, task):
|
def started(self, task):
|
||||||
"""Implement me to be informed about the task starting.
|
|
||||||
|
|
||||||
:param task:
|
|
||||||
:type task:
|
|
||||||
"""
|
"""
|
||||||
pass
|
Implement me to be informed about the task starting.
|
||||||
|
"""
|
||||||
|
|
||||||
def stopped(self, task):
|
def stopped(self, task):
|
||||||
"""Implement me to be informed about the task stopping.
|
|
||||||
|
|
||||||
If the task had an error, task.exception will be set.
|
|
||||||
|
|
||||||
:param task:
|
|
||||||
:type task:
|
|
||||||
"""
|
"""
|
||||||
pass
|
Implement me to be informed about the task stopping.
|
||||||
|
If the task had an error, task.exception will be set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
# this is a Dummy task that can be used to test if this works at all
|
# this is a Dummy task that can be used to test if this works at all
|
||||||
@@ -321,10 +293,11 @@ class DummyTask(Task):
|
|||||||
|
|
||||||
|
|
||||||
class BaseMultiTask(Task, ITaskListener):
|
class BaseMultiTask(Task, ITaskListener):
|
||||||
"""I perform multiple tasks.
|
"""
|
||||||
|
I perform multiple tasks.
|
||||||
|
|
||||||
:cvar tasks: the tasks to run.
|
@ivar tasks: the tasks to run
|
||||||
:vartype tasks: list of L{Task}
|
@type tasks: list of L{Task}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
description = 'Doing various tasks'
|
description = 'Doing various tasks'
|
||||||
@@ -335,23 +308,21 @@ class BaseMultiTask(Task, ITaskListener):
|
|||||||
self._task = 0
|
self._task = 0
|
||||||
|
|
||||||
def addTask(self, task):
|
def addTask(self, task):
|
||||||
"""Add a task.
|
"""
|
||||||
|
Add a task.
|
||||||
|
|
||||||
:param task:
|
@type task: L{Task}
|
||||||
:type task: L{Task}
|
|
||||||
"""
|
"""
|
||||||
if self.tasks is None:
|
if self.tasks is None:
|
||||||
self.tasks = []
|
self.tasks = []
|
||||||
self.tasks.append(task)
|
self.tasks.append(task)
|
||||||
|
|
||||||
def start(self, runner):
|
def start(self, runner):
|
||||||
"""Start tasks.
|
"""
|
||||||
|
Start tasks.
|
||||||
|
|
||||||
Tasks can still be added while running. For example,
|
Tasks can still be added while running. For example,
|
||||||
a first task can determine how many additional tasks to run.
|
a first task can determine how many additional tasks to run.
|
||||||
|
|
||||||
:param runner:
|
|
||||||
:type runner:
|
|
||||||
"""
|
"""
|
||||||
Task.start(self, runner)
|
Task.start(self, runner)
|
||||||
|
|
||||||
@@ -363,7 +334,9 @@ class BaseMultiTask(Task, ITaskListener):
|
|||||||
self.next()
|
self.next()
|
||||||
|
|
||||||
def next(self):
|
def next(self):
|
||||||
"""Start the next task."""
|
"""
|
||||||
|
Start the next task.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
# start next task
|
# start next task
|
||||||
task = self.tasks[self._task]
|
task = self.tasks[self._task]
|
||||||
@@ -376,7 +349,7 @@ class BaseMultiTask(Task, ITaskListener):
|
|||||||
task.start(self.runner)
|
task.start(self.runner)
|
||||||
self.debug('BaseMultiTask.next(): started task %d of %d: %r',
|
self.debug('BaseMultiTask.next(): started task %d of %d: %r',
|
||||||
self._task, len(self.tasks), task)
|
self._task, len(self.tasks), task)
|
||||||
except Exception as e:
|
except Exception, e:
|
||||||
self.setException(e)
|
self.setException(e)
|
||||||
self.debug('Got exception during next: %r', self.exceptionMessage)
|
self.debug('Got exception during next: %r', self.exceptionMessage)
|
||||||
self.stop()
|
self.stop()
|
||||||
@@ -390,12 +363,9 @@ class BaseMultiTask(Task, ITaskListener):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def stopped(self, task):
|
def stopped(self, task):
|
||||||
"""Subclasses should chain up to me at the end of their implementation.
|
"""
|
||||||
|
Subclasses should chain up to me at the end of their implementation.
|
||||||
They should fall through to chaining up if there is an exception.
|
They should fall through to chaining up if there is an exception.
|
||||||
|
|
||||||
:param task:
|
|
||||||
:type task:
|
|
||||||
"""
|
"""
|
||||||
self.log('BaseMultiTask.stopped: task %r (%d of %d)',
|
self.log('BaseMultiTask.stopped: task %r (%d of %d)',
|
||||||
task, self.tasks.index(task) + 1, len(self.tasks))
|
task, self.tasks.index(task) + 1, len(self.tasks))
|
||||||
@@ -418,11 +388,10 @@ class BaseMultiTask(Task, ITaskListener):
|
|||||||
|
|
||||||
|
|
||||||
class MultiSeparateTask(BaseMultiTask):
|
class MultiSeparateTask(BaseMultiTask):
|
||||||
"""I perform multiple tasks.
|
"""
|
||||||
|
I perform multiple tasks.
|
||||||
I track progress of each individual task, going back to 0 for each task.
|
I track progress of each individual task, going back to 0 for each task.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
description = 'Doing various tasks separately'
|
description = 'Doing various tasks separately'
|
||||||
|
|
||||||
def start(self, runner):
|
def start(self, runner):
|
||||||
@@ -445,8 +414,8 @@ class MultiSeparateTask(BaseMultiTask):
|
|||||||
|
|
||||||
|
|
||||||
class MultiCombinedTask(BaseMultiTask):
|
class MultiCombinedTask(BaseMultiTask):
|
||||||
"""I perform multiple tasks.
|
"""
|
||||||
|
I perform multiple tasks.
|
||||||
I track progress as a combined progress on all tasks on task granularity.
|
I track progress as a combined progress on all tasks on task granularity.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -464,41 +433,37 @@ class MultiCombinedTask(BaseMultiTask):
|
|||||||
|
|
||||||
|
|
||||||
class TaskRunner(LogStub):
|
class TaskRunner(LogStub):
|
||||||
"""I am a base class for task runners.
|
"""
|
||||||
|
I am a base class for task runners.
|
||||||
Task runners should be reusable.
|
Task runners should be reusable.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logCategory = 'TaskRunner'
|
logCategory = 'TaskRunner'
|
||||||
|
|
||||||
def run(self, task):
|
def run(self, task):
|
||||||
"""Run the given task.
|
"""
|
||||||
|
Run the given task.
|
||||||
|
|
||||||
:param task:
|
@type task: Task
|
||||||
:type task: Task
|
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
# methods for tasks to call
|
# methods for tasks to call
|
||||||
def schedule(self, delta, callable, *args, **kwargs):
|
def schedule(self, delta, callable, *args, **kwargs):
|
||||||
"""Schedule a single future call.
|
"""
|
||||||
|
Schedule a single future call.
|
||||||
|
|
||||||
Subclasses should implement this.
|
Subclasses should implement this.
|
||||||
|
|
||||||
:param delta: time in the future to schedule call for, in seconds.
|
@type delta: float
|
||||||
:type delta: float
|
@param delta: time in the future to schedule call for, in seconds.
|
||||||
:param callable:
|
|
||||||
:type callable:
|
|
||||||
:param args:
|
|
||||||
:type args:
|
|
||||||
:param kwargs:
|
|
||||||
:type kwargs:
|
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class SyncRunner(TaskRunner, ITaskListener):
|
class SyncRunner(TaskRunner, ITaskListener):
|
||||||
"""I run the task synchronously in a gobject MainLoop."""
|
"""
|
||||||
|
I run the task synchronously in a gobject MainLoop.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, verbose=True):
|
def __init__(self, verbose=True):
|
||||||
self._verbose = verbose
|
self._verbose = verbose
|
||||||
@@ -537,7 +502,7 @@ class SyncRunner(TaskRunner, ITaskListener):
|
|||||||
try:
|
try:
|
||||||
self.debug('start task %r' % task)
|
self.debug('start task %r' % task)
|
||||||
task.start(self)
|
task.start(self)
|
||||||
except Exception as e:
|
except Exception, e:
|
||||||
# getExceptionMessage uses global exception state that doesn't
|
# getExceptionMessage uses global exception state that doesn't
|
||||||
# hang around, so store the message
|
# hang around, so store the message
|
||||||
task.setException(e)
|
task.setException(e)
|
||||||
@@ -551,7 +516,7 @@ class SyncRunner(TaskRunner, ITaskListener):
|
|||||||
callable, args, kwargs)
|
callable, args, kwargs)
|
||||||
callable(*args, **kwargs)
|
callable(*args, **kwargs)
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception, e:
|
||||||
self.debug('exception when calling scheduled callable %r',
|
self.debug('exception when calling scheduled callable %r',
|
||||||
callable)
|
callable)
|
||||||
task.setException(e)
|
task.setException(e)
|
||||||
|
|||||||
@@ -18,9 +18,10 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with whipper. If not, see <http://www.gnu.org/licenses/>.
|
# along with whipper. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""Reading .cue files.
|
"""
|
||||||
|
Reading .cue files
|
||||||
|
|
||||||
.. seealso:: http://digitalx.org/cuesheetsyntax.php
|
See http://digitalx.org/cuesheetsyntax.php
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
@@ -58,25 +59,18 @@ _INDEX_RE = re.compile(r"""
|
|||||||
|
|
||||||
|
|
||||||
class CueFile(object):
|
class CueFile(object):
|
||||||
"""I represent a .cue file as an object.
|
|
||||||
|
|
||||||
:cvar logCategory:
|
|
||||||
:vartype logCategory:
|
|
||||||
:ivar path:
|
|
||||||
:vartype path:
|
|
||||||
:ivar rems:
|
|
||||||
:vartype rems:
|
|
||||||
:ivar messages:
|
|
||||||
:vartype messages:
|
|
||||||
:ivar leadout:
|
|
||||||
:vartype leadout:
|
|
||||||
:ivar table: the index table.
|
|
||||||
:vartype table: L{table.Table}
|
|
||||||
"""
|
"""
|
||||||
|
I represent a .cue file as an object.
|
||||||
|
|
||||||
|
@type table: L{table.Table}
|
||||||
|
@ivar table: the index table.
|
||||||
|
"""
|
||||||
logCategory = 'CueFile'
|
logCategory = 'CueFile'
|
||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
|
"""
|
||||||
|
@type path: unicode
|
||||||
|
"""
|
||||||
assert type(path) is unicode, "%r is not unicode" % path
|
assert type(path) is unicode, "%r is not unicode" % path
|
||||||
|
|
||||||
self._path = path
|
self._path = path
|
||||||
@@ -157,12 +151,10 @@ class CueFile(object):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
def message(self, number, message):
|
def message(self, number, message):
|
||||||
"""Add a message about a given line in the cue file.
|
"""
|
||||||
|
Add a message about a given line in the cue file.
|
||||||
|
|
||||||
:param number: line number, counting from 0.
|
@param number: line number, counting from 0.
|
||||||
:type number:
|
|
||||||
:param message:
|
|
||||||
:type message:
|
|
||||||
"""
|
"""
|
||||||
self._messages.append((number + 1, message))
|
self._messages.append((number + 1, message))
|
||||||
|
|
||||||
@@ -187,24 +179,23 @@ class CueFile(object):
|
|||||||
return -1
|
return -1
|
||||||
|
|
||||||
def getRealPath(self, path):
|
def getRealPath(self, path):
|
||||||
"""Translate the .cue's FILE to an existing path.
|
"""
|
||||||
|
Translate the .cue's FILE to an existing path.
|
||||||
|
|
||||||
:param path:
|
@type path: unicode
|
||||||
:type path: unicode
|
|
||||||
"""
|
"""
|
||||||
return common.getRealPath(self._path, path)
|
return common.getRealPath(self._path, path)
|
||||||
|
|
||||||
|
|
||||||
class File:
|
class File:
|
||||||
"""I represent a FILE line in a cue file.
|
"""
|
||||||
|
I represent a FILE line in a cue file.
|
||||||
:ivar path:
|
|
||||||
:vartype path:
|
|
||||||
:ivar format:
|
|
||||||
:vartype format:
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, path, format):
|
def __init__(self, path, format):
|
||||||
|
"""
|
||||||
|
@type path: unicode
|
||||||
|
"""
|
||||||
assert type(path) is unicode, "%r is not unicode" % path
|
assert type(path) is unicode, "%r is not unicode" % path
|
||||||
|
|
||||||
self.path = path
|
self.path = path
|
||||||
|
|||||||
@@ -18,6 +18,10 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with whipper. If not, see <http://www.gnu.org/licenses/>.
|
# along with whipper. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Wrap on-disk CD images based on the .cue file.
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from whipper.common import encode
|
from whipper.common import encode
|
||||||
@@ -31,23 +35,17 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class Image(object):
|
class Image(object):
|
||||||
"""Wrap on-disk CD images based on the .cue file.
|
|
||||||
|
|
||||||
:ivar path: .cue path.
|
|
||||||
:vartype path: unicode
|
|
||||||
:ivar cue:
|
|
||||||
:vartype cue:
|
|
||||||
:ivar offsets:
|
|
||||||
:vartype offsets:
|
|
||||||
:ivar lengths:
|
|
||||||
:vartype lengths:
|
|
||||||
:ivar table: the Table of Contents for this image.
|
|
||||||
:vartype table: L{table.Table}
|
|
||||||
"""
|
"""
|
||||||
|
@ivar table: The Table of Contents for this image.
|
||||||
|
@type table: L{table.Table}
|
||||||
|
"""
|
||||||
logCategory = 'Image'
|
logCategory = 'Image'
|
||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
|
"""
|
||||||
|
@type path: unicode
|
||||||
|
@param path: .cue path
|
||||||
|
"""
|
||||||
assert type(path) is unicode, "%r is not unicode" % path
|
assert type(path) is unicode, "%r is not unicode" % path
|
||||||
|
|
||||||
self._path = path
|
self._path = path
|
||||||
@@ -59,22 +57,19 @@ class Image(object):
|
|||||||
self.table = None
|
self.table = None
|
||||||
|
|
||||||
def getRealPath(self, path):
|
def getRealPath(self, path):
|
||||||
"""Translate the .cue's FILE to an existing path.
|
"""
|
||||||
|
Translate the .cue's FILE to an existing path.
|
||||||
|
|
||||||
:param path: .cue path.
|
@param path: .cue path
|
||||||
:type path:
|
|
||||||
"""
|
"""
|
||||||
assert type(path) is unicode, "%r is not unicode" % path
|
assert type(path) is unicode, "%r is not unicode" % path
|
||||||
|
|
||||||
return self.cue.getRealPath(path)
|
return self.cue.getRealPath(path)
|
||||||
|
|
||||||
def setup(self, runner):
|
def setup(self, runner):
|
||||||
"""Do initial setup.
|
"""
|
||||||
|
Do initial setup, like figuring out track lengths, and
|
||||||
Like figuring out track lengths and constructing the Table of Contents.
|
constructing the Table of Contents.
|
||||||
|
|
||||||
:param runner:
|
|
||||||
:type runner:
|
|
||||||
"""
|
"""
|
||||||
logger.debug('setup image start')
|
logger.debug('setup image start')
|
||||||
verify = ImageVerifyTask(self)
|
verify = ImageVerifyTask(self)
|
||||||
@@ -113,18 +108,8 @@ class Image(object):
|
|||||||
|
|
||||||
|
|
||||||
class ImageVerifyTask(task.MultiSeparateTask):
|
class ImageVerifyTask(task.MultiSeparateTask):
|
||||||
"""I verify a disk image and get the necessary track lengths.
|
"""
|
||||||
|
I verify a disk image and get the necessary track lengths.
|
||||||
:cvar logCategory:
|
|
||||||
:vartype logCategory:
|
|
||||||
:cvar description:
|
|
||||||
:vartype description:
|
|
||||||
:cvar lengths:
|
|
||||||
:vartype lengths:
|
|
||||||
:ivar image:
|
|
||||||
:vartype image:
|
|
||||||
:ivar tasks:
|
|
||||||
:vartype tasks:
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logCategory = 'ImageVerifyTask'
|
logCategory = 'ImageVerifyTask'
|
||||||
@@ -188,14 +173,8 @@ class ImageVerifyTask(task.MultiSeparateTask):
|
|||||||
|
|
||||||
|
|
||||||
class ImageEncodeTask(task.MultiSeparateTask):
|
class ImageEncodeTask(task.MultiSeparateTask):
|
||||||
"""I encode a disk image to a different format.
|
"""
|
||||||
|
I encode a disk image to a different format.
|
||||||
:ivar image:
|
|
||||||
:vartype image:
|
|
||||||
:ivar tasks:
|
|
||||||
:vartype tasks:
|
|
||||||
:ivar lengths:
|
|
||||||
:vartype lengths:
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
description = "Encoding tracks"
|
description = "Encoding tracks"
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with whipper. If not, see <http://www.gnu.org/licenses/>.
|
# along with whipper. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""Wrap Table of Contents."""
|
"""
|
||||||
|
Wrap Table of Contents.
|
||||||
|
"""
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import urllib
|
import urllib
|
||||||
@@ -51,22 +53,20 @@ CDTEXT_FIELDS = [
|
|||||||
|
|
||||||
|
|
||||||
class Track:
|
class Track:
|
||||||
"""I represent a track entry in an Table.
|
"""
|
||||||
|
I represent a track entry in an Table.
|
||||||
|
|
||||||
:cvar number: track number (1-based).
|
@ivar number: track number (1-based)
|
||||||
:vartype number: int
|
@type number: int
|
||||||
:cvar audio: whether the track is audio.
|
@ivar audio: whether the track is audio
|
||||||
:vartype audio: bool
|
@type audio: bool
|
||||||
:cvar indexes:
|
@type indexes: dict of number -> L{Index}
|
||||||
:vartype indexes: dict of number -> L{Index}
|
@ivar isrc: ISRC code (12 alphanumeric characters)
|
||||||
:cvar isrc: ISRC code (12 alphanumeric characters).
|
@type isrc: str
|
||||||
:vartype isrc: str
|
@ivar cdtext: dictionary of CD Text information; see L{CDTEXT_KEYS}.
|
||||||
:cvar cdtext: dictionary of CD Text information; see L{CDTEXT_KEYS}.
|
@type cdtext: str -> unicode
|
||||||
:vartype cdtext: str -> unicode
|
@ivar pre_emphasis: whether track is pre-emphasised
|
||||||
:cvar session:
|
@type pre_emphasis: bool
|
||||||
:vartype session:
|
|
||||||
:cvar pre_emphasis: whether track is pre-emphasised.
|
|
||||||
:vartype pre_emphasis: bool
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
number = None
|
number = None
|
||||||
@@ -88,18 +88,8 @@ class Track:
|
|||||||
|
|
||||||
def index(self, number, absolute=None, path=None, relative=None,
|
def index(self, number, absolute=None, path=None, relative=None,
|
||||||
counter=None):
|
counter=None):
|
||||||
"""Index constructor.
|
"""
|
||||||
|
@type path: unicode or None
|
||||||
:param number:
|
|
||||||
:type number:
|
|
||||||
:param absolute: (Default value = None)
|
|
||||||
:type absolute:
|
|
||||||
:param path: (Default value = None)
|
|
||||||
:type path: unicode or None
|
|
||||||
:param relative: (Default value = None)
|
|
||||||
:type relative:
|
|
||||||
:param counter: (Default value = None)
|
|
||||||
:type counter:
|
|
||||||
"""
|
"""
|
||||||
if path is not None:
|
if path is not None:
|
||||||
assert type(path) is unicode, "%r is not unicode" % path
|
assert type(path) is unicode, "%r is not unicode" % path
|
||||||
@@ -111,20 +101,24 @@ class Track:
|
|||||||
return self.indexes[number]
|
return self.indexes[number]
|
||||||
|
|
||||||
def getFirstIndex(self):
|
def getFirstIndex(self):
|
||||||
"""Get the first chronological index for this track.
|
"""
|
||||||
|
Get the first chronological index for this track.
|
||||||
|
|
||||||
Typically this is INDEX 01; but it could be INDEX 00 if there's
|
Typically this is INDEX 01; but it could be INDEX 00 if there's
|
||||||
a pre-gap.
|
a pre-gap.
|
||||||
"""
|
"""
|
||||||
indexes = sorted(self.indexes.keys())
|
indexes = self.indexes.keys()
|
||||||
|
indexes.sort()
|
||||||
return self.indexes[indexes[0]]
|
return self.indexes[indexes[0]]
|
||||||
|
|
||||||
def getLastIndex(self):
|
def getLastIndex(self):
|
||||||
indexes = sorted(self.indexes.keys())
|
indexes = self.indexes.keys()
|
||||||
|
indexes.sort()
|
||||||
return self.indexes[indexes[-1]]
|
return self.indexes[indexes[-1]]
|
||||||
|
|
||||||
def getPregap(self):
|
def getPregap(self):
|
||||||
"""Compute the length of the pregap for this track.
|
"""
|
||||||
|
Returns the length of the pregap for this track.
|
||||||
|
|
||||||
The pregap is 0 if there is no index 0, and the difference between
|
The pregap is 0 if there is no index 0, and the difference between
|
||||||
index 1 and index 0 if there is.
|
index 1 and index 0 if there is.
|
||||||
@@ -136,21 +130,11 @@ class Track:
|
|||||||
|
|
||||||
|
|
||||||
class Index:
|
class Index:
|
||||||
"""I represent an index for the given source.
|
|
||||||
|
|
||||||
:cvar number:
|
|
||||||
:vartype number:
|
|
||||||
:cvar absolute:
|
|
||||||
:vartype absolute:
|
|
||||||
:cvar path:
|
|
||||||
:vartype path: unicode or None
|
|
||||||
:cvar relative:
|
|
||||||
:vartype relative:
|
|
||||||
:cvar counter: counter for the index source; distinguishes between
|
|
||||||
the matching FILE lines in .cue files for example
|
|
||||||
:vartype counter:
|
|
||||||
"""
|
"""
|
||||||
|
@ivar counter: counter for the index source; distinguishes between
|
||||||
|
the matching FILE lines in .cue files for example
|
||||||
|
@type path: unicode or None
|
||||||
|
"""
|
||||||
number = None
|
number = None
|
||||||
absolute = None
|
absolute = None
|
||||||
path = None
|
path = None
|
||||||
@@ -175,18 +159,14 @@ class Index:
|
|||||||
|
|
||||||
|
|
||||||
class Table(object):
|
class Table(object):
|
||||||
"""I represent a table of indexes on a CD.
|
"""
|
||||||
|
I represent a table of indexes on a CD.
|
||||||
|
|
||||||
:cvar tracks: tracks on this CD.
|
@ivar tracks: tracks on this CD
|
||||||
:vartype tracks: list of L{Track}
|
@type tracks: list of L{Track}
|
||||||
:cvar leadout:
|
@ivar catalog: catalog number
|
||||||
:vartype leadout:
|
@type catalog: str
|
||||||
:cvar catalog: catalog number.
|
@type cdtext: dict of str -> str
|
||||||
:vartype catalog: str
|
|
||||||
:cvar cdtext:
|
|
||||||
:vartype cdtext: dict of str -> str
|
|
||||||
:cvar mbdiscid:
|
|
||||||
:vartype mbdiscid:
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
tracks = None # list of Track
|
tracks = None # list of Track
|
||||||
@@ -213,23 +193,23 @@ class Table(object):
|
|||||||
logger.debug('set logName')
|
logger.debug('set logName')
|
||||||
|
|
||||||
def getTrackStart(self, number):
|
def getTrackStart(self, number):
|
||||||
"""Get the start of the given track number's index 1, in CD frames.
|
"""
|
||||||
|
@param number: the track number, 1-based
|
||||||
|
@type number: int
|
||||||
|
|
||||||
:param number: the track number, 1-based.
|
@returns: the start of the given track number's index 1, in CD frames
|
||||||
:type number: int
|
@rtype: int
|
||||||
:returns: the start of the given track number's index 1, in CD frames.
|
|
||||||
:rtype: int
|
|
||||||
"""
|
"""
|
||||||
track = self.tracks[number - 1]
|
track = self.tracks[number - 1]
|
||||||
return track.getIndex(1).absolute
|
return track.getIndex(1).absolute
|
||||||
|
|
||||||
def getTrackEnd(self, number):
|
def getTrackEnd(self, number):
|
||||||
"""Get the end of the given track number.
|
"""
|
||||||
|
@param number: the track number, 1-based
|
||||||
|
@type number: int
|
||||||
|
|
||||||
:param number: the track number, 1-based.
|
@returns: the end of the given track number (ie index 1 of next track)
|
||||||
:type number: int
|
@rtype: int
|
||||||
:returns: the end of the given track number (ie index 1 of next track).
|
|
||||||
:rtype: int
|
|
||||||
"""
|
"""
|
||||||
# default to end of disc
|
# default to end of disc
|
||||||
end = self.leadout - 1
|
end = self.leadout - 1
|
||||||
@@ -248,28 +228,25 @@ class Table(object):
|
|||||||
return end
|
return end
|
||||||
|
|
||||||
def getTrackLength(self, number):
|
def getTrackLength(self, number):
|
||||||
"""Get the length if the given track number, in cd frames.
|
"""
|
||||||
|
@param number: the track number, 1-based
|
||||||
|
@type number: int
|
||||||
|
|
||||||
:param number: the track number, 1-based.
|
@returns: the length of the given track number, in CD frames
|
||||||
:type number: int
|
@rtype: int
|
||||||
:returns: the length of the given track number, in CD frames.
|
|
||||||
:rtype: int
|
|
||||||
"""
|
"""
|
||||||
return self.getTrackEnd(number) - self.getTrackStart(number) + 1
|
return self.getTrackEnd(number) - self.getTrackStart(number) + 1
|
||||||
|
|
||||||
def getAudioTracks(self):
|
def getAudioTracks(self):
|
||||||
"""Get the number of audio tracks on the CD.
|
"""
|
||||||
|
@returns: the number of audio tracks on the CD
|
||||||
:returns: the number of audio tracks on the CD.
|
@rtype: int
|
||||||
:rtype: int
|
|
||||||
"""
|
"""
|
||||||
return len([t for t in self.tracks if t.audio])
|
return len([t for t in self.tracks if t.audio])
|
||||||
|
|
||||||
def hasDataTracks(self):
|
def hasDataTracks(self):
|
||||||
"""Tell wheter the disc contains data tracks.
|
"""
|
||||||
|
@returns: whether this disc contains data tracks
|
||||||
:returns: whether this disc contains data tracks
|
|
||||||
:rtype: bool
|
|
||||||
"""
|
"""
|
||||||
return len([t for t in self.tracks if not t.audio]) > 0
|
return len([t for t in self.tracks if not t.audio]) > 0
|
||||||
|
|
||||||
@@ -282,16 +259,16 @@ class Table(object):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
def getCDDBValues(self):
|
def getCDDBValues(self):
|
||||||
"""Get all CDDB values needed to calculate disc id and lookup URL.
|
"""
|
||||||
|
Get all CDDB values needed to calculate disc id and lookup URL.
|
||||||
|
|
||||||
This includes:
|
This includes:
|
||||||
* CDDB disc id.
|
- CDDB disc id
|
||||||
* number of audio tracks.
|
- number of audio tracks
|
||||||
* offset of index 1 of each track.
|
- offset of index 1 of each track
|
||||||
* length of disc in seconds (including data track).
|
- length of disc in seconds (including data track)
|
||||||
|
|
||||||
:returns:
|
@rtype: list of int
|
||||||
:rtype: list of int
|
|
||||||
"""
|
"""
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
@@ -343,19 +320,21 @@ class Table(object):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def getCDDBDiscId(self):
|
def getCDDBDiscId(self):
|
||||||
"""Calculate the CDDB disc ID.
|
"""
|
||||||
|
Calculate the CDDB disc ID.
|
||||||
|
|
||||||
:returns: the 8-character hexadecimal disc ID.
|
@rtype: str
|
||||||
:rtype: str
|
@returns: the 8-character hexadecimal disc ID
|
||||||
"""
|
"""
|
||||||
values = self.getCDDBValues()
|
values = self.getCDDBValues()
|
||||||
return "%08x" % values[0]
|
return "%08x" % values[0]
|
||||||
|
|
||||||
def getMusicBrainzDiscId(self):
|
def getMusicBrainzDiscId(self):
|
||||||
"""Calculate the MusicBrainz disc ID.
|
"""
|
||||||
|
Calculate the MusicBrainz disc ID.
|
||||||
|
|
||||||
:returns: the 28-character base64-encoded disc ID.
|
@rtype: str
|
||||||
:rtype: str
|
@returns: the 28-character base64-encoded disc ID
|
||||||
"""
|
"""
|
||||||
if self.mbdiscid:
|
if self.mbdiscid:
|
||||||
logger.debug('getMusicBrainzDiscId: returning cached %r'
|
logger.debug('getMusicBrainzDiscId: returning cached %r'
|
||||||
@@ -426,13 +405,10 @@ class Table(object):
|
|||||||
'https', host, '/cdtoc/attach', '', query, ''))
|
'https', host, '/cdtoc/attach', '', query, ''))
|
||||||
|
|
||||||
def getFrameLength(self, data=False):
|
def getFrameLength(self, data=False):
|
||||||
"""Get the length in frames (excluding HTOA).
|
"""
|
||||||
|
Get the length in frames (excluding HTOA)
|
||||||
|
|
||||||
:param data: whether to include the data tracks in the length
|
@param data: whether to include the data tracks in the length
|
||||||
(Default value = False)
|
|
||||||
:type data:
|
|
||||||
:returns:
|
|
||||||
:rtype:
|
|
||||||
"""
|
"""
|
||||||
# the 'real' leadout, not offset by 150 frames
|
# the 'real' leadout, not offset by 150 frames
|
||||||
if data:
|
if data:
|
||||||
@@ -447,20 +423,22 @@ class Table(object):
|
|||||||
return durationFrames
|
return durationFrames
|
||||||
|
|
||||||
def duration(self):
|
def duration(self):
|
||||||
"""Get the duration in ms for all audio tracks (excluding HTOA)."""
|
"""
|
||||||
|
Get the duration in ms for all audio tracks (excluding HTOA).
|
||||||
|
"""
|
||||||
return int(self.getFrameLength() * 1000.0 / common.FRAMES_PER_SECOND)
|
return int(self.getFrameLength() * 1000.0 / common.FRAMES_PER_SECOND)
|
||||||
|
|
||||||
def _getMusicBrainzValues(self):
|
def _getMusicBrainzValues(self):
|
||||||
"""Get all MusicBrainz values needed to compute disc id and submit URL.
|
"""
|
||||||
|
Get all MusicBrainz values needed to calculate disc id and submit URL.
|
||||||
|
|
||||||
This includes:
|
This includes:
|
||||||
* track number of first track.
|
- track number of first track
|
||||||
* number of audio tracks.
|
- number of audio tracks
|
||||||
* leadout of disc.
|
- leadout of disc
|
||||||
* offset of index 1 of each track.
|
- offset of index 1 of each track
|
||||||
|
|
||||||
:returns:
|
@rtype: list of int
|
||||||
:rtype: list of int
|
|
||||||
"""
|
"""
|
||||||
# MusicBrainz disc id does not take into account data tracks
|
# MusicBrainz disc id does not take into account data tracks
|
||||||
|
|
||||||
@@ -498,16 +476,14 @@ class Table(object):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def cue(self, cuePath='', program='whipper'):
|
def cue(self, cuePath='', program='whipper'):
|
||||||
"""Dump our internal representation to a .cue file content.
|
"""
|
||||||
|
@param cuePath: path to the cue file to be written. If empty,
|
||||||
:param cuePath: path to the cue file to be written. If empty,
|
|
||||||
will treat paths as if in current directory.
|
will treat paths as if in current directory.
|
||||||
(Default value = '')
|
|
||||||
:type cuePath:
|
|
||||||
:param program: (Default value = 'whipper')
|
Dump our internal representation to a .cue file content.
|
||||||
:type program:
|
|
||||||
:returns:
|
@rtype: C{unicode}
|
||||||
:rtype: C{unicode}
|
|
||||||
"""
|
"""
|
||||||
logger.debug('generating .cue for cuePath %r', cuePath)
|
logger.debug('generating .cue for cuePath %r', cuePath)
|
||||||
|
|
||||||
@@ -567,7 +543,8 @@ class Table(object):
|
|||||||
if not track.audio:
|
if not track.audio:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
indexes = sorted(track.indexes.keys())
|
indexes = track.indexes.keys()
|
||||||
|
indexes.sort()
|
||||||
|
|
||||||
wroteTrack = False
|
wroteTrack = False
|
||||||
|
|
||||||
@@ -619,7 +596,7 @@ class Table(object):
|
|||||||
# handle any other INDEX 00 after its TRACK
|
# handle any other INDEX 00 after its TRACK
|
||||||
lines.append(" INDEX "
|
lines.append(" INDEX "
|
||||||
"%02d %s" % (0, common.framesToMSF(
|
"%02d %s" % (0, common.framesToMSF(
|
||||||
index00.relative)))
|
index00.relative)))
|
||||||
|
|
||||||
if number > 0:
|
if number > 0:
|
||||||
# index 00 is output after TRACK up above
|
# index 00 is output after TRACK up above
|
||||||
@@ -634,8 +611,8 @@ class Table(object):
|
|||||||
# methods that modify the table
|
# methods that modify the table
|
||||||
|
|
||||||
def clearFiles(self):
|
def clearFiles(self):
|
||||||
"""Clear all file backings.
|
"""
|
||||||
|
Clear all file backings.
|
||||||
Resets indexes paths and relative offsets.
|
Resets indexes paths and relative offsets.
|
||||||
"""
|
"""
|
||||||
# FIXME: do a loop over track indexes better, with a pythonic
|
# FIXME: do a loop over track indexes better, with a pythonic
|
||||||
@@ -657,23 +634,15 @@ class Table(object):
|
|||||||
break
|
break
|
||||||
|
|
||||||
def setFile(self, track, index, path, length, counter=None):
|
def setFile(self, track, index, path, length, counter=None):
|
||||||
"""Set the given file as the source from the given index on.
|
"""
|
||||||
|
Sets the given file as the source from the given index on.
|
||||||
Will loop over all indexes that fall within the given length,
|
Will loop over all indexes that fall within the given length,
|
||||||
to adjust the path.
|
to adjust the path.
|
||||||
|
|
||||||
Assumes all indexes have an absolute offset and will raise if not.
|
Assumes all indexes have an absolute offset and will raise if not.
|
||||||
|
|
||||||
:param track:
|
@type track: C{int}
|
||||||
:type track: C{int}
|
@type index: C{int}
|
||||||
:param index:
|
|
||||||
:type index: C{int}
|
|
||||||
:param path:
|
|
||||||
:type path:
|
|
||||||
:param length:
|
|
||||||
:type length:
|
|
||||||
:param counter: (Default value = None)
|
|
||||||
:type counter:
|
|
||||||
"""
|
"""
|
||||||
logger.debug('setFile: track %d, index %d, path %r, '
|
logger.debug('setFile: track %d, index %d, path %r, '
|
||||||
'length %r, counter %r', track, index, path, length,
|
'length %r, counter %r', track, index, path, length,
|
||||||
@@ -701,8 +670,8 @@ class Table(object):
|
|||||||
break
|
break
|
||||||
|
|
||||||
def absolutize(self):
|
def absolutize(self):
|
||||||
"""Calculate absolute offsets on indexes as much as possible.
|
"""
|
||||||
|
Calculate absolute offsets on indexes as much as possible.
|
||||||
Only possible for as long as tracks draw from the same file.
|
Only possible for as long as tracks draw from the same file.
|
||||||
"""
|
"""
|
||||||
t = self.tracks[0].number
|
t = self.tracks[0].number
|
||||||
@@ -739,14 +708,12 @@ class Table(object):
|
|||||||
break
|
break
|
||||||
|
|
||||||
def merge(self, other, session=2):
|
def merge(self, other, session=2):
|
||||||
"""Merge the given table at the end.
|
"""
|
||||||
|
Merges the given table at the end.
|
||||||
|
The other table is assumed to be from an additional session,
|
||||||
|
|
||||||
The other table is assumed to be from an additional session.
|
|
||||||
|
|
||||||
:param other:
|
@type other: L{Table}
|
||||||
:type other: L{Table}
|
|
||||||
:param session: (Default value = 2)
|
|
||||||
:type session:
|
|
||||||
"""
|
"""
|
||||||
gap = self._getSessionGap(session)
|
gap = self._getSessionGap(session)
|
||||||
|
|
||||||
@@ -791,15 +758,14 @@ class Table(object):
|
|||||||
# lookups
|
# lookups
|
||||||
|
|
||||||
def getNextTrackIndex(self, track, index):
|
def getNextTrackIndex(self, track, index):
|
||||||
"""Return the next track and index.
|
"""
|
||||||
|
Return the next track and index.
|
||||||
|
|
||||||
:param track: track number, 1-based.
|
@param track: track number, 1-based
|
||||||
:type track:
|
|
||||||
:param index:
|
@raises IndexError: on last index
|
||||||
:type index:
|
|
||||||
:raises IndexError: on last index.
|
@rtype: tuple of (int, int)
|
||||||
:returns:
|
|
||||||
:rtype: tuple of (int, int)
|
|
||||||
"""
|
"""
|
||||||
t = self.tracks[track - 1]
|
t = self.tracks[track - 1]
|
||||||
indexes = t.indexes.keys()
|
indexes = t.indexes.keys()
|
||||||
@@ -821,9 +787,9 @@ class Table(object):
|
|||||||
# various tests for types of Table
|
# various tests for types of Table
|
||||||
|
|
||||||
def hasTOC(self):
|
def hasTOC(self):
|
||||||
"""Check if the Table has a complete TOC.
|
"""
|
||||||
|
Check if the Table has a complete TOC.
|
||||||
A TOC is a list of all tracks and their Index 01, with absolute
|
a TOC is a list of all tracks and their Index 01, with absolute
|
||||||
offsets, as well as the leadout.
|
offsets, as well as the leadout.
|
||||||
"""
|
"""
|
||||||
if not self.leadout:
|
if not self.leadout:
|
||||||
@@ -841,10 +807,9 @@ class Table(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def accuraterip_ids(self):
|
def accuraterip_ids(self):
|
||||||
"""Compute both AccurateRip disc ids.
|
"""
|
||||||
|
returns both AccurateRip disc ids as a tuple of 8-char
|
||||||
The ids are returned as a tuple of 8-char hexadecimal
|
hexadecimal strings (discid1, discid2)
|
||||||
strings (discid1, discid2).
|
|
||||||
"""
|
"""
|
||||||
# AccurateRip does not take into account data tracks,
|
# AccurateRip does not take into account data tracks,
|
||||||
# but does count the data track to determine the leadout offset
|
# but does count the data track to determine the leadout offset
|
||||||
@@ -877,7 +842,9 @@ class Table(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def canCue(self):
|
def canCue(self):
|
||||||
"""Check if this table can be used to generate a .cue file."""
|
"""
|
||||||
|
Check if this table can be used to generate a .cue file
|
||||||
|
"""
|
||||||
if not self.hasTOC():
|
if not self.hasTOC():
|
||||||
logger.debug('No TOC, cannot cue')
|
logger.debug('No TOC, cannot cue')
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -18,9 +18,10 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with whipper. If not, see <http://www.gnu.org/licenses/>.
|
# along with whipper. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""Reading .toc files.
|
"""
|
||||||
|
Reading .toc files
|
||||||
|
|
||||||
The .toc file format is described in the man page of cdrdao.
|
The .toc file format is described in the man page of cdrdao
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
@@ -92,40 +93,29 @@ _INDEX_RE = re.compile(r"""
|
|||||||
|
|
||||||
|
|
||||||
class Sources:
|
class Sources:
|
||||||
"""I represent the list of sources used in the .toc file.
|
"""
|
||||||
|
I represent the list of sources used in the .toc file.
|
||||||
Each SILENCE and each FILE is a source.
|
Each SILENCE and each FILE is a source.
|
||||||
If the filename for FILE doesn't change, the counter is not increased.
|
If the filename for FILE doesn't change, the counter is not increased.
|
||||||
|
|
||||||
ivar sources:
|
|
||||||
vartype sources:
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._sources = []
|
self._sources = []
|
||||||
|
|
||||||
def append(self, counter, offset, source):
|
def append(self, counter, offset, source):
|
||||||
"""Append tuple containing source information to sources.
|
"""
|
||||||
|
@param counter: the source counter; updates for each different
|
||||||
This method also logs the event.
|
data source (silence or different file path)
|
||||||
|
@type counter: int
|
||||||
:param counter: the source counter; updates for each different
|
@param offset: the absolute disc offset where this source starts
|
||||||
data source (silence or different file path).
|
|
||||||
:type counter: int
|
|
||||||
:param offset: the absolute disc offset where this source starts.
|
|
||||||
:type offset:
|
|
||||||
:param source:
|
|
||||||
:type source:
|
|
||||||
"""
|
"""
|
||||||
logger.debug('Appending source, counter %d, abs offset %d, '
|
logger.debug('Appending source, counter %d, abs offset %d, '
|
||||||
'source %r' % (counter, offset, source))
|
'source %r' % (counter, offset, source))
|
||||||
self._sources.append((counter, offset, source))
|
self._sources.append((counter, offset, source))
|
||||||
|
|
||||||
def get(self, offset):
|
def get(self, offset):
|
||||||
"""Retrieve the source used at the given offset.
|
"""
|
||||||
|
Retrieve the source used at the given offset.
|
||||||
:param offset:
|
|
||||||
:type offset:
|
|
||||||
"""
|
"""
|
||||||
for i, (c, o, s) in enumerate(self._sources):
|
for i, (c, o, s) in enumerate(self._sources):
|
||||||
if offset < o:
|
if offset < o:
|
||||||
@@ -134,10 +124,8 @@ class Sources:
|
|||||||
return self._sources[-1]
|
return self._sources[-1]
|
||||||
|
|
||||||
def getCounterStart(self, counter):
|
def getCounterStart(self, counter):
|
||||||
"""Retrieve the absolute offset of the first source for this counter.
|
"""
|
||||||
|
Retrieve the absolute offset of the first source for this counter
|
||||||
:param counter:
|
|
||||||
:type counter:
|
|
||||||
"""
|
"""
|
||||||
for i, (c, o, s) in enumerate(self._sources):
|
for i, (c, o, s) in enumerate(self._sources):
|
||||||
if c == counter:
|
if c == counter:
|
||||||
@@ -147,21 +135,11 @@ class Sources:
|
|||||||
|
|
||||||
|
|
||||||
class TocFile(object):
|
class TocFile(object):
|
||||||
"""I represent a .toc file.
|
|
||||||
|
|
||||||
:ivar path:
|
|
||||||
:vartype path: unicode
|
|
||||||
:ivar messages:
|
|
||||||
:vartype messages:
|
|
||||||
:ivar table:
|
|
||||||
:vartype table:
|
|
||||||
:ivar logName:
|
|
||||||
:vartype logName:
|
|
||||||
:ivar sources:
|
|
||||||
:vartype sources:
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
|
"""
|
||||||
|
@type path: unicode
|
||||||
|
"""
|
||||||
assert type(path) is unicode, "%r is not unicode" % path
|
assert type(path) is unicode, "%r is not unicode" % path
|
||||||
self._path = path
|
self._path = path
|
||||||
self._messages = []
|
self._messages = []
|
||||||
@@ -400,22 +378,17 @@ class TocFile(object):
|
|||||||
logger.debug('parse: leadout: %r', self.table.leadout)
|
logger.debug('parse: leadout: %r', self.table.leadout)
|
||||||
|
|
||||||
def message(self, number, message):
|
def message(self, number, message):
|
||||||
"""Add a message about a given line in the cue file.
|
"""
|
||||||
|
Add a message about a given line in the cue file.
|
||||||
|
|
||||||
:param number: line number, counting from 0.
|
@param number: line number, counting from 0.
|
||||||
:type number:
|
|
||||||
:param message:
|
|
||||||
:type message:
|
|
||||||
"""
|
"""
|
||||||
self._messages.append((number + 1, message))
|
self._messages.append((number + 1, message))
|
||||||
|
|
||||||
def getTrackLength(self, track):
|
def getTrackLength(self, track):
|
||||||
"""Compute the length of the given track.
|
"""
|
||||||
|
Returns the length of the given track, from its INDEX 01 to the next
|
||||||
The lenght is computed from its INDEX 01 to the next track's INDEX 01.
|
track's INDEX 01
|
||||||
|
|
||||||
:param track:
|
|
||||||
:type track:
|
|
||||||
"""
|
"""
|
||||||
# returns track length in frames, or -1 if can't be determined and
|
# returns track length in frames, or -1 if can't be determined and
|
||||||
# complete file should be assumed
|
# complete file should be assumed
|
||||||
@@ -437,26 +410,26 @@ class TocFile(object):
|
|||||||
return -1
|
return -1
|
||||||
|
|
||||||
def getRealPath(self, path):
|
def getRealPath(self, path):
|
||||||
"""Translate the .toc's FILE to an existing path.
|
"""
|
||||||
|
Translate the .toc's FILE to an existing path.
|
||||||
|
|
||||||
:param path:
|
@type path: unicode
|
||||||
:type path: unicode
|
|
||||||
"""
|
"""
|
||||||
return common.getRealPath(self._path, path)
|
return common.getRealPath(self._path, path)
|
||||||
|
|
||||||
|
|
||||||
class File:
|
class File:
|
||||||
"""I represent a FILE line in a .toc file.
|
"""
|
||||||
|
I represent a FILE line in a .toc file.
|
||||||
:ivar path:
|
|
||||||
:vartype path: C{unicode}
|
|
||||||
:ivar start: starting point for the track in this file, in frames.
|
|
||||||
:vartype start: C{int}
|
|
||||||
:ivar length: length for the track in this file, in frames.
|
|
||||||
:vartype length:
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, path, start, length):
|
def __init__(self, path, start, length):
|
||||||
|
"""
|
||||||
|
@type path: C{unicode}
|
||||||
|
@type start: C{int}
|
||||||
|
@param start: starting point for the track in this file, in frames
|
||||||
|
@param length: length for the track in this file, in frames
|
||||||
|
"""
|
||||||
assert type(path) is unicode, "%r is not unicode" % path
|
assert type(path) is unicode, "%r is not unicode" % path
|
||||||
|
|
||||||
self.path = path
|
self.path = path
|
||||||
|
|||||||
@@ -37,10 +37,13 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class FileSizeError(Exception):
|
class FileSizeError(Exception):
|
||||||
"""The given path does not have the expected size."""
|
|
||||||
|
|
||||||
message = None
|
message = None
|
||||||
|
|
||||||
|
"""
|
||||||
|
The given path does not have the expected size.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, path, message):
|
def __init__(self, path, message):
|
||||||
self.args = (path, message)
|
self.args = (path, message)
|
||||||
self.path = path
|
self.path = path
|
||||||
@@ -48,7 +51,9 @@ class FileSizeError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class ReturnCodeError(Exception):
|
class ReturnCodeError(Exception):
|
||||||
"""The program had a non-zero return code."""
|
"""
|
||||||
|
The program had a non-zero return code.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, returncode):
|
def __init__(self, returncode):
|
||||||
self.args = (returncode, )
|
self.args = (returncode, )
|
||||||
@@ -74,22 +79,6 @@ _ERROR_RE = re.compile("^scsi_read error:")
|
|||||||
|
|
||||||
|
|
||||||
class ProgressParser:
|
class ProgressParser:
|
||||||
"""Parse cdparanoia's output information.
|
|
||||||
|
|
||||||
:cvar read:
|
|
||||||
:vartype read:
|
|
||||||
:cvar wrote:
|
|
||||||
:vartype wrote:
|
|
||||||
:cvar errors:
|
|
||||||
:vartype errors:
|
|
||||||
:ivar reads:
|
|
||||||
:vartype reads:
|
|
||||||
:ivar start: first frame to rip.
|
|
||||||
:vartype start: int
|
|
||||||
:ivar stop: last frame to rip (inclusive).
|
|
||||||
:vartype stop: int
|
|
||||||
"""
|
|
||||||
|
|
||||||
read = 0 # last [read] frame
|
read = 0 # last [read] frame
|
||||||
wrote = 0 # last [wrote] frame
|
wrote = 0 # last [wrote] frame
|
||||||
errors = 0 # count of number of scsi errors
|
errors = 0 # count of number of scsi errors
|
||||||
@@ -98,6 +87,12 @@ class ProgressParser:
|
|||||||
reads = 0 # total number of reads
|
reads = 0 # total number of reads
|
||||||
|
|
||||||
def __init__(self, start, stop):
|
def __init__(self, start, stop):
|
||||||
|
"""
|
||||||
|
@param start: first frame to rip
|
||||||
|
@type start: int
|
||||||
|
@param stop: last frame to rip (inclusive)
|
||||||
|
@type stop: int
|
||||||
|
"""
|
||||||
self.start = start
|
self.start = start
|
||||||
self.stop = stop
|
self.stop = stop
|
||||||
|
|
||||||
@@ -107,10 +102,8 @@ class ProgressParser:
|
|||||||
self._reads = {} # read count for each sector
|
self._reads = {} # read count for each sector
|
||||||
|
|
||||||
def parse(self, line):
|
def parse(self, line):
|
||||||
"""Parse a line.
|
"""
|
||||||
|
Parse a line.
|
||||||
:param line:
|
|
||||||
:type line:
|
|
||||||
"""
|
"""
|
||||||
m = _PROGRESS_RE.search(line)
|
m = _PROGRESS_RE.search(line)
|
||||||
if m:
|
if m:
|
||||||
@@ -191,12 +184,9 @@ class ProgressParser:
|
|||||||
self.wrote = frameOffset
|
self.wrote = frameOffset
|
||||||
|
|
||||||
def getTrackQuality(self):
|
def getTrackQuality(self):
|
||||||
"""Each frame gets read twice.
|
"""
|
||||||
|
Each frame gets read twice.
|
||||||
More than two reads for a frame reduce track quality.
|
More than two reads for a frame reduce track quality.
|
||||||
|
|
||||||
:returns:
|
|
||||||
:rtype: float or int
|
|
||||||
"""
|
"""
|
||||||
frames = self.stop - self.start + 1 # + 1 since stop is inclusive
|
frames = self.stop - self.start + 1 # + 1 since stop is inclusive
|
||||||
reads = self.reads
|
reads = self.reads
|
||||||
@@ -213,35 +203,10 @@ class ProgressParser:
|
|||||||
# FIXME: handle errors
|
# FIXME: handle errors
|
||||||
|
|
||||||
class ReadTrackTask(task.Task):
|
class ReadTrackTask(task.Task):
|
||||||
"""I am a task that reads a track using cdparanoia.
|
"""
|
||||||
|
I am a task that reads a track using cdparanoia.
|
||||||
|
|
||||||
:cvar description:
|
@ivar reads: how many reads were done to rip the track
|
||||||
:vartype description:
|
|
||||||
:cvar quality:
|
|
||||||
:cvar speed:
|
|
||||||
:cvar duration:
|
|
||||||
:ivar path: where to store the ripped track.
|
|
||||||
:vartype path: unicode
|
|
||||||
:ivar table: table of contents of CD.
|
|
||||||
:vartype table: L{table.Table}
|
|
||||||
:ivar start: first frame to rip.
|
|
||||||
:vartype start: int
|
|
||||||
:ivar stop: last frame to rip (inclusive); >= start.
|
|
||||||
:vartype stop: int
|
|
||||||
:ivar offset: read offset, in samples.
|
|
||||||
:vartype offset: int
|
|
||||||
:ivar parser:
|
|
||||||
:vartype parser:
|
|
||||||
:ivar device: the device to rip from.
|
|
||||||
:vartype device: str
|
|
||||||
:ivar start_time:
|
|
||||||
:vartype start_time:
|
|
||||||
:ivar overread:
|
|
||||||
:vartype overread:
|
|
||||||
:ivar buffer:
|
|
||||||
:vartype buffer:
|
|
||||||
:ivar errors:
|
|
||||||
:vartype errors:
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
description = "Reading track"
|
description = "Reading track"
|
||||||
@@ -253,6 +218,26 @@ class ReadTrackTask(task.Task):
|
|||||||
|
|
||||||
def __init__(self, path, table, start, stop, overread, offset=0,
|
def __init__(self, path, table, start, stop, overread, offset=0,
|
||||||
device=None, action="Reading", what="track"):
|
device=None, action="Reading", what="track"):
|
||||||
|
"""
|
||||||
|
Read the given track.
|
||||||
|
|
||||||
|
@param path: where to store the ripped track
|
||||||
|
@type path: unicode
|
||||||
|
@param table: table of contents of CD
|
||||||
|
@type table: L{table.Table}
|
||||||
|
@param start: first frame to rip
|
||||||
|
@type start: int
|
||||||
|
@param stop: last frame to rip (inclusive); >= start
|
||||||
|
@type stop: int
|
||||||
|
@param offset: read offset, in samples
|
||||||
|
@type offset: int
|
||||||
|
@param device: the device to rip from
|
||||||
|
@type device: str
|
||||||
|
@param action: a string representing the action; e.g. Read/Verify
|
||||||
|
@type action: str
|
||||||
|
@param what: a string representing what's being read; e.g. Track
|
||||||
|
@type what: str
|
||||||
|
"""
|
||||||
assert type(path) is unicode, "%r is not unicode" % path
|
assert type(path) is unicode, "%r is not unicode" % path
|
||||||
|
|
||||||
self.path = path
|
self.path = path
|
||||||
@@ -314,7 +299,7 @@ class ReadTrackTask(task.Task):
|
|||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
close_fds=True)
|
close_fds=True)
|
||||||
except OSError as e:
|
except OSError, e:
|
||||||
import errno
|
import errno
|
||||||
if e.errno == errno.ENOENT:
|
if e.errno == errno.ENOENT:
|
||||||
raise common.MissingDependencyException('cd-paranoia')
|
raise common.MissingDependencyException('cd-paranoia')
|
||||||
@@ -413,45 +398,24 @@ class ReadTrackTask(task.Task):
|
|||||||
|
|
||||||
|
|
||||||
class ReadVerifyTrackTask(task.MultiSeparateTask):
|
class ReadVerifyTrackTask(task.MultiSeparateTask):
|
||||||
"""I am a task that reads and verifies a track using cdparanoia.
|
"""
|
||||||
|
I am a task that reads and verifies a track using cdparanoia.
|
||||||
I also encode the track.
|
I also encode the track.
|
||||||
|
|
||||||
The path where the file is stored can be changed if necessary, for
|
The path where the file is stored can be changed if necessary, for
|
||||||
example if the file name is too long.
|
example if the file name is too long.
|
||||||
|
|
||||||
:cvar checksum: the checksum of the track; set if they match.
|
@ivar path: the path where the file is to be stored.
|
||||||
:vartype checksum:
|
@ivar checksum: the checksum of the track; set if they match.
|
||||||
:cvar testchecksum: the test checksum of the track.
|
@ivar testchecksum: the test checksum of the track.
|
||||||
:vartype testchecksum:
|
@ivar copychecksum: the copy checksum of the track.
|
||||||
:cvar copychecksum: the copy checksum of the track.
|
@ivar testspeed: the test speed of the track, as a multiple of
|
||||||
:vartype copychecksum:
|
track duration.
|
||||||
:cvar peak: the peak level of the track
|
@ivar copyspeed: the copy speed of the track, as a multiple of
|
||||||
:vartype peak:
|
track duration.
|
||||||
:cvar quality:
|
@ivar testduration: the test duration of the track, in seconds.
|
||||||
:vartype quality:
|
@ivar copyduration: the copy duration of the track, in seconds.
|
||||||
:cvar testspeed: the test speed of the track, as a multiple of
|
@ivar peak: the peak level of the track
|
||||||
track duration.
|
|
||||||
:vartype testspeed:
|
|
||||||
:cvar copyspeed: the copy speed of the track, as a multiple of
|
|
||||||
track duration.
|
|
||||||
:vartype copyspeed:
|
|
||||||
:cvar testduration: the test duration of the track, in seconds.
|
|
||||||
:vartype testduration:
|
|
||||||
:cvar copyduration: the copy duration of the track, in seconds.
|
|
||||||
:vartype copyduration:
|
|
||||||
:ivar path: the path where the file is to be stored.
|
|
||||||
:vartype path: str
|
|
||||||
:ivar table: table of contents of CD.
|
|
||||||
:vartype table: L{table.Table}
|
|
||||||
:ivar stop: last frame to rip (inclusive).
|
|
||||||
:vartype stop: int
|
|
||||||
:ivar offset: read offset, in samples.
|
|
||||||
:vartype offset: int
|
|
||||||
:ivar device: the device to rip from.
|
|
||||||
:vartype device: str
|
|
||||||
:ivar taglist: a dict of tags.
|
|
||||||
:vartype taglist: dict
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
checksum = None
|
checksum = None
|
||||||
@@ -469,6 +433,22 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
|
|||||||
|
|
||||||
def __init__(self, path, table, start, stop, overread, offset=0,
|
def __init__(self, path, table, start, stop, overread, offset=0,
|
||||||
device=None, taglist=None, what="track"):
|
device=None, taglist=None, what="track"):
|
||||||
|
"""
|
||||||
|
@param path: where to store the ripped track
|
||||||
|
@type path: str
|
||||||
|
@param table: table of contents of CD
|
||||||
|
@type table: L{table.Table}
|
||||||
|
@param start: first frame to rip
|
||||||
|
@type start: int
|
||||||
|
@param stop: last frame to rip (inclusive)
|
||||||
|
@type stop: int
|
||||||
|
@param offset: read offset, in samples
|
||||||
|
@type offset: int
|
||||||
|
@param device: the device to rip from
|
||||||
|
@type device: str
|
||||||
|
@param taglist: a dict of tags
|
||||||
|
@type taglist: dict
|
||||||
|
"""
|
||||||
task.MultiSeparateTask.__init__(self)
|
task.MultiSeparateTask.__init__(self)
|
||||||
|
|
||||||
logger.debug('Creating read and verify task on %r', path)
|
logger.debug('Creating read and verify task on %r', path)
|
||||||
@@ -498,7 +478,7 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
|
|||||||
try:
|
try:
|
||||||
tmpoutpath = path + u'.part'
|
tmpoutpath = path + u'.part'
|
||||||
open(tmpoutpath, 'wb').close()
|
open(tmpoutpath, 'wb').close()
|
||||||
except IOError as e:
|
except IOError, e:
|
||||||
if errno.ENAMETOOLONG != e.errno:
|
if errno.ENAMETOOLONG != e.errno:
|
||||||
raise
|
raise
|
||||||
path = common.shrinkPath(path)
|
path = common.shrinkPath(path)
|
||||||
@@ -560,7 +540,7 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
|
|||||||
try:
|
try:
|
||||||
logger.debug('Moving to final path %r', self.path)
|
logger.debug('Moving to final path %r', self.path)
|
||||||
os.rename(self._tmppath, self.path)
|
os.rename(self._tmppath, self.path)
|
||||||
except Exception as e:
|
except Exception, e:
|
||||||
logger.debug('Exception while moving to final '
|
logger.debug('Exception while moving to final '
|
||||||
'path %r: %r', self.path, str(e))
|
'path %r: %r', self.path, str(e))
|
||||||
self.exception = e
|
self.exception = e
|
||||||
@@ -568,7 +548,7 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
|
|||||||
os.unlink(self._tmppath)
|
os.unlink(self._tmppath)
|
||||||
else:
|
else:
|
||||||
logger.debug('stop: exception %r', self.exception)
|
logger.debug('stop: exception %r', self.exception)
|
||||||
except Exception as e:
|
except Exception, e:
|
||||||
print 'WARNING: unhandled exception %r' % (e, )
|
print 'WARNING: unhandled exception %r' % (e, )
|
||||||
|
|
||||||
task.MultiSeparateTask.stop(self)
|
task.MultiSeparateTask.stop(self)
|
||||||
|
|||||||
@@ -13,14 +13,8 @@ CDRDAO = 'cdrdao'
|
|||||||
|
|
||||||
|
|
||||||
def read_toc(device, fast_toc=False):
|
def read_toc(device, fast_toc=False):
|
||||||
"""Get the cd's toc using cdrdao and parse it.
|
"""
|
||||||
|
Return cdrdao-generated table of contents for 'device'.
|
||||||
:param device: optical disk drive.
|
|
||||||
:type device:
|
|
||||||
:param fast_toc: enable cdrdao's fast-toc option? (Default value = False)
|
|
||||||
:type fast_toc: bool
|
|
||||||
:returns:
|
|
||||||
:rtype: TocFile
|
|
||||||
"""
|
"""
|
||||||
# cdrdao MUST be passed a non-existing filename as its last argument
|
# 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
|
# to write the TOC to; it does not support writing to stdout or
|
||||||
@@ -54,12 +48,8 @@ def read_toc(device, fast_toc=False):
|
|||||||
|
|
||||||
|
|
||||||
def DetectCdr(device):
|
def DetectCdr(device):
|
||||||
"""Check if inserted disk is a CD-R.
|
"""
|
||||||
|
Return whether cdrdao detects a CD-R for 'device'.
|
||||||
:param device: optical disk drive.
|
|
||||||
:type device:
|
|
||||||
:returns: False if inserted disk is not a CD-R, True otherwise.
|
|
||||||
:rtype: bool
|
|
||||||
"""
|
"""
|
||||||
cmd = [CDRDAO, 'disk-info', '-v1', '--device', device]
|
cmd = [CDRDAO, 'disk-info', '-v1', '--device', device]
|
||||||
logger.debug("executing %r", cmd)
|
logger.debug("executing %r", cmd)
|
||||||
@@ -71,10 +61,8 @@ def DetectCdr(device):
|
|||||||
|
|
||||||
|
|
||||||
def version():
|
def version():
|
||||||
"""Detect cdrdao's version.
|
"""
|
||||||
|
Return cdrdao version as a string.
|
||||||
:returns:
|
|
||||||
:rtype:
|
|
||||||
"""
|
"""
|
||||||
cdrdao = Popen(CDRDAO, stderr=PIPE)
|
cdrdao = Popen(CDRDAO, stderr=PIPE)
|
||||||
out, err = cdrdao.communicate()
|
out, err = cdrdao.communicate()
|
||||||
@@ -92,31 +80,21 @@ def version():
|
|||||||
|
|
||||||
|
|
||||||
def ReadTOCTask(device):
|
def ReadTOCTask(device):
|
||||||
"""Stopgap morituri-insanity compatibility layer.
|
"""
|
||||||
|
stopgap morituri-insanity compatibility layer
|
||||||
:param device: optical disk drive.
|
|
||||||
:type device:
|
|
||||||
:returns:
|
|
||||||
:rtype: TocFile
|
|
||||||
"""
|
"""
|
||||||
return read_toc(device, fast_toc=True)
|
return read_toc(device, fast_toc=True)
|
||||||
|
|
||||||
|
|
||||||
def ReadTableTask(device):
|
def ReadTableTask(device):
|
||||||
"""Stopgap morituri-insanity compatibility layer.
|
"""
|
||||||
|
stopgap morituri-insanity compatibility layer
|
||||||
:param device: optical disk drive.
|
|
||||||
:type device:
|
|
||||||
:returns:
|
|
||||||
:rtype: TocFile
|
|
||||||
"""
|
"""
|
||||||
return read_toc(device)
|
return read_toc(device)
|
||||||
|
|
||||||
|
|
||||||
def getCDRDAOVersion():
|
def getCDRDAOVersion():
|
||||||
"""Stopgap morituri-insanity compatibility layer.
|
"""
|
||||||
|
stopgap morituri-insanity compatibility layer
|
||||||
:returns:
|
|
||||||
:rtype:
|
|
||||||
"""
|
"""
|
||||||
return version()
|
return version()
|
||||||
|
|||||||
@@ -5,15 +5,9 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def encode(infile, outfile):
|
def encode(infile, outfile):
|
||||||
"""Encode infile to outfile, with flac.
|
"""
|
||||||
|
Encodes infile to outfile, with flac.
|
||||||
Uses ``-f`` because whipper already creates the file.
|
Uses '-f' because whipper already creates the file.
|
||||||
|
|
||||||
:param infile: full path to input audio track.
|
|
||||||
:type infile: str
|
|
||||||
:param outfile: full path to output audio track.
|
|
||||||
:type outfile: str
|
|
||||||
:raises CalledProcessError: if the flac encoder returns non-zero.
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# TODO: Replace with Popen so that we can catch stderr and write it to
|
# TODO: Replace with Popen so that we can catch stderr and write it to
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ SOX = 'sox'
|
|||||||
|
|
||||||
|
|
||||||
def peak_level(track_path):
|
def peak_level(track_path):
|
||||||
"""Accept a path to a sox-decodable audio file.
|
"""
|
||||||
|
Accepts a path to a sox-decodable audio file.
|
||||||
|
|
||||||
:param track_path: full path to audio track.
|
Returns track peak level from sox ('maximum amplitude') as a float.
|
||||||
:type track_path: str
|
Returns None on error.
|
||||||
:returns: track peak absolute value from sox or None on error.
|
|
||||||
:rtype: int or None
|
|
||||||
"""
|
"""
|
||||||
if not os.path.exists(track_path):
|
if not os.path.exists(track_path):
|
||||||
logger.warning("SoX peak detection failed: file not found")
|
logger.warning("SoX peak detection failed: file not found")
|
||||||
|
|||||||
@@ -10,29 +10,19 @@ SOXI = 'soxi'
|
|||||||
|
|
||||||
|
|
||||||
class AudioLengthTask(ctask.PopenTask):
|
class AudioLengthTask(ctask.PopenTask):
|
||||||
"""Calculate the length of a track in audio samples.
|
|
||||||
|
|
||||||
:cvar logCategory:
|
|
||||||
:vartype logCategory:
|
|
||||||
:cvar description:
|
|
||||||
:vartype description:
|
|
||||||
:cvar length: length of the decoded audio file, in audio samples.
|
|
||||||
:vartype length: int or None
|
|
||||||
:ivar logName:
|
|
||||||
:vartype logName:
|
|
||||||
:ivar command:
|
|
||||||
:vartype command:
|
|
||||||
:ivar error:
|
|
||||||
:vartype error:
|
|
||||||
:ivar output:
|
|
||||||
:vartype output:
|
|
||||||
"""
|
"""
|
||||||
|
I calculate the length of a track in audio samples.
|
||||||
|
|
||||||
|
@ivar length: length of the decoded audio file, in audio samples.
|
||||||
|
"""
|
||||||
logCategory = 'AudioLengthTask'
|
logCategory = 'AudioLengthTask'
|
||||||
description = 'Getting length of audio track'
|
description = 'Getting length of audio track'
|
||||||
length = None
|
length = None
|
||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
|
"""
|
||||||
|
@type path: unicode
|
||||||
|
"""
|
||||||
assert type(path) is unicode, "%r is not unicode" % path
|
assert type(path) is unicode, "%r is not unicode" % path
|
||||||
|
|
||||||
self.logName = os.path.basename(path).encode('utf-8')
|
self.logName = os.path.basename(path).encode('utf-8')
|
||||||
|
|||||||
@@ -5,34 +5,27 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def eject_device(device):
|
def eject_device(device):
|
||||||
"""Eject the given device.
|
"""
|
||||||
|
Eject the given device.
|
||||||
:param device: optical disk drive.
|
|
||||||
:type device:
|
|
||||||
"""
|
"""
|
||||||
logger.debug("ejecting device %s", device)
|
logger.debug("ejecting device %s", device)
|
||||||
os.system('eject %s' % device)
|
os.system('eject %s' % device)
|
||||||
|
|
||||||
|
|
||||||
def load_device(device):
|
def load_device(device):
|
||||||
"""Load the given device.
|
"""
|
||||||
|
Load the given device.
|
||||||
:param device: optical disk drive.
|
|
||||||
:type device:
|
|
||||||
"""
|
"""
|
||||||
logger.debug("loading (eject -t) device %s", device)
|
logger.debug("loading (eject -t) device %s", device)
|
||||||
os.system('eject -t %s' % device)
|
os.system('eject -t %s' % device)
|
||||||
|
|
||||||
|
|
||||||
def unmount_device(device):
|
def unmount_device(device):
|
||||||
"""Unmount the given device if it is mounted.
|
"""
|
||||||
|
Unmount the given device if it is mounted, as happens with automounted
|
||||||
Data tracks are usually automounted.
|
data tracks.
|
||||||
|
|
||||||
If the given device is a symlink, the target will be checked.
|
If the given device is a symlink, the target will be checked.
|
||||||
|
|
||||||
:param device: optical disk drive.
|
|
||||||
:type device:
|
|
||||||
"""
|
"""
|
||||||
device = os.path.realpath(device)
|
device = os.path.realpath(device)
|
||||||
logger.debug('possibly unmount real path %r' % device)
|
logger.debug('possibly unmount real path %r' % device)
|
||||||
|
|||||||
@@ -14,28 +14,14 @@ class WhipperLogger(result.Logger):
|
|||||||
_errors = False
|
_errors = False
|
||||||
|
|
||||||
def log(self, ripResult, epoch=time.time()):
|
def log(self, ripResult, epoch=time.time()):
|
||||||
"""Join all logfile lines in a single str.
|
"""Returns big str: logfile joined text lines"""
|
||||||
|
|
||||||
:param ripResult:
|
|
||||||
:type ripResult:
|
|
||||||
:param epoch: rip time since the epoch (Default value = time.time()).
|
|
||||||
:type epoch: float
|
|
||||||
:returns: logfile report
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
lines = self.logRip(ripResult, epoch=epoch)
|
lines = self.logRip(ripResult, epoch=epoch)
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def logRip(self, ripResult, epoch):
|
def logRip(self, ripResult, epoch):
|
||||||
"""Generate logfile as a list of str lines.
|
"""Returns logfile lines list"""
|
||||||
|
|
||||||
:param ripResult:
|
|
||||||
:type ripResult:
|
|
||||||
:param epoch: rip time since the epoch.
|
|
||||||
:type epoch: float
|
|
||||||
:returns:
|
|
||||||
:rtype: str
|
|
||||||
"""
|
|
||||||
lines = []
|
lines = []
|
||||||
|
|
||||||
# Ripper version
|
# Ripper version
|
||||||
@@ -170,15 +156,8 @@ class WhipperLogger(result.Logger):
|
|||||||
return lines
|
return lines
|
||||||
|
|
||||||
def trackLog(self, trackResult):
|
def trackLog(self, trackResult):
|
||||||
"""Generate tracks section lines.
|
"""Returns Tracks section lines: data picked from trackResult"""
|
||||||
|
|
||||||
Tracks information are mostly taken from ``trackResult``.
|
|
||||||
|
|
||||||
:param trackResult:
|
|
||||||
:type trackResult:
|
|
||||||
:returns: tracks section lines.
|
|
||||||
:rtype: list
|
|
||||||
"""
|
|
||||||
lines = []
|
lines = []
|
||||||
|
|
||||||
# Track number
|
# Track number
|
||||||
|
|||||||
@@ -23,15 +23,6 @@ import time
|
|||||||
|
|
||||||
|
|
||||||
class TrackResult:
|
class TrackResult:
|
||||||
"""I hold information about the rip result of a track.
|
|
||||||
|
|
||||||
* *CRC:* calculated 4 byte AccurateRip CRC.
|
|
||||||
* *DBCRC:* 4 byte AccurateRip CRC from the AR database.
|
|
||||||
* *DBConfidence:* confidence for the matched AccurateRip DB CRC.
|
|
||||||
* *DBMaxConfidence:* track's maximum confidence in the AccurateRip DB.
|
|
||||||
* *DBMaxConfidenceCRC:* maximum confidence CRC.
|
|
||||||
"""
|
|
||||||
|
|
||||||
number = None
|
number = None
|
||||||
filename = None
|
filename = None
|
||||||
pregap = 0 # in frames
|
pregap = 0 # in frames
|
||||||
@@ -49,6 +40,14 @@ class TrackResult:
|
|||||||
classVersion = 3
|
classVersion = 3
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
"""
|
||||||
|
CRC: calculated 4 byte AccurateRip CRC
|
||||||
|
DBCRC: 4 byte AccurateRip CRC from the AR database
|
||||||
|
DBConfidence: confidence for the matched AccurateRip DB CRC
|
||||||
|
|
||||||
|
DBMaxConfidence: track's maximum confidence in the AccurateRip DB
|
||||||
|
DBMaxConfidenceCRC: maximum confidence CRC
|
||||||
|
"""
|
||||||
self.AR = {
|
self.AR = {
|
||||||
'v1': {
|
'v1': {
|
||||||
'CRC': None,
|
'CRC': None,
|
||||||
@@ -66,24 +65,20 @@ class TrackResult:
|
|||||||
|
|
||||||
|
|
||||||
class RipResult:
|
class RipResult:
|
||||||
"""I hold information about the result for rips.
|
"""
|
||||||
|
I hold information about the result for rips.
|
||||||
I can be used to write log files.
|
I can be used to write log files.
|
||||||
|
|
||||||
:cvar offset: sample read offset.
|
@ivar offset: sample read offset
|
||||||
:vartype offset:
|
@ivar table: the full index table
|
||||||
:cvar table: the full index table.
|
@type table: L{whipper.image.table.Table}
|
||||||
:vartype table: L{whipper.image.table.Table}
|
|
||||||
:cvar vendor: vendor of the CD drive.
|
@ivar vendor: vendor of the CD drive
|
||||||
:vartype vendor:
|
@ivar model: model of the CD drive
|
||||||
:cvar model: model of the CD drive.
|
@ivar release: release of the CD drive
|
||||||
:vartype model:
|
|
||||||
:cvar release: release of the CD drive.
|
@ivar cdrdaoVersion: version of cdrdao used for the rip
|
||||||
:vartype release:
|
@ivar cdparanoiaVersion: version of cdparanoia used for the rip
|
||||||
:cvar cdrdaoVersion: version of cdrdao used for the rip.
|
|
||||||
:vartype cdrdaoVersion:
|
|
||||||
:cvar cdparanoiaVersion: version of cdparanoia used for the rip.
|
|
||||||
:vartype cdparanoiaVersion:
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
offset = 0
|
offset = 0
|
||||||
@@ -108,10 +103,11 @@ class RipResult:
|
|||||||
self.tracks = []
|
self.tracks = []
|
||||||
|
|
||||||
def getTrackResult(self, number):
|
def getTrackResult(self, number):
|
||||||
"""Get the TrackResult for the given track number.
|
"""
|
||||||
|
@param number: the track number (0 for HTOA)
|
||||||
|
|
||||||
:param number: the track number (0 for HTOA)
|
@type number: int
|
||||||
:type number: int
|
@rtype: L{TrackResult}
|
||||||
"""
|
"""
|
||||||
for t in self.tracks:
|
for t in self.tracks:
|
||||||
if t.number == number:
|
if t.number == number:
|
||||||
@@ -121,16 +117,19 @@ class RipResult:
|
|||||||
|
|
||||||
|
|
||||||
class Logger(object):
|
class Logger(object):
|
||||||
"""I log the result of a rip."""
|
"""
|
||||||
|
I log the result of a rip.
|
||||||
|
"""
|
||||||
|
|
||||||
def log(self, ripResult, epoch=time.time()):
|
def log(self, ripResult, epoch=time.time()):
|
||||||
"""Create a log from the given ripresult.
|
"""
|
||||||
|
Create a log from the given ripresult.
|
||||||
|
|
||||||
:param epoch: when the log file gets generated
|
@param epoch: when the log file gets generated
|
||||||
(Default value = time.time())
|
@type epoch: float
|
||||||
:type epoch: float
|
@type ripResult: L{RipResult}
|
||||||
:param ripResult:
|
|
||||||
:type ripResult:
|
@rtype: str
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -147,7 +146,11 @@ class EntryPoint(object):
|
|||||||
|
|
||||||
|
|
||||||
def getLoggers():
|
def getLoggers():
|
||||||
"""Get all logger plugins with entry point ``whipper.logger``."""
|
"""
|
||||||
|
Get all logger plugins with entry point 'whipper.logger'.
|
||||||
|
|
||||||
|
@rtype: dict of C{str} -> C{Logger}
|
||||||
|
"""
|
||||||
d = {}
|
d = {}
|
||||||
|
|
||||||
pluggables = list(pkg_resources.iter_entry_points("whipper.logger"))
|
pluggables = list(pkg_resources.iter_entry_points("whipper.logger"))
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ class TestCase(unittest.TestCase):
|
|||||||
def failUnlessRaises(self, exception, f, *args, **kwargs):
|
def failUnlessRaises(self, exception, f, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
result = f(*args, **kwargs)
|
result = f(*args, **kwargs)
|
||||||
except exception as inst:
|
except exception, inst:
|
||||||
return inst
|
return inst
|
||||||
except exception as e:
|
except exception, e:
|
||||||
raise Exception('%s raised instead of %s:\n %s' %
|
raise Exception('%s raised instead of %s:\n %s' %
|
||||||
(sys.exec_info()[0], exception.__name__, str(e))
|
(sys.exec_info()[0], exception.__name__, str(e))
|
||||||
)
|
)
|
||||||
@@ -63,14 +63,9 @@ class TestCase(unittest.TestCase):
|
|||||||
assertRaises = failUnlessRaises
|
assertRaises = failUnlessRaises
|
||||||
|
|
||||||
def readCue(self, name):
|
def readCue(self, name):
|
||||||
"""Read a cue file and replace its version number with the current one.
|
"""
|
||||||
|
Read a .cue file, and replace the version comment with the current
|
||||||
The version number is replaced to use the cue file in comparisons.
|
version so we can use it in comparisons.
|
||||||
|
|
||||||
:param name: cuesheet filename (without path).
|
|
||||||
:type name: str
|
|
||||||
:returns: the cuesheet with version number replaced.
|
|
||||||
:rtype: str
|
|
||||||
"""
|
"""
|
||||||
cuefile = os.path.join(os.path.dirname(__file__), name)
|
cuefile = os.path.join(os.path.dirname(__file__), name)
|
||||||
ret = open(cuefile).read().decode('utf-8')
|
ret = open(cuefile).read().decode('utf-8')
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class TestVerifyResult(TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.result = RipResult()
|
self.result = RipResult()
|
||||||
for n in range(1, 2 + 1):
|
for n in range(1, 2+1):
|
||||||
track = TrackResult()
|
track = TrackResult()
|
||||||
track.number = n
|
track.number = n
|
||||||
self.result.tracks.append(track)
|
self.result.tracks.append(track)
|
||||||
|
|||||||
@@ -116,9 +116,11 @@ class MetadataTestCase(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def testNorthernGateway(self):
|
def testNorthernGateway(self):
|
||||||
"""Check MBz metadata for artists tagged with: [unknown] / an alias.
|
"""
|
||||||
|
check the received metadata for artists tagged with [unknown]
|
||||||
|
and artists tagged with an alias in MusicBrainz
|
||||||
|
|
||||||
.. seealso:: https://github.com/JoeLametta/whipper/issues/156
|
see https://github.com/JoeLametta/whipper/issues/155
|
||||||
"""
|
"""
|
||||||
filename = 'whipper.release.38b05c7d-65fe-4dc0-9c10-33a391b86703.json'
|
filename = 'whipper.release.38b05c7d-65fe-4dc0-9c10-33a391b86703.json'
|
||||||
path = os.path.join(os.path.dirname(__file__), filename)
|
path = os.path.join(os.path.dirname(__file__), filename)
|
||||||
@@ -155,12 +157,9 @@ class MetadataTestCase(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def testNenaAndKimWildSingle(self):
|
def testNenaAndKimWildSingle(self):
|
||||||
"""Check MBz metadata for artists with different names.
|
"""
|
||||||
|
check the received metadata for artists that differ between
|
||||||
An artist can have different names on MusicBrainz like:
|
named on release and named in recording
|
||||||
*artist in MusicBrainz*, *artist as credited*.
|
|
||||||
|
|
||||||
.. seealso:: https://github.com/JoeLametta/whipper/issues/156
|
|
||||||
"""
|
"""
|
||||||
filename = 'whipper.release.f484a9fc-db21-4106-9408-bcd105c90047.json'
|
filename = 'whipper.release.f484a9fc-db21-4106-9408-bcd105c90047.json'
|
||||||
path = os.path.join(os.path.dirname(__file__), filename)
|
path = os.path.join(os.path.dirname(__file__), filename)
|
||||||
|
|||||||
Reference in New Issue
Block a user