Revert "Convert docstrings to reStructuredText"
This reverts commit 3b1bd242d0.
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user