Merge pull request #389 from whipper-team/bugfix/improve-docstrings
Improve docstrings
This commit is contained in:
@@ -29,25 +29,28 @@ class BaseCommand:
|
||||
"""
|
||||
Register and handle whipper command arguments with ArgumentParser.
|
||||
|
||||
Register arguments by overriding `add_arguments()` and modifying
|
||||
`self.parser`. Option defaults are read from the dot-separated
|
||||
`prog_name` section of the config file (e.g., 'whipper cd rip'
|
||||
options are read from '[whipper.cd.rip]'). Runs
|
||||
`argparse.parse_args()` then calls `handle_arguments()`.
|
||||
Register arguments by overriding ``add_arguments()`` and modifying
|
||||
``self.parser``. Option defaults are read from the dot-separated
|
||||
``prog_name`` section of the config file (e.g., ``whipper cd rip``
|
||||
options are read from ``[whipper.cd.rip]``). Runs
|
||||
``argparse.parse_args()`` then calls ``handle_arguments()``.
|
||||
|
||||
Provides self.epilog() formatting command for argparse.
|
||||
Provides ``self.epilog()`` formatting command for argparse.
|
||||
|
||||
device_option = True adds -d / --device option to current command
|
||||
no_add_help = True removes -h / --help option from current command
|
||||
Overriding ``formatter_class`` sets the argparse formatter class.
|
||||
|
||||
Overriding formatter_class sets the argparse formatter class.
|
||||
|
||||
If the 'subcommands' dictionary is set, __init__ searches the
|
||||
arguments for subcommands.keys() and instantiates the class
|
||||
If the ``subcommands`` dictionary is set, ``__init__`` searches the
|
||||
arguments for ``subcommands.keys()`` and instantiates the class
|
||||
implementing the subcommand as self.cmd, passing all non-understood
|
||||
arguments, the current options namespace, and the full command path
|
||||
name.
|
||||
|
||||
:cvar device_option: if set to True adds ``-d`` / ``--device``
|
||||
option to current command
|
||||
:cvar no_add_help: if set to True removes ``-h`` ``--help``
|
||||
option from current command
|
||||
"""
|
||||
|
||||
device_option = False
|
||||
no_add_help = False # for rip.main.Whipper
|
||||
formatter_class = argparse.RawDescriptionHelpFormatter
|
||||
|
||||
@@ -43,8 +43,7 @@ class EntryNotFound(Exception):
|
||||
|
||||
class _AccurateRipResponse:
|
||||
"""
|
||||
An AccurateRip response contains a collection of metadata identifying a
|
||||
particular digital audio compact disc.
|
||||
An AR resp. contains a collection of metadata identifying a specific disc.
|
||||
|
||||
For disc level metadata it contains the track count, two internal disc
|
||||
IDs, and the CDDB disc ID.
|
||||
@@ -55,9 +54,12 @@ class _AccurateRipResponse:
|
||||
|
||||
The response is stored as a packed binary structure.
|
||||
"""
|
||||
|
||||
def __init__(self, data):
|
||||
"""
|
||||
The checksums and confidences arrays are indexed by relative track
|
||||
Init _AccurateRipResponse.
|
||||
|
||||
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.
|
||||
"""
|
||||
@@ -98,12 +100,14 @@ def _split_responses(raw_entry):
|
||||
|
||||
def calculate_checksums(track_paths):
|
||||
"""
|
||||
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.
|
||||
Calculate AccurateRip checksums for the given tracks.
|
||||
|
||||
HTOA checksums are not included in the database and are not calculated.
|
||||
|
||||
:returns: ARv1 and ARv2 checksums as two arrays of character strings in a
|
||||
dictionary: ``{'v1': ['deadbeef', ...], 'v2': [...]}``
|
||||
or None instead of checksum string for unchecksummable tracks.
|
||||
:rtype: dict(string, list()) or None
|
||||
"""
|
||||
track_count = len(track_paths)
|
||||
v1_checksums = []
|
||||
@@ -152,9 +156,10 @@ def _save_entry(raw_entry, path):
|
||||
def get_db_entry(path):
|
||||
"""
|
||||
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().
|
||||
``path`` is in the format of the output of ``table.accuraterip_path()``.
|
||||
"""
|
||||
cached_path = join(_CACHE_DIR, path)
|
||||
if exists(cached_path):
|
||||
@@ -183,11 +188,11 @@ def _assign_checksums_and_confidences(tracks, checksums, responses):
|
||||
|
||||
def _match_responses(tracks, responses):
|
||||
"""
|
||||
Match and save track accuraterip response checksums against
|
||||
all non-hidden tracks.
|
||||
Match and save track AR response checksums against all non-hidden tracks.
|
||||
|
||||
Returns True if every track has a match for every entry for either
|
||||
AccurateRip version.
|
||||
:returns: True if every track has a match for every entry for either
|
||||
AccurateRip version, False otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
for r in responses:
|
||||
for i, track in enumerate(tracks):
|
||||
@@ -211,7 +216,8 @@ def _match_responses(tracks, responses):
|
||||
def verify_result(result, responses, checksums):
|
||||
"""
|
||||
Verify track AccurateRip checksums against database responses.
|
||||
Stores track checksums and database values on result.
|
||||
|
||||
Store track checksums and database values on result.
|
||||
"""
|
||||
if not (result and responses and checksums):
|
||||
return False
|
||||
@@ -226,9 +232,7 @@ def verify_result(result, responses, checksums):
|
||||
|
||||
|
||||
def print_report(result):
|
||||
"""
|
||||
Print AccurateRip verification results.
|
||||
"""
|
||||
"""Print AccurateRip verification results."""
|
||||
for _, track in enumerate(result.tracks):
|
||||
status = 'rip NOT accurate'
|
||||
conf = '(not found)'
|
||||
|
||||
@@ -45,6 +45,7 @@ class Persister:
|
||||
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.
|
||||
@@ -56,8 +57,7 @@ class Persister:
|
||||
|
||||
def persist(self, obj=None):
|
||||
"""
|
||||
Persist the given object, if we have a persistence path and the
|
||||
object changed.
|
||||
Persist the given obj if we have a persist. path and the obj changed.
|
||||
|
||||
If object is not given, re-persist our object, always.
|
||||
If object is given, only persist if it was changed.
|
||||
@@ -115,9 +115,7 @@ class Persister:
|
||||
|
||||
|
||||
class PersistedCache:
|
||||
"""
|
||||
I wrap a directory of persisted objects.
|
||||
"""
|
||||
"""Wrap a directory of persisted objects."""
|
||||
|
||||
path = None
|
||||
|
||||
@@ -129,9 +127,7 @@ class PersistedCache:
|
||||
return os.path.join(self.path, '%s.pickle' % key)
|
||||
|
||||
def get(self, key):
|
||||
"""
|
||||
Returns the persister for the given key.
|
||||
"""
|
||||
"""Return the persister for the given key."""
|
||||
persister = Persister(self._getPath(key))
|
||||
if persister.object:
|
||||
if hasattr(persister.object, 'instanceVersion'):
|
||||
@@ -154,8 +150,9 @@ class ResultCache:
|
||||
|
||||
def getRipResult(self, cddbdiscid, create=True):
|
||||
"""
|
||||
Retrieve the persistable RipResult either from our cache (from a
|
||||
previous, possibly aborted rip), or return a new one.
|
||||
Get the persistable RipResult either from our cache or ret. a new one.
|
||||
|
||||
The cached RipResult may come from an aborted rip.
|
||||
|
||||
:rtype: :any:`Persistable` for :any:`result.RipResult`
|
||||
"""
|
||||
@@ -183,9 +180,8 @@ class ResultCache:
|
||||
|
||||
|
||||
class TableCache:
|
||||
|
||||
"""
|
||||
I read and write entries to and from the cache of tables.
|
||||
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
|
||||
|
||||
@@ -39,14 +39,14 @@ BYTES_PER_FRAME = SAMPLES_PER_FRAME * 4
|
||||
|
||||
|
||||
class EjectError(SystemError):
|
||||
"""
|
||||
Possibly ejects the drive in command.main.
|
||||
"""
|
||||
"""Possibly eject 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
|
||||
Init EjectError.
|
||||
|
||||
:param args: a tuple used by ``BaseException.__str__``
|
||||
:param device: device path to eject
|
||||
"""
|
||||
self.args = args
|
||||
self.device = device
|
||||
@@ -54,13 +54,12 @@ class EjectError(SystemError):
|
||||
|
||||
def msfToFrames(msf):
|
||||
"""
|
||||
Converts a string value in MM:SS:FF to frames.
|
||||
Convert a string value in MM:SS:FF to frames.
|
||||
|
||||
:param msf: the MM:SS:FF value to convert
|
||||
:type msf: str
|
||||
|
||||
:rtype: int
|
||||
:type msf: str
|
||||
:returns: number of frames
|
||||
:rtype: int
|
||||
"""
|
||||
if ':' not in msf:
|
||||
return int(msf)
|
||||
@@ -97,21 +96,19 @@ 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 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
|
||||
|
||||
seconds
|
||||
:type fractional: int
|
||||
:returns: a nicely formatted time string
|
||||
:rtype: string
|
||||
:returns: a nicely formatted time string.
|
||||
"""
|
||||
chunks = []
|
||||
|
||||
@@ -149,16 +146,13 @@ class EmptyError(Exception):
|
||||
|
||||
|
||||
class MissingFrames(Exception):
|
||||
"""
|
||||
Less frames decoded than expected.
|
||||
"""
|
||||
"""Less frames decoded than expected."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def truncate_filename(path):
|
||||
"""
|
||||
Truncate filename to the max. len. allowed by the path's filesystem
|
||||
"""
|
||||
"""Truncate filename to the max. len. allowed by the path's filesystem."""
|
||||
p, f = os.path.split(os.path.normpath(path))
|
||||
f, e = os.path.splitext(f)
|
||||
# Get the filename length limit in bytes
|
||||
@@ -172,7 +166,8 @@ def truncate_filename(path):
|
||||
def shrinkPath(path):
|
||||
"""
|
||||
Shrink a full path to a shorter version.
|
||||
Used to handle ENAMETOOLONG
|
||||
|
||||
Used to handle ``ENAMETOOLONG``.
|
||||
"""
|
||||
parts = list(os.path.split(path))
|
||||
length = len(parts[-1])
|
||||
@@ -204,14 +199,15 @@ def shrinkPath(path):
|
||||
def getRealPath(refPath, filePath):
|
||||
"""
|
||||
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: str
|
||||
|
||||
:type filePath: str
|
||||
: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: str
|
||||
:type filePath: str
|
||||
"""
|
||||
assert isinstance(filePath, str), "%r is not str" % filePath
|
||||
|
||||
@@ -258,10 +254,9 @@ 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
|
||||
Used to determine the path to use in .cue/.m3u files.
|
||||
"""
|
||||
logger.debug('getRelativePath: target %r, collection %r',
|
||||
targetPath, collectionPath)
|
||||
@@ -280,9 +275,7 @@ def getRelativePath(targetPath, collectionPath):
|
||||
|
||||
|
||||
def validate_template(template, kind):
|
||||
"""
|
||||
Raise exception if disc/track template includes invalid variables
|
||||
"""
|
||||
"""Raise exception if disc/track template includes invalid variables."""
|
||||
if kind == 'disc':
|
||||
matches = re.findall(r'%[^ARSXdrxy]', template)
|
||||
elif kind == 'track':
|
||||
@@ -294,20 +287,22 @@ def validate_template(template, kind):
|
||||
|
||||
class VersionGetter:
|
||||
"""
|
||||
I get the version of a program by looking for it in command output
|
||||
according to a regexp.
|
||||
Get the version of a program.
|
||||
|
||||
It is extracted by looking for it in command output according to a RegEX.
|
||||
"""
|
||||
|
||||
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
|
||||
"""
|
||||
Init VersionGetter.
|
||||
|
||||
:param dependency: name of the dependency providing the program
|
||||
:param args: the arguments to invoke to show the version
|
||||
:type args: list(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
|
||||
|
||||
@@ -96,9 +96,7 @@ class Config:
|
||||
self.write()
|
||||
|
||||
def getReadOffset(self, vendor, model, release):
|
||||
"""
|
||||
Get a read offset for the given drive.
|
||||
"""
|
||||
"""Get a read offset for the given drive."""
|
||||
section = self._findDriveSection(vendor, model, release)
|
||||
|
||||
try:
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
# 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.
|
||||
"""
|
||||
"""Handle communication with the MusicBrainz server using NGS."""
|
||||
from urllib.error import HTTPError
|
||||
|
||||
import whipper
|
||||
@@ -62,16 +60,19 @@ class TrackMetadata:
|
||||
|
||||
class DiscMetadata:
|
||||
"""
|
||||
:param artist: artist(s) name
|
||||
:param sortName: release artist sort name
|
||||
:param release: earliest release date, in YYYY-MM-DD
|
||||
:type release: str
|
||||
:param title: title of the disc (with disambiguation)
|
||||
:param releaseTitle: title of the release (without disambiguation)
|
||||
:type tracks: list of :any:`TrackMetadata`
|
||||
:param countries: MusicBrainz release countries
|
||||
:type countries: list or None
|
||||
Represent the disc metadata.
|
||||
|
||||
:cvar artist: artist(s) name
|
||||
:cvar sortName: release artist sort name
|
||||
:cvar release: earliest release date, in YYYY-MM-DD
|
||||
:vartype release: str
|
||||
:cvar title: title of the disc (with disambiguation)
|
||||
:cvar releaseTitle: title of the release (without disambiguation)
|
||||
:vartype tracks: list of :any:`TrackMetadata`
|
||||
:cvar countries: MusicBrainz release countries
|
||||
:vartype countries: list or None
|
||||
"""
|
||||
|
||||
artist = None
|
||||
sortName = None
|
||||
title = None
|
||||
@@ -123,10 +124,7 @@ def _record(record, which, name, what):
|
||||
|
||||
|
||||
class _Credit(list):
|
||||
"""
|
||||
I am a representation of an artist-credit in MusicBrainz for a disc
|
||||
or track.
|
||||
"""
|
||||
"""Represent an artist-credit in MusicBrainz for a disc or track."""
|
||||
|
||||
def joiner(self, attributeGetter, joinString=None):
|
||||
res = []
|
||||
@@ -217,10 +215,11 @@ def _getPerformers(recording):
|
||||
|
||||
def _getMetadata(release, discid=None, country=None):
|
||||
"""
|
||||
:type release: dict
|
||||
Get disc metadata based upon the provided release id.
|
||||
|
||||
:param release: a release dict as returned in the value for key release
|
||||
from get_release_by_id
|
||||
|
||||
:type release: dict
|
||||
:rtype: DiscMetadata or None
|
||||
"""
|
||||
logger.debug('getMetadata for release id %r', release['id'])
|
||||
@@ -378,14 +377,16 @@ def getReleaseMetadata(release_id, discid=None, country=None, record=False):
|
||||
|
||||
def musicbrainz(discid, country=None, record=False):
|
||||
"""
|
||||
Based on a MusicBrainz disc id, get a list of DiscMetadata objects
|
||||
for the given disc id.
|
||||
Get a list of DiscMetadata objects for the given MusicBrainz disc id.
|
||||
|
||||
Example disc id: Mj48G109whzEmAbPBoGvd4KyCS4-
|
||||
|
||||
:type discid: str
|
||||
Example disc id: ``Mj48G109whzEmAbPBoGvd4KyCS4-``
|
||||
|
||||
:type discid: str
|
||||
:rtype: list of :any:`DiscMetadata`
|
||||
:param country: country name used to filter releases by provenance
|
||||
:type country: str
|
||||
:param record: whether to record to disc as a JSON serialization
|
||||
:type record: bool
|
||||
"""
|
||||
logger.debug('looking up results for discid %r', discid)
|
||||
|
||||
|
||||
@@ -22,16 +22,20 @@ import re
|
||||
|
||||
|
||||
class PathFilter:
|
||||
"""
|
||||
I filter path components for safe storage on file systems.
|
||||
"""
|
||||
"""Filter path components for safe storage on file systems."""
|
||||
|
||||
def __init__(self, slashes=True, quotes=True, fat=True, special=False):
|
||||
"""
|
||||
Init PathFilter.
|
||||
|
||||
:param slashes: whether to convert slashes to dashes
|
||||
:param quotes: whether to normalize quotes
|
||||
:param fat: whether to strip characters illegal on FAT filesystems
|
||||
:type slashes: bool
|
||||
:param quotes: whether to normalize quotes
|
||||
:type quotes: bool
|
||||
:param fat: whether to strip characters illegal on FAT filesystems
|
||||
:type fat: bool
|
||||
:param special: whether to strip special characters
|
||||
:type special: bool
|
||||
"""
|
||||
self._slashes = slashes
|
||||
self._quotes = quotes
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
# 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
|
||||
@@ -47,10 +45,10 @@ class Program:
|
||||
I maintain program state and functionality.
|
||||
|
||||
:vartype metadata: mbngs.DiscMetadata
|
||||
:cvar result: the rip's result
|
||||
:vartype result: result.RipResult
|
||||
:vartype outdir: str
|
||||
:vartype config: whipper.common.config.Config
|
||||
:cvar result: the rip's result
|
||||
:vartype result: result.RipResult
|
||||
:vartype outdir: str
|
||||
:vartype config: whipper.common.config.Config
|
||||
"""
|
||||
|
||||
cuePath = None
|
||||
@@ -61,7 +59,9 @@ class Program:
|
||||
|
||||
def __init__(self, config, record=False):
|
||||
"""
|
||||
:param record: whether to record results of API calls for playback.
|
||||
Init Program.
|
||||
|
||||
:param record: whether to record results of API calls for playback
|
||||
"""
|
||||
self._record = record
|
||||
self._cache = cache.ResultCache()
|
||||
@@ -89,7 +89,9 @@ class Program:
|
||||
os.chdir(workingDirectory)
|
||||
|
||||
def getFastToc(self, runner, device):
|
||||
"""Retrieve the normal TOC table from the drive.
|
||||
"""
|
||||
Retrieve the normal TOC table from the drive.
|
||||
|
||||
Also warn about buggy cdrdao versions.
|
||||
"""
|
||||
from pkg_resources import parse_version as V
|
||||
@@ -150,8 +152,9 @@ class Program:
|
||||
|
||||
def getRipResult(self, cddbdiscid):
|
||||
"""
|
||||
Retrieve the persistable RipResult either from our cache (from a
|
||||
previous, possibly aborted rip), or return a new one.
|
||||
Get the persistable RipResult either from our cache or ret. a new one.
|
||||
|
||||
The cached RipResult may come from an aborted rip.
|
||||
|
||||
:rtype: result.RipResult
|
||||
"""
|
||||
@@ -176,28 +179,31 @@ class Program:
|
||||
|
||||
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: release artist
|
||||
- %S: release artist 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
|
||||
|
||||
* ``%A``: release artist
|
||||
* ``%S``: release artist 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 isinstance(outdir, str), "%r is not str" % outdir
|
||||
assert isinstance(template, str), "%r is not str" % template
|
||||
@@ -247,8 +253,9 @@ class Program:
|
||||
@staticmethod
|
||||
def getCDDB(cddbdiscid):
|
||||
"""
|
||||
:param cddbdiscid: list of id, tracks, offsets, seconds
|
||||
Fetch basic metadata from freedb's CDDB.
|
||||
|
||||
:param cddbdiscid: list of id, tracks, offsets, seconds
|
||||
:rtype: str
|
||||
"""
|
||||
# FIXME: convert to nonblocking?
|
||||
@@ -272,7 +279,20 @@ class Program:
|
||||
def getMusicBrainz(self, ittoc, mbdiscid, release=None, country=None,
|
||||
prompt=False):
|
||||
"""
|
||||
:type ittoc: whipper.image.table.Table
|
||||
Fetch MusicBrainz's metadata for the given MusicBrainz disc id.
|
||||
|
||||
:param ittoc: disc TOC
|
||||
:type ittoc: whipper.image.table.Table
|
||||
:param mbdiscid: MusicBrainz DiscID
|
||||
:type mbdiscid: str
|
||||
:param release: MusicBrainz release id to match to
|
||||
(if there are multiple)
|
||||
:type release: str or None
|
||||
:param country: country name used to filter releases by provenance
|
||||
:type country: str or None
|
||||
:param prompt: whether to prompt if there are multiple
|
||||
matching releases
|
||||
:type prompt: bool
|
||||
"""
|
||||
# look up disc on MusicBrainz
|
||||
print('Disc duration: %s, %d audio tracks' % (
|
||||
@@ -393,9 +413,10 @@ class Program:
|
||||
"""
|
||||
Based on the metadata, get a dict of tags for the given track.
|
||||
|
||||
:param number: track number (0 for HTOA)
|
||||
:type number: int
|
||||
|
||||
:param number: track number (0 for HTOA)
|
||||
:type number: int
|
||||
:param mbdiscid: MusicBrainz DiscID
|
||||
:type mbdiscid: str
|
||||
:rtype: dict
|
||||
"""
|
||||
trackArtist = 'Unknown Artist'
|
||||
@@ -469,6 +490,7 @@ class Program:
|
||||
Check if we have hidden track one audio.
|
||||
|
||||
:returns: tuple of (start, stop), or None
|
||||
:rtype: tuple(int, int) or None
|
||||
"""
|
||||
track = self.result.table.tracks[0]
|
||||
try:
|
||||
@@ -531,11 +553,26 @@ class Program:
|
||||
def ripTrack(self, runner, trackResult, offset, device, taglist,
|
||||
overread, what=None, coverArtPath=None):
|
||||
"""
|
||||
Rip and store a track of the disc.
|
||||
|
||||
Ripping the track may change the track's filename as stored in
|
||||
trackResult.
|
||||
|
||||
:param trackResult: the object to store information in.
|
||||
:type trackResult: result.TrackResult
|
||||
:param runner: synchronous track rip task
|
||||
:type runner: task.SyncRunner
|
||||
:param trackResult: the object to store information in
|
||||
:type trackResult: result.TrackResult
|
||||
:param offset: ripping offset, in CD frames
|
||||
:type offset: int
|
||||
:param device: path to the hardware disc drive
|
||||
:type device: str
|
||||
:param taglist: dictionary of tags for the given track
|
||||
:type taglist: dict
|
||||
:param overread: whether to force overreading into the
|
||||
lead-out portion of the disc
|
||||
:type overread: bool
|
||||
:param what: a string representing what's being read; e.g. Track
|
||||
:type what: str or None
|
||||
"""
|
||||
if trackResult.number == 0:
|
||||
start, stop = self.getHTOA()
|
||||
@@ -581,7 +618,8 @@ class Program:
|
||||
|
||||
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.
|
||||
|
||||
@@ -35,14 +35,13 @@ class Operator:
|
||||
self._resuming = False
|
||||
|
||||
def addOperation(self, operation):
|
||||
"""
|
||||
Add an operation.
|
||||
"""
|
||||
"""Add an operation."""
|
||||
self._todo.append(operation)
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
Load state from the given state path using the given key.
|
||||
|
||||
Verifies the state.
|
||||
"""
|
||||
todo = os.path.join(self._statePath, self._key + '.todo')
|
||||
@@ -67,9 +66,7 @@ class Operator:
|
||||
self._resuming = True
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Saves the state to the given state path using the given key.
|
||||
"""
|
||||
"""Save 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):
|
||||
@@ -88,9 +85,7 @@ class Operator:
|
||||
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)]
|
||||
@@ -110,10 +105,10 @@ class FileRenamer(Operator):
|
||||
"""
|
||||
Add a rename operation.
|
||||
|
||||
:param source: source filename
|
||||
:type source: str
|
||||
:param source: source filename
|
||||
:type source: str
|
||||
:param destination: destination filename
|
||||
:type destination: str
|
||||
:type destination: str
|
||||
"""
|
||||
|
||||
|
||||
@@ -122,27 +117,27 @@ class Operation:
|
||||
def verify(self):
|
||||
"""
|
||||
Check if the operation will succeed in the current conditions.
|
||||
Consider this a pre-flight check.
|
||||
|
||||
Consider this a pre-flight check.
|
||||
Does not eliminate the need to handle errors as they happen.
|
||||
"""
|
||||
|
||||
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.
|
||||
|
||||
Perform it without knowing if it already has been (partly) performed.
|
||||
"""
|
||||
self.do()
|
||||
|
||||
def serialize(self):
|
||||
"""
|
||||
Serialize the operation.
|
||||
|
||||
The return value should bu usable with :any:`deserialize`
|
||||
|
||||
:rtype: str
|
||||
@@ -152,7 +147,7 @@ class Operation:
|
||||
"""
|
||||
Deserialize the operation with the given operation data.
|
||||
|
||||
:type data: str
|
||||
:type data: str
|
||||
"""
|
||||
raise NotImplementedError
|
||||
deserialize = classmethod(deserialize)
|
||||
|
||||
@@ -25,9 +25,7 @@ class LoggableMultiSeparateTask(task.MultiSeparateTask):
|
||||
|
||||
|
||||
class PopenTask(task.Task):
|
||||
"""
|
||||
I am a task that runs a command using Popen.
|
||||
"""
|
||||
"""Task that runs a command using Popen."""
|
||||
|
||||
logCategory = 'PopenTask'
|
||||
bufsize = 1024
|
||||
@@ -117,31 +115,21 @@ class PopenTask(task.Task):
|
||||
# self.stop()
|
||||
|
||||
def readbytesout(self, bytes_stdout):
|
||||
"""
|
||||
Called when bytes have been read from stdout.
|
||||
"""
|
||||
"""Call when bytes have been read from stdout."""
|
||||
pass
|
||||
|
||||
def readbyteserr(self, bytes_stderr):
|
||||
"""
|
||||
Called when bytes have been read from stderr.
|
||||
"""
|
||||
"""Call when bytes have been read from stderr."""
|
||||
pass
|
||||
|
||||
def done(self):
|
||||
"""
|
||||
Called when the command completed successfully.
|
||||
"""
|
||||
"""Call when the command completed successfully."""
|
||||
pass
|
||||
|
||||
def failed(self):
|
||||
"""
|
||||
Called when the command failed.
|
||||
"""
|
||||
"""Call when the command failed."""
|
||||
pass
|
||||
|
||||
def commandMissing(self):
|
||||
"""
|
||||
Called when the command is missing.
|
||||
"""
|
||||
"""Call when the command is missing."""
|
||||
pass
|
||||
|
||||
47
whipper/extern/freedb.py
vendored
47
whipper/extern/freedb.py
vendored
@@ -18,21 +18,23 @@
|
||||
|
||||
|
||||
def digit_sum(i):
|
||||
"""returns the sum of all digits for the given integer"""
|
||||
|
||||
"""Return the sum of all digits for the given integer."""
|
||||
return sum(map(int, str(i)))
|
||||
|
||||
|
||||
class DiscID:
|
||||
def __init__(self, offsets, total_length, track_count, playable_length):
|
||||
"""offsets is a list of track offsets, in CD frames
|
||||
total_length is the total length of the disc, in seconds
|
||||
track_count is the total number of tracks on the disc
|
||||
playable_length is the playable length of the disc, in seconds
|
||||
"""
|
||||
Init DiscID.
|
||||
|
||||
the first three items are for generating the hex disc ID itself
|
||||
while the last is for performing queries"""
|
||||
:param offsets: list of track offsets, in CD frames
|
||||
:param total_length: total length of the disc, in seconds
|
||||
:param track_count: total number of tracks on the disc
|
||||
:param playable_length: playable length of the disc, in seconds
|
||||
|
||||
The first three items are for generating the hex disc ID itself
|
||||
while the last is for performing queries.
|
||||
"""
|
||||
assert(len(offsets) == track_count)
|
||||
for o in offsets:
|
||||
assert(o >= 0)
|
||||
@@ -61,16 +63,15 @@ class DiscID:
|
||||
|
||||
|
||||
def perform_lookup(disc_id, freedb_server, freedb_port):
|
||||
"""performs a web-based lookup using a DiscID
|
||||
on the given freedb_server string and freedb_int port
|
||||
|
||||
iterates over a list of MetaData objects per successful match, like:
|
||||
[track1, track2, ...], [track1, track2, ...], ...
|
||||
|
||||
may raise HTTPError if an error occurs querying the server
|
||||
or ValueError if the server returns invalid data
|
||||
"""
|
||||
Perform a web-based lookup using a DiscID on the given server and port.
|
||||
|
||||
Iterate over a list of MetaData objects per successful match, like:
|
||||
``[track1, track2, ...], [track1, track2, ...], ...``
|
||||
|
||||
:raises HTTPError: if an error occurs querying the server
|
||||
:raises ValueError: if the server returns invalid data
|
||||
"""
|
||||
import re
|
||||
from time import sleep
|
||||
|
||||
@@ -154,8 +155,18 @@ def perform_lookup(disc_id, freedb_server, freedb_port):
|
||||
|
||||
|
||||
def freedb_command(freedb_server, freedb_port, cmd, *args):
|
||||
"""given a freedb_server string, freedb_port int,
|
||||
command string and argument strings, yields a list of strings"""
|
||||
"""
|
||||
Generate and perform a query against FreeDB using the given command.
|
||||
|
||||
Yields a list of Unicode strings.
|
||||
|
||||
:param freedb_server: URL of FreeDB server to be queried
|
||||
:type freedb_server: str
|
||||
:param freedb_port: port number of FreeDB server to be queried
|
||||
:type freedb_port: int
|
||||
:param cmd: CDDB command
|
||||
:type cmd: str
|
||||
"""
|
||||
|
||||
from urllib.error import URLError
|
||||
from urllib.request import urlopen
|
||||
|
||||
89
whipper/extern/task/task.py
vendored
89
whipper/extern/task/task.py
vendored
@@ -27,9 +27,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TaskException(Exception):
|
||||
"""
|
||||
I wrap an exception that happened during task execution.
|
||||
"""
|
||||
"""Wrap an exception that happened during task execution."""
|
||||
|
||||
exception = None # original exception
|
||||
|
||||
@@ -44,6 +42,7 @@ class TaskException(Exception):
|
||||
def _getExceptionMessage(exception, frame=-1, filename=None):
|
||||
"""
|
||||
Return a short message based on an exception, useful for debugging.
|
||||
|
||||
Tries to find where the exception was triggered.
|
||||
"""
|
||||
import traceback
|
||||
@@ -69,9 +68,7 @@ def _getExceptionMessage(exception, frame=-1, filename=None):
|
||||
|
||||
|
||||
class LogStub:
|
||||
"""
|
||||
I am a stub for a log interface.
|
||||
"""
|
||||
"""Stub for a log interface."""
|
||||
|
||||
@staticmethod
|
||||
def log(message, *args):
|
||||
@@ -88,18 +85,20 @@ class LogStub:
|
||||
|
||||
class Task(LogStub):
|
||||
"""
|
||||
I wrap a task in an asynchronous interface.
|
||||
I can be listened to for starting, stopping, description changes
|
||||
Wrap a task in an asynchronous interface.
|
||||
|
||||
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.
|
||||
|
||||
:cvar description: what am I doing
|
||||
:cvar exception: set if an exception happened during the task
|
||||
execution. Will be raised through run() at the end.
|
||||
:cvar description: what am I doing
|
||||
:cvar 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.'
|
||||
@@ -126,7 +125,7 @@ class Task(LogStub):
|
||||
using those methods.
|
||||
|
||||
If start doesn't raise an exception, the task should run until
|
||||
complete, or setException and stop().
|
||||
complete, or ``setException()`` and ``stop()``.
|
||||
"""
|
||||
self.debug('starting')
|
||||
self.setProgress(self.progress)
|
||||
@@ -137,6 +136,7 @@ class Task(LogStub):
|
||||
def stop(self):
|
||||
"""
|
||||
Stop the task.
|
||||
|
||||
Also resets the runner on the task.
|
||||
|
||||
Subclasses should chain up to me at the end.
|
||||
@@ -160,6 +160,7 @@ class Task(LogStub):
|
||||
def setProgress(self, value):
|
||||
"""
|
||||
Notify about progress changes bigger than the increment.
|
||||
|
||||
Called by subclass implementations as the task progresses.
|
||||
"""
|
||||
if (value - self.progress > self.increment or
|
||||
@@ -177,8 +178,9 @@ class Task(LogStub):
|
||||
# FIXME: unify?
|
||||
def setExceptionAndTraceback(self, exception):
|
||||
"""
|
||||
Call this to set a synthetically created exception (and not one
|
||||
that was actually raised and caught)
|
||||
Call this to set a synthetically created exception.
|
||||
|
||||
Not one that was actually raised and caught.
|
||||
"""
|
||||
import traceback
|
||||
|
||||
@@ -201,9 +203,7 @@ class Task(LogStub):
|
||||
setAndRaiseException = setExceptionAndTraceback
|
||||
|
||||
def setException(self, exception):
|
||||
"""
|
||||
Call this to set a caught exception on the task.
|
||||
"""
|
||||
"""Call this to set a caught exception on the task."""
|
||||
import traceback
|
||||
|
||||
self.exception = exception
|
||||
@@ -244,35 +244,36 @@ class Task(LogStub):
|
||||
|
||||
# FIXME: should this become a real interface, like in zope ?
|
||||
class ITaskListener:
|
||||
"""
|
||||
I am an interface for objects listening to tasks.
|
||||
"""
|
||||
"""An interface for objects listening to tasks."""
|
||||
# listener callbacks
|
||||
|
||||
def progressed(self, task, value):
|
||||
"""
|
||||
Implement me to be informed about progress.
|
||||
|
||||
:type value: float
|
||||
:param task: a task
|
||||
:type task: Task
|
||||
:param value: progress, from 0.0 to 1.0
|
||||
:type value: float
|
||||
"""
|
||||
|
||||
def described(self, task, description):
|
||||
"""
|
||||
Implement me to be informed about description changes.
|
||||
|
||||
:type description: str
|
||||
:param task: a task
|
||||
:type task: Task
|
||||
:param description: description
|
||||
:type description: str
|
||||
"""
|
||||
|
||||
def started(self, task):
|
||||
"""
|
||||
Implement me to be informed about the task starting.
|
||||
"""
|
||||
"""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.
|
||||
"""
|
||||
|
||||
@@ -297,8 +298,8 @@ class BaseMultiTask(Task, ITaskListener):
|
||||
"""
|
||||
I perform multiple tasks.
|
||||
|
||||
:ivar tasks: the tasks to run
|
||||
:type tasks: list of :any:`Task`
|
||||
:cvar tasks: the tasks to run
|
||||
:vartype tasks: list(Task)
|
||||
"""
|
||||
|
||||
description = 'Doing various tasks'
|
||||
@@ -322,7 +323,7 @@ class BaseMultiTask(Task, ITaskListener):
|
||||
"""
|
||||
Start tasks.
|
||||
|
||||
Tasks can still be added while running. For example,
|
||||
Tasks can still be added while running. For example,
|
||||
a first task can determine how many additional tasks to run.
|
||||
"""
|
||||
Task.start(self, runner)
|
||||
@@ -335,9 +336,7 @@ 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]
|
||||
@@ -364,9 +363,10 @@ class BaseMultiTask(Task, ITaskListener):
|
||||
def progressed(self, task, value):
|
||||
pass
|
||||
|
||||
def stopped(self, task):
|
||||
def stopped(self, task): # noqa: D401
|
||||
"""
|
||||
Subclasses should chain up to me at the end of their implementation.
|
||||
|
||||
They should fall through to chaining up if there is an exception.
|
||||
"""
|
||||
self.debug('BaseMultiTask.stopped: task %r (%d of %d)',
|
||||
@@ -391,9 +391,11 @@ class BaseMultiTask(Task, ITaskListener):
|
||||
|
||||
class MultiSeparateTask(BaseMultiTask):
|
||||
"""
|
||||
I perform multiple tasks.
|
||||
I track progress of each individual task, going back to 0 for each task.
|
||||
Perform multiple tasks.
|
||||
|
||||
Track progress of each individual task, going back to 0 for each task.
|
||||
"""
|
||||
|
||||
description = 'Doing various tasks separately'
|
||||
|
||||
def start(self, runner):
|
||||
@@ -417,8 +419,9 @@ class MultiSeparateTask(BaseMultiTask):
|
||||
|
||||
class MultiCombinedTask(BaseMultiTask):
|
||||
"""
|
||||
I perform multiple tasks.
|
||||
I track progress as a combined progress on all tasks on task granularity.
|
||||
Perform multiple tasks.
|
||||
|
||||
Track progress as a combined progress on all tasks on task granularity.
|
||||
"""
|
||||
|
||||
description = 'Doing various tasks combined'
|
||||
@@ -436,16 +439,18 @@ class MultiCombinedTask(BaseMultiTask):
|
||||
|
||||
class TaskRunner(LogStub):
|
||||
"""
|
||||
I am a base class for task runners.
|
||||
Base class for task runners.
|
||||
|
||||
Task runners should be reusable.
|
||||
"""
|
||||
|
||||
logCategory = 'TaskRunner'
|
||||
|
||||
def run(self, task):
|
||||
"""
|
||||
Run the given task.
|
||||
|
||||
:type task: Task
|
||||
:type task: Task
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -456,16 +461,16 @@ class TaskRunner(LogStub):
|
||||
|
||||
Subclasses should implement this.
|
||||
|
||||
:type delta: float
|
||||
:param delta: time in the future to schedule call for, in seconds.
|
||||
:type delta: float
|
||||
:param callable_task: a task
|
||||
:type callable_task: Task
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SyncRunner(TaskRunner, ITaskListener):
|
||||
"""
|
||||
I run the task synchronously in a GObject MainLoop.
|
||||
"""
|
||||
"""Run the task synchronously in a GObject MainLoop."""
|
||||
|
||||
def __init__(self, verbose=True):
|
||||
self._verbose = verbose
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
# along with whipper. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Reading .cue files
|
||||
Read .cue files.
|
||||
|
||||
See http://digitalx.org/cuesheetsyntax.php
|
||||
"""
|
||||
@@ -58,17 +58,15 @@ _INDEX_RE = re.compile(r"""
|
||||
|
||||
|
||||
class CueFile:
|
||||
"""
|
||||
I represent a .cue file as an object.
|
||||
|
||||
:vartype table: table.Table
|
||||
:ivar table: the index table.
|
||||
"""
|
||||
"""Represent a .cue file as an object."""
|
||||
logCategory = 'CueFile'
|
||||
|
||||
def __init__(self, path):
|
||||
"""
|
||||
:type path: str
|
||||
Init CueFile.
|
||||
|
||||
:param path: path to track
|
||||
:type path: str
|
||||
"""
|
||||
assert isinstance(path, str), "%r is not str" % path
|
||||
|
||||
@@ -153,7 +151,10 @@ class CueFile:
|
||||
"""
|
||||
Add a message about a given line in the cue file.
|
||||
|
||||
:param number: line number, counting from 0.
|
||||
:param message: a text line in the cue sheet
|
||||
:type message: str
|
||||
:param number: line number, counting from 0
|
||||
:type number: int
|
||||
"""
|
||||
self._messages.append((number + 1, message))
|
||||
|
||||
@@ -181,19 +182,21 @@ class CueFile:
|
||||
"""
|
||||
Translate the .cue's FILE to an existing path.
|
||||
|
||||
:type path: str
|
||||
:param path: path to track
|
||||
:type path: str
|
||||
"""
|
||||
return common.getRealPath(self._path, path)
|
||||
|
||||
|
||||
class File:
|
||||
"""
|
||||
I represent a FILE line in a cue file.
|
||||
"""
|
||||
"""Represent a FILE line in a cue file."""
|
||||
|
||||
def __init__(self, path, file_format):
|
||||
"""
|
||||
:type path: str
|
||||
Init File.
|
||||
|
||||
:param path: path to track
|
||||
:type path: str
|
||||
"""
|
||||
assert isinstance(path, str), "%r is not str" % path
|
||||
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
# 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.
|
||||
"""
|
||||
"""Wrap on-disk CD images based on the .cue file."""
|
||||
|
||||
import os
|
||||
|
||||
@@ -36,15 +34,19 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class Image:
|
||||
"""
|
||||
:ivar table: The Table of Contents for this image.
|
||||
Represent a CD image based on the .cue file.
|
||||
|
||||
:ivar table: The Table of Contents for this image
|
||||
:vartype table: table.Table
|
||||
"""
|
||||
logCategory = 'Image'
|
||||
|
||||
def __init__(self, path):
|
||||
"""
|
||||
:type path: str
|
||||
Init Image.
|
||||
|
||||
:param path: .cue path
|
||||
:type path: str
|
||||
"""
|
||||
assert isinstance(path, str), "%r is not str" % path
|
||||
|
||||
@@ -61,6 +63,7 @@ class Image:
|
||||
Translate the .cue's FILE to an existing path.
|
||||
|
||||
:param path: .cue path
|
||||
:type path: unicode
|
||||
"""
|
||||
assert isinstance(path, str), "%r is not str" % path
|
||||
|
||||
@@ -68,8 +71,10 @@ class Image:
|
||||
|
||||
def setup(self, runner):
|
||||
"""
|
||||
Do initial setup, like figuring out track lengths, and
|
||||
constructing the Table of Contents.
|
||||
Perform initial setup.
|
||||
|
||||
Like figuring out track lengths, and constructing
|
||||
the Table of Contents.
|
||||
"""
|
||||
logger.debug('setup image start')
|
||||
verify = ImageVerifyTask(self)
|
||||
@@ -108,9 +113,7 @@ class Image:
|
||||
|
||||
|
||||
class ImageVerifyTask(task.MultiSeparateTask):
|
||||
"""
|
||||
I verify a disk image and get the necessary track lengths.
|
||||
"""
|
||||
"""Verify a disk image and get the necessary track lengths."""
|
||||
|
||||
logCategory = 'ImageVerifyTask'
|
||||
|
||||
@@ -174,9 +177,7 @@ class ImageVerifyTask(task.MultiSeparateTask):
|
||||
|
||||
|
||||
class ImageEncodeTask(task.MultiSeparateTask):
|
||||
"""
|
||||
I encode a disk image to a different format.
|
||||
"""
|
||||
"""Encode a disk image to a different format."""
|
||||
|
||||
description = "Encoding tracks"
|
||||
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
# 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
|
||||
from urllib.parse import urlunparse, urlencode
|
||||
@@ -54,19 +52,20 @@ CDTEXT_FIELDS = [
|
||||
|
||||
class Track:
|
||||
"""
|
||||
I represent a track entry in an Table.
|
||||
Represent a track entry in a Table.
|
||||
|
||||
:cvar number: track number (1-based)
|
||||
:vartype number: int
|
||||
:cvar audio: whether the track is audio
|
||||
:vartype audio: bool
|
||||
:vartype indexes: dict of number -> :any:`Index`
|
||||
:cvar isrc: ISRC code (12 alphanumeric characters)
|
||||
:vartype isrc: str
|
||||
:cvar cdtext: dictionary of CD Text information;
|
||||
:any:`see CDTEXT_KEYS`
|
||||
:vartype cdtext: str
|
||||
:cvar pre_emphasis: whether track is pre-emphasised
|
||||
:cvar number: track number (1-based)
|
||||
:vartype number: int
|
||||
:cvar audio: whether the track is audio
|
||||
:vartype audio: bool
|
||||
:cvar indexes: dict of number
|
||||
:vartype indexes: dict of number -> :any:`Index`
|
||||
:cvar isrc: ISRC code (12 alphanumeric characters)
|
||||
:vartype isrc: str
|
||||
:cvar cdtext: dictionary of CD Text information;
|
||||
:any:`see CDTEXT_KEYS`
|
||||
:vartype cdtext: str
|
||||
:cvar pre_emphasis: whether track is pre-emphasised
|
||||
:vartype pre_emphasis: bool
|
||||
"""
|
||||
|
||||
@@ -90,7 +89,19 @@ class Track:
|
||||
def index(self, number, absolute=None, path=None, relative=None,
|
||||
counter=None):
|
||||
"""
|
||||
:type path: str or None
|
||||
Instantiate Index object and store it in class variable.
|
||||
|
||||
:param number: index number
|
||||
:type number: int
|
||||
:param absolute: absolute index offset, in CD frames
|
||||
:type absolute: int or None
|
||||
:param path: path to track
|
||||
:type path: str or None
|
||||
:param relative: relative index offset, in CD frames
|
||||
:type relative: int or None
|
||||
:param counter: the source counter; updates for each different
|
||||
data source (silence or different file path)
|
||||
:type counter: int or None
|
||||
"""
|
||||
if path is not None:
|
||||
assert isinstance(path, str), "%r is not str" % path
|
||||
@@ -117,7 +128,7 @@ class Track:
|
||||
|
||||
def getPregap(self):
|
||||
"""
|
||||
Returns the length of the pregap for this track.
|
||||
Return 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.
|
||||
@@ -130,10 +141,15 @@ class Track:
|
||||
|
||||
class Index:
|
||||
"""
|
||||
Represent an index of a track on a CD.
|
||||
|
||||
:cvar counter: counter for the index source; distinguishes between
|
||||
the matching FILE lines in .cue files for example
|
||||
:vartype path: str or None
|
||||
:vartype counter: int
|
||||
:cvar path: path to track
|
||||
:vartype path: str or None
|
||||
"""
|
||||
|
||||
number = None
|
||||
absolute = None
|
||||
path = None
|
||||
@@ -159,13 +175,12 @@ class Index:
|
||||
|
||||
class Table:
|
||||
"""
|
||||
I represent a table of indexes on a CD.
|
||||
Represent a table of indexes on a CD.
|
||||
|
||||
:cvar tracks: tracks on this CD
|
||||
:vartype tracks: list of :any:`Track`
|
||||
:cvar catalog: catalog number
|
||||
:cvar tracks: tracks on this CD
|
||||
:vartype tracks: list(Track)
|
||||
:cvar catalog: catalog number
|
||||
:vartype catalog: str
|
||||
:vartype cdtext: dict of str -> str
|
||||
"""
|
||||
|
||||
tracks = None # list of Track
|
||||
@@ -193,22 +208,24 @@ class Table:
|
||||
|
||||
def getTrackStart(self, number):
|
||||
"""
|
||||
:param number: the track number, 1-based
|
||||
:type number: int
|
||||
Return the start of the given track number's index 1, in CD frames.
|
||||
|
||||
: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
|
||||
:rtype: int
|
||||
"""
|
||||
track = self.tracks[number - 1]
|
||||
return track.getIndex(1).absolute
|
||||
|
||||
def getTrackEnd(self, number):
|
||||
"""
|
||||
:param number: the track number, 1-based
|
||||
:type number: int
|
||||
Return the end of the given track number, in CD frames.
|
||||
|
||||
: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
|
||||
:rtype: int
|
||||
"""
|
||||
# default to end of disc
|
||||
end = self.leadout - 1
|
||||
@@ -231,24 +248,30 @@ class Table:
|
||||
|
||||
def getTrackLength(self, number):
|
||||
"""
|
||||
:param number: the track number, 1-based
|
||||
:type number: int
|
||||
Return the length, in CD frames, for the given track number.
|
||||
|
||||
:param number: the track number, 1-based
|
||||
:type number: int
|
||||
:returns: the length of the given track number, in CD frames
|
||||
:rtype: int
|
||||
:rtype: int
|
||||
"""
|
||||
return self.getTrackEnd(number) - self.getTrackStart(number) + 1
|
||||
|
||||
def getAudioTracks(self):
|
||||
"""
|
||||
:returns: the number of audio tracks on the CD
|
||||
:rtype: int
|
||||
Return the number of audio tracks on the disc.
|
||||
|
||||
:returns: the number of audio tracks on the disc
|
||||
:rtype: int
|
||||
"""
|
||||
return len([t for t in self.tracks if t.audio])
|
||||
|
||||
def hasDataTracks(self):
|
||||
"""
|
||||
:returns: whether this disc contains data tracks
|
||||
Return whether the disc contains data tracks.
|
||||
|
||||
:returns: whether the disc contains data tracks
|
||||
:rtype: bool
|
||||
"""
|
||||
return len([t for t in self.tracks if not t.audio]) > 0
|
||||
|
||||
@@ -266,12 +289,13 @@ class Table:
|
||||
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)
|
||||
|
||||
:rtype: list of int
|
||||
* CDDB disc id
|
||||
* number of audio tracks
|
||||
* offset of index 1 of each track
|
||||
* length of disc in seconds (including data track)
|
||||
|
||||
:rtype: list(int)
|
||||
"""
|
||||
offsets = []
|
||||
|
||||
@@ -323,8 +347,8 @@ class Table:
|
||||
"""
|
||||
Calculate the CDDB disc ID.
|
||||
|
||||
:rtype: str
|
||||
:returns: the 8-character hexadecimal disc ID
|
||||
:rtype: str
|
||||
"""
|
||||
values = self.getCDDBValues()
|
||||
return "%08x" % int(values)
|
||||
@@ -333,8 +357,8 @@ class Table:
|
||||
"""
|
||||
Calculate the MusicBrainz disc ID.
|
||||
|
||||
:rtype: str
|
||||
:returns: the 28-character base64-encoded disc ID
|
||||
:rtype: str
|
||||
"""
|
||||
if self.mbdiscid:
|
||||
logger.debug('getMusicBrainzDiscId: returning cached %r',
|
||||
@@ -367,9 +391,10 @@ class Table:
|
||||
|
||||
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
|
||||
:type data: bool
|
||||
"""
|
||||
# the 'real' leadout, not offset by 150 frames
|
||||
if data:
|
||||
@@ -384,9 +409,7 @@ class Table:
|
||||
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):
|
||||
@@ -394,12 +417,13 @@ class Table:
|
||||
Get all MusicBrainz values needed to calculate disc id and submit URL.
|
||||
|
||||
This includes:
|
||||
- track number of first track
|
||||
- number of audio tracks
|
||||
- leadout of disc
|
||||
- offset of index 1 of each track
|
||||
|
||||
:rtype: list of int
|
||||
* track number of first track
|
||||
* number of audio tracks
|
||||
* leadout of disc
|
||||
* offset of index 1 of each track
|
||||
|
||||
:rtype: list(int)
|
||||
"""
|
||||
# MusicBrainz disc id does not take into account data tracks
|
||||
|
||||
@@ -447,12 +471,13 @@ class Table:
|
||||
|
||||
def cue(self, cuePath='', program='whipper'):
|
||||
"""
|
||||
:param cuePath: path to the cue file to be written. If empty,
|
||||
will treat paths as if in current directory.
|
||||
|
||||
|
||||
Dump our internal representation to a .cue file content.
|
||||
|
||||
:param cuePath: path to the cue file to be written. If empty,
|
||||
will treat paths as if in current directory
|
||||
:type cuePath: unicode
|
||||
:param program: name of the program (ripping software)
|
||||
:type program: str
|
||||
:rtype: str
|
||||
"""
|
||||
logger.debug('generating .cue for cuePath %r', cuePath)
|
||||
@@ -582,6 +607,7 @@ class Table:
|
||||
def clearFiles(self):
|
||||
"""
|
||||
Clear all file backings.
|
||||
|
||||
Resets indexes paths and relative offsets.
|
||||
"""
|
||||
# FIXME: do a loop over track indexes better, with a pythonic
|
||||
@@ -604,14 +630,24 @@ class Table:
|
||||
|
||||
def setFile(self, track, index, path, length, counter=None):
|
||||
"""
|
||||
Sets the given file as the source from the given index on.
|
||||
Set 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.
|
||||
|
||||
:type track: int
|
||||
:type index: int
|
||||
:param track: track number, 1-based
|
||||
:type track: int
|
||||
:param index: index of the track
|
||||
:type index: int
|
||||
:param path: path to track
|
||||
:type path: unicode
|
||||
:param length: length of the given track, in CD frames
|
||||
:type length: int
|
||||
:param counter: counter for the index source; distinguishes between
|
||||
the matching FILE lines in .cue files for example
|
||||
:type counter: int or None
|
||||
"""
|
||||
logger.debug('setFile: track %d, index %d, path %r, length %r, '
|
||||
'counter %r', track, index, path, length, counter)
|
||||
@@ -640,6 +676,7 @@ class Table:
|
||||
def absolutize(self):
|
||||
"""
|
||||
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
|
||||
@@ -677,11 +714,14 @@ class Table:
|
||||
|
||||
def merge(self, other, session=2):
|
||||
"""
|
||||
Merges the given table at the end.
|
||||
Merge the given table at the end.
|
||||
|
||||
The other table is assumed to be from an additional session,
|
||||
|
||||
|
||||
:type other: Table
|
||||
:param other: session table
|
||||
:type other: Table
|
||||
:param session: session number
|
||||
:type session: int
|
||||
"""
|
||||
gap = self._getSessionGap(session)
|
||||
|
||||
@@ -729,10 +769,11 @@ class Table:
|
||||
Return the next track and index.
|
||||
|
||||
:param track: track number, 1-based
|
||||
|
||||
:type track: int
|
||||
:raises IndexError: on last index
|
||||
|
||||
:rtype: tuple of (int, int)
|
||||
:rtype: tuple(int, int)
|
||||
:param index: index of the next track
|
||||
:type index: int
|
||||
"""
|
||||
t = self.tracks[track - 1]
|
||||
indexes = list(t.indexes)
|
||||
@@ -756,7 +797,8 @@ class 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
|
||||
|
||||
A TOC is a list of all tracks and their Index 01, with absolute
|
||||
offsets, as well as the leadout.
|
||||
"""
|
||||
if not self.leadout:
|
||||
@@ -775,8 +817,11 @@ class Table:
|
||||
|
||||
def accuraterip_ids(self):
|
||||
"""
|
||||
returns both AccurateRip disc ids as a tuple of 8-char
|
||||
hexadecimal strings (discid1, discid2)
|
||||
Return both AccurateRip disc ids.
|
||||
|
||||
:returns: both AccurateRip disc ids as a tuple of 8-char
|
||||
hexadecimal strings
|
||||
:rtype: tuple(str, str)
|
||||
"""
|
||||
# AccurateRip does not take into account data tracks,
|
||||
# but does count the data track to determine the leadout offset
|
||||
@@ -809,9 +854,7 @@ class Table:
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
# along with whipper. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Reading .toc files
|
||||
Read .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
|
||||
@@ -93,7 +93,8 @@ _INDEX_RE = re.compile(r"""
|
||||
|
||||
class Sources:
|
||||
"""
|
||||
I represent the list of sources used in the .toc file.
|
||||
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.
|
||||
"""
|
||||
@@ -103,19 +104,22 @@ class Sources:
|
||||
|
||||
def append(self, counter, offset, source):
|
||||
"""
|
||||
Append ``(counter, offset, source)`` tuple to the ``sources`` list.
|
||||
|
||||
: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 counter: int
|
||||
:param offset: the absolute disc offset where this source starts
|
||||
:type offset: int
|
||||
:param source: data source
|
||||
:type source: File or None
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
"""Retrieve the source used at the given offset."""
|
||||
for i, (_, o, _) in enumerate(self._sources):
|
||||
if offset < o:
|
||||
return self._sources[i - 1]
|
||||
@@ -124,7 +128,11 @@ class Sources:
|
||||
|
||||
def getCounterStart(self, counter):
|
||||
"""
|
||||
Retrieve the absolute offset of the first source for this counter
|
||||
Retrieve the absolute offset of the first source for this counter.
|
||||
|
||||
:param counter: the source counter; updates for each different
|
||||
data source (silence or different file path)
|
||||
:type counter: int
|
||||
"""
|
||||
for i, (c, _, _) in enumerate(self._sources):
|
||||
if c == counter:
|
||||
@@ -137,7 +145,10 @@ class TocFile:
|
||||
|
||||
def __init__(self, path):
|
||||
"""
|
||||
:type path: str
|
||||
Init TocFile.
|
||||
|
||||
:param path: path to track
|
||||
:type path: str
|
||||
"""
|
||||
assert isinstance(path, str), "%r is not str" % path
|
||||
self._path = path
|
||||
@@ -379,14 +390,19 @@ class TocFile:
|
||||
"""
|
||||
Add a message about a given line in the cue file.
|
||||
|
||||
:param number: line number, counting from 0.
|
||||
:param number: line number, counting from 0
|
||||
:type number: int
|
||||
:param message: a text line in the cue sheet
|
||||
:type message: str
|
||||
"""
|
||||
self._messages.append((number + 1, message))
|
||||
|
||||
def getTrackLength(self, track):
|
||||
"""
|
||||
Returns the length of the given track, from its INDEX 01 to the next
|
||||
track's INDEX 01
|
||||
Return the length of the given track, in CD frames.
|
||||
|
||||
The track length is calculated 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
|
||||
@@ -411,22 +427,25 @@ class TocFile:
|
||||
"""
|
||||
Translate the .toc's FILE to an existing path.
|
||||
|
||||
:type path: str
|
||||
:param path: path to track
|
||||
:type path: str
|
||||
"""
|
||||
return common.getRealPath(self._path, path)
|
||||
|
||||
|
||||
class File:
|
||||
"""
|
||||
I represent a FILE line in a .toc file.
|
||||
"""
|
||||
"""Represent a FILE line in a .toc file."""
|
||||
|
||||
def __init__(self, path, start, length):
|
||||
"""
|
||||
:type path: str
|
||||
:type start: int
|
||||
:param start: starting point for the track in this file, in frames
|
||||
Init File.
|
||||
|
||||
:param path: path to track
|
||||
:type path: unicode
|
||||
:param start: starting point for the track in this file, in frames
|
||||
:type start: int
|
||||
:param length: length for the track in this file, in frames
|
||||
:type length: int
|
||||
"""
|
||||
assert isinstance(path, str), "%r is not str" % path
|
||||
|
||||
|
||||
@@ -37,13 +37,10 @@ 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
|
||||
@@ -51,9 +48,7 @@ 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, )
|
||||
@@ -88,10 +83,12 @@ class ProgressParser:
|
||||
|
||||
def __init__(self, start, stop):
|
||||
"""
|
||||
:param start: first frame to rip
|
||||
:type start: int
|
||||
:param stop: last frame to rip (inclusive)
|
||||
:type stop: int
|
||||
Init ProgressParser.
|
||||
|
||||
: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
|
||||
@@ -102,9 +99,7 @@ class ProgressParser:
|
||||
self._reads = {} # read count for each sector
|
||||
|
||||
def parse(self, line):
|
||||
"""
|
||||
Parse a line.
|
||||
"""
|
||||
"""Parse a line."""
|
||||
m = _PROGRESS_RE.search(line)
|
||||
if m:
|
||||
# code = int(m.group('code'))
|
||||
@@ -185,6 +180,7 @@ class ProgressParser:
|
||||
def getTrackQuality(self):
|
||||
"""
|
||||
Each frame gets read twice.
|
||||
|
||||
More than two reads for a frame reduce track quality.
|
||||
"""
|
||||
frames = self.stop - self.start + 1 # + 1 since stop is inclusive
|
||||
@@ -203,9 +199,7 @@ class ProgressParser:
|
||||
|
||||
|
||||
class ReadTrackTask(task.Task):
|
||||
"""
|
||||
I am a task that reads a track using cdparanoia.
|
||||
"""
|
||||
"""Task that reads a track using cdparanoia."""
|
||||
|
||||
description = "Reading track"
|
||||
quality = None # set at end of reading
|
||||
@@ -219,22 +213,22 @@ class ReadTrackTask(task.Task):
|
||||
"""
|
||||
Read the given track.
|
||||
|
||||
:param path: where to store the ripped track
|
||||
:type path: str
|
||||
:param table: table of contents of CD
|
||||
:type table: table.Table
|
||||
:param start: first frame to rip
|
||||
:type start: int
|
||||
:param stop: last frame to rip (inclusive); >= start
|
||||
:type stop: int
|
||||
:param path: where to store the ripped track
|
||||
:type path: str
|
||||
:param table: table of contents of CD
|
||||
:type table: 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
|
||||
:type offset: int
|
||||
:param device: the device to rip from
|
||||
:type device: str
|
||||
: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
|
||||
:type action: str
|
||||
:param what: a string representing what's being read; e.g. Track
|
||||
:type what: str
|
||||
"""
|
||||
assert isinstance(path, str), "%r is not str" % path
|
||||
|
||||
@@ -395,22 +389,23 @@ class ReadTrackTask(task.Task):
|
||||
|
||||
class ReadVerifyTrackTask(task.MultiSeparateTask):
|
||||
"""
|
||||
I am a task that reads and verifies a track using cdparanoia.
|
||||
I also encode the track.
|
||||
Task that reads and verifies a track using cdparanoia.
|
||||
|
||||
It also encodes 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.
|
||||
:cvar testchecksum: the test checksum of the track.
|
||||
:cvar copychecksum: the copy checksum of the track.
|
||||
:cvar testspeed: the test speed of the track, as a multiple of
|
||||
track duration.
|
||||
:cvar copyspeed: the copy speed of the track, as a multiple of
|
||||
track duration.
|
||||
:cvar testduration: the test duration of the track, in seconds.
|
||||
:cvar copyduration: the copy duration of the track, in seconds.
|
||||
:cvar peak: the peak level of the track
|
||||
:cvar checksum: the checksum of the track; set if they match
|
||||
:cvar testchecksum: the test checksum of the track
|
||||
:cvar copychecksum: the copy checksum of the track
|
||||
:cvar testspeed: the test speed of the track, as a multiple of
|
||||
track duration
|
||||
:cvar copyspeed: the copy speed of the track, as a multiple of
|
||||
track duration
|
||||
:cvar testduration: the test duration of the track, in seconds
|
||||
:cvar copyduration: the copy duration of the track, in seconds
|
||||
:cvar peak: the peak level of the track
|
||||
"""
|
||||
|
||||
checksum = None
|
||||
@@ -429,20 +424,22 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
|
||||
def __init__(self, path, table, start, stop, overread, offset=0,
|
||||
device=None, taglist=None, what="track", coverArtPath=None):
|
||||
"""
|
||||
:param path: where to store the ripped track
|
||||
:type path: str
|
||||
:param table: table of contents of CD
|
||||
:type table: 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
|
||||
Init ReadVerifyTrackTask.
|
||||
|
||||
:param path: where to store the ripped track
|
||||
:type path: str
|
||||
:param table: table of contents of CD
|
||||
:type table: 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
|
||||
:type taglist: dict
|
||||
"""
|
||||
task.MultiSeparateTask.__init__(self)
|
||||
|
||||
|
||||
@@ -59,24 +59,22 @@ class ProgressParser:
|
||||
|
||||
|
||||
class ReadTOCTask(task.Task):
|
||||
"""
|
||||
Task that reads the TOC of the disc using cdrdao
|
||||
"""
|
||||
"""Task that reads the TOC of the disc using cdrdao."""
|
||||
|
||||
description = "Reading TOC"
|
||||
toc = None
|
||||
|
||||
def __init__(self, device, fast_toc=False, toc_path=None):
|
||||
"""
|
||||
Read the TOC for 'device'.
|
||||
Read the TOC for ``device``.
|
||||
|
||||
:param device: block device to read TOC from
|
||||
:type device: str
|
||||
:param fast_toc: If to use fast-toc cdrdao mode
|
||||
:type fast_toc: bool
|
||||
:param toc_path: Where to save TOC if wanted.
|
||||
:type toc_path: str
|
||||
:param device: block device to read TOC from
|
||||
:type device: str
|
||||
:param fast_toc: whether to use fast-toc cdrdao mode
|
||||
:type fast_toc: bool
|
||||
:param toc_path: where to save TOC if wanted
|
||||
:type toc_path: str
|
||||
"""
|
||||
|
||||
self.device = device
|
||||
self.fast_toc = fast_toc
|
||||
self.toc_path = toc_path
|
||||
@@ -161,9 +159,7 @@ class ReadTOCTask(task.Task):
|
||||
|
||||
|
||||
def DetectCdr(device):
|
||||
"""
|
||||
Return whether cdrdao detects a CD-R for 'device'.
|
||||
"""
|
||||
"""Whether cdrdao detects a CD-R for ``device``."""
|
||||
cmd = [CDRDAO, 'disk-info', '-v1', '--device', device]
|
||||
logger.debug("executing %r", cmd)
|
||||
p = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
||||
@@ -171,9 +167,7 @@ def DetectCdr(device):
|
||||
|
||||
|
||||
def version():
|
||||
"""
|
||||
Return cdrdao version as a string.
|
||||
"""
|
||||
"""Return cdrdao version as a string."""
|
||||
cdrdao = Popen(CDRDAO, stderr=PIPE)
|
||||
_, err = cdrdao.communicate()
|
||||
if cdrdao.returncode != 1:
|
||||
|
||||
@@ -6,8 +6,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def encode(infile, outfile):
|
||||
"""
|
||||
Encodes infile to outfile, with flac.
|
||||
Uses '-f' because whipper already creates the file.
|
||||
Encode 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
|
||||
|
||||
@@ -9,10 +9,10 @@ SOX = 'sox'
|
||||
|
||||
def peak_level(track_path):
|
||||
"""
|
||||
Accepts a path to a sox-decodable audio file.
|
||||
Accept a path to a sox-decodable audio file.
|
||||
|
||||
Returns track peak level from sox ('maximum amplitude') as a float.
|
||||
Returns None on error.
|
||||
:returns: track peak level from sox ('maximum amplitude')
|
||||
:rtype: float or None
|
||||
"""
|
||||
if not os.path.exists(track_path):
|
||||
logger.warning("SoX peak detection failed: file not found")
|
||||
|
||||
@@ -11,17 +11,22 @@ SOXI = 'soxi'
|
||||
|
||||
class AudioLengthTask(ctask.PopenTask):
|
||||
"""
|
||||
I calculate the length of a track in audio samples.
|
||||
Calculate the length of a track in audio samples.
|
||||
|
||||
:cvar length: length of the decoded audio file, in audio samples.
|
||||
:cvar length: length of the decoded audio file, in audio samples
|
||||
:vartype length: int
|
||||
"""
|
||||
|
||||
logCategory = 'AudioLengthTask'
|
||||
description = 'Getting length of audio track'
|
||||
length = None
|
||||
|
||||
def __init__(self, path):
|
||||
"""
|
||||
:type path: str
|
||||
Init AudioLengthTask.
|
||||
|
||||
:param path: path to audio track
|
||||
:type path: str
|
||||
"""
|
||||
assert isinstance(path, str), "%r is not str" % path
|
||||
|
||||
|
||||
@@ -6,9 +6,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def eject_device(device):
|
||||
"""
|
||||
Eject the given device.
|
||||
"""
|
||||
"""Eject the given device."""
|
||||
logger.debug("ejecting device %s", device)
|
||||
try:
|
||||
# `eject device` prints nothing to stdout
|
||||
@@ -19,9 +17,7 @@ def eject_device(device):
|
||||
|
||||
|
||||
def load_device(device):
|
||||
"""
|
||||
Load the given device.
|
||||
"""
|
||||
"""Load the given device."""
|
||||
logger.debug("loading (eject -t) device %s", device)
|
||||
try:
|
||||
# `eject -t device` prints nothing to stdout
|
||||
@@ -34,8 +30,9 @@ def load_device(device):
|
||||
|
||||
def unmount_device(device):
|
||||
"""
|
||||
Unmount the given device if it is mounted, as happens with automounted
|
||||
data tracks.
|
||||
Unmount the given device if it is mounted.
|
||||
|
||||
This usually happens with automounted data tracks.
|
||||
|
||||
If the given device is a symlink, the target will be checked.
|
||||
"""
|
||||
|
||||
@@ -17,13 +17,11 @@ class WhipperLogger(result.Logger):
|
||||
_errors = False
|
||||
|
||||
def log(self, ripResult, epoch=time.time()):
|
||||
"""Returns big str: logfile joined text lines"""
|
||||
|
||||
"""Return logfile as string."""
|
||||
return self.logRip(ripResult, epoch)
|
||||
|
||||
def logRip(self, ripResult, epoch):
|
||||
"""Returns logfile lines list"""
|
||||
|
||||
"""Return logfile as list of lines."""
|
||||
riplog = OrderedDict()
|
||||
|
||||
# Ripper version
|
||||
@@ -189,8 +187,7 @@ class WhipperLogger(result.Logger):
|
||||
return riplog
|
||||
|
||||
def trackLog(self, trackResult):
|
||||
"""Returns Tracks section lines: data picked from trackResult"""
|
||||
|
||||
"""Return Tracks section lines: data picked from trackResult."""
|
||||
track = OrderedDict()
|
||||
|
||||
# Filename (including path) of ripped track
|
||||
|
||||
@@ -41,12 +41,13 @@ class TrackResult:
|
||||
|
||||
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
|
||||
Init TrackResult.
|
||||
|
||||
DBMaxConfidence: track's maximum confidence in the AccurateRip DB
|
||||
DBMaxConfidenceCRC: maximum confidence CRC
|
||||
* 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': {
|
||||
@@ -66,20 +67,19 @@ class TrackResult:
|
||||
|
||||
class RipResult:
|
||||
"""
|
||||
I hold information about the result for rips.
|
||||
I can be used to write log files.
|
||||
Hold information about the result for rips.
|
||||
|
||||
It can be used to write log files.
|
||||
|
||||
:cvar offset: sample read offset
|
||||
:cvar table: the full index table
|
||||
:vartype table: whipper.image.table.Table
|
||||
:cvar table: the full index table
|
||||
:vartype table: whipper.image.table.Table
|
||||
:cvar metadata: disc metadata from MusicBrainz (if available)
|
||||
:vartype metadata: whipper.common.mbngs.DiscMetadata
|
||||
|
||||
:cvar vendor: vendor of the CD drive
|
||||
:cvar model: model of the CD drive
|
||||
:cvar vendor: vendor of the CD drive
|
||||
:cvar model: model of the CD drive
|
||||
:cvar release: release of the CD drive
|
||||
|
||||
:cvar cdrdaoVersion: version of cdrdao used for the rip
|
||||
:cvar cdrdaoVersion: version of cdrdao used for the rip
|
||||
:cvar cdparanoiaVersion: version of cdparanoia used for the rip
|
||||
"""
|
||||
|
||||
@@ -107,9 +107,11 @@ class RipResult:
|
||||
|
||||
def getTrackResult(self, number):
|
||||
"""
|
||||
:param number: the track number (0 for HTOA)
|
||||
Return TrackResult for the given track number.
|
||||
|
||||
:type number: int
|
||||
:param number: the track number (0 for HTOA)
|
||||
:type number: int
|
||||
:returns: TrackResult for the given track number
|
||||
:rtype: TrackResult
|
||||
"""
|
||||
for t in self.tracks:
|
||||
@@ -120,18 +122,15 @@ class RipResult:
|
||||
|
||||
|
||||
class Logger:
|
||||
"""
|
||||
I log the result of a rip.
|
||||
"""
|
||||
"""Log the result of a rip."""
|
||||
|
||||
def log(self, ripResult, epoch=time.time()):
|
||||
"""
|
||||
Create a log from the given ripresult.
|
||||
|
||||
:param epoch: when the log file gets generated
|
||||
:type epoch: float
|
||||
:type ripResult: RipResult
|
||||
|
||||
:param epoch: when the log file gets generated
|
||||
:type epoch: float
|
||||
:type ripResult: RipResult
|
||||
:rtype: str
|
||||
"""
|
||||
raise NotImplementedError
|
||||
@@ -151,9 +150,9 @@ class EntryPoint:
|
||||
|
||||
def getLoggers():
|
||||
"""
|
||||
Get all logger plugins with entry point 'whipper.logger'.
|
||||
Get all logger plugins with entry point ``whipper.logger``.
|
||||
|
||||
:rtype: dict of :class:`str` -> :any:`Logger`
|
||||
:rtype: dict(str, Logger)
|
||||
"""
|
||||
d = {}
|
||||
|
||||
|
||||
@@ -66,8 +66,9 @@ class TestCase(unittest.TestCase):
|
||||
@staticmethod
|
||||
def readCue(name):
|
||||
"""
|
||||
Read a .cue file, and replace the version comment with the current
|
||||
version so we can use it in comparisons.
|
||||
Read a .cue file replacing the version comment with the current value.
|
||||
|
||||
So that it can be used in comparisons.
|
||||
"""
|
||||
cuefile = os.path.join(os.path.dirname(__file__), name)
|
||||
with open(cuefile) as f:
|
||||
|
||||
@@ -158,10 +158,10 @@ class MetadataTestCase(unittest.TestCase):
|
||||
|
||||
def testUnknownArtist(self):
|
||||
"""
|
||||
check the received metadata for artists tagged with [unknown]
|
||||
and artists tagged with an alias in MusicBrainz
|
||||
Check the received metadata for artists tagged with [unknown]
|
||||
and artists tagged with an alias in MusicBrainz.
|
||||
|
||||
see https://github.com/whipper-team/whipper/issues/155
|
||||
See https://github.com/whipper-team/whipper/issues/155
|
||||
"""
|
||||
# Using: CunninLynguists - Sloppy Seconds, Volume 1
|
||||
# https://musicbrainz.org/release/8478d4da-0cda-4e46-ae8c-1eeacfa5cf37
|
||||
@@ -199,8 +199,8 @@ class MetadataTestCase(unittest.TestCase):
|
||||
|
||||
def testNenaAndKimWildSingle(self):
|
||||
"""
|
||||
check the received metadata for artists that differ between
|
||||
named on release and named in recording
|
||||
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