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