35
.travis.yml
Normal file
35
.travis.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
sudo: required
|
||||
|
||||
language: bash
|
||||
|
||||
branches:
|
||||
only:
|
||||
- fork
|
||||
|
||||
install:
|
||||
# Dependencies
|
||||
- sudo apt-get update -qq
|
||||
- sudo pip install --upgrade pip
|
||||
- sudo apt-get install -qq cdparanoia cdrdao gstreamer0.10-plugins-base gstreamer0.10-plugins-good libcdio-dev libiso9660-dev python-cddb python-gobject swig
|
||||
- sudo pip install musicbrainzngs pycdio
|
||||
|
||||
# Testing dependencies
|
||||
- sudo apt-get install -qq python-gst0.10
|
||||
- sudo pip install twisted
|
||||
|
||||
# Checkout
|
||||
- ./autogen.sh
|
||||
|
||||
# Building
|
||||
- ./configure
|
||||
- make
|
||||
|
||||
# Installing
|
||||
- sudo make install
|
||||
|
||||
# Check flacenc availability
|
||||
- sudo apt-get install -qq gstreamer0.10-tools
|
||||
- gst-inspect-0.10 flacenc
|
||||
|
||||
script:
|
||||
- python -m unittest discover
|
||||
99
README.md
Normal file → Executable file
99
README.md
Normal file → Executable file
@@ -1,12 +1,33 @@
|
||||
morituri is a CD ripper aiming for accuracy over speed for UNIX systems.
|
||||
Its features are modeled to compare with Exact Audio Copy on Windows.
|
||||
The home page is https://thomas.apestaart.org/morituri/trac/
|
||||
FORK INFORMATIONS
|
||||
---------
|
||||
The name of this fork is still to be decided, right now I'll be using whipper.
|
||||
|
||||
This branch is very close to morituri's master one (the internal 'morituri' references are still unchanged), I've just merged the following commits:
|
||||
- [#79](https://github.com/thomasvs/morituri/issues/79)
|
||||
- [#92](https://github.com/thomasvs/morituri/issues/92)
|
||||
- [#109](https://github.com/thomasvs/morituri/issues/109)
|
||||
- [#133](https://github.com/thomasvs/morituri/issues/133) (with custom `.travis.yml`)
|
||||
- [#137](https://github.com/thomasvs/morituri/issues/137)
|
||||
- [#139](https://github.com/thomasvs/morituri/issues/139)
|
||||
- [#140](https://github.com/thomasvs/morituri/issues/140)
|
||||
- [#141](https://github.com/thomasvs/morituri/issues/141)
|
||||
|
||||
And changed the default logger to the morituri-whatlogger's one.
|
||||
|
||||
WHIPPER [](https://travis-ci.org/JoeLametta/whipper)
|
||||
---------
|
||||
whipper is a fork of the morituri project (CDDA ripper, for *nix systems, aiming for accuracy over speed).
|
||||
|
||||
It improves morituri which development seems to have halted/slowed down merging old pull requests and improving it with new functions.
|
||||
|
||||
If possible, I'll try to mainline the useful commits of this fork but, in the future, this may not be possible because of different project choices.
|
||||
|
||||
The home page is still TBD.
|
||||
|
||||
RATIONALE
|
||||
---------
|
||||
For a more detailed rationale, see my wiki page ['The Art of the Rip'](
|
||||
https://thomas.apestaart.org/thomas/trac/wiki/DAD/Rip).
|
||||
For a more detailed rationale, see morituri's wiki page ['The Art of the Rip'](
|
||||
http://thomas.apestaart.org/thomas/trac/wiki/DAD/Rip).
|
||||
|
||||
FEATURES
|
||||
--------
|
||||
@@ -14,7 +35,7 @@ FEATURES
|
||||
* support for AccurateRip (V1) verification
|
||||
* detects sample read offset and ability to defeat cache of drives
|
||||
* performs test and copy rip
|
||||
* detects and rips Hidden Track One Audio
|
||||
* detects and rips Hidden Track One Audio (only if not digitally silent)
|
||||
* templates for file and directory naming
|
||||
* support for lossless encoding and lossy encoding or re-encoding of images
|
||||
* tagging using GStreamer, including embedding MusicBrainz id's
|
||||
@@ -27,29 +48,30 @@ REQUIREMENTS
|
||||
- cdparanoia, for the actual ripping
|
||||
- cdrdao, for session, TOC, pregap, and ISRC extraction
|
||||
- GStreamer and its python bindings, for encoding
|
||||
- gst-plugins-base >= 0.10.22 for appsink
|
||||
- gstreamer0.10-base-plugins >= 0.10.22 for appsink
|
||||
- gstreamer0.10-good-plugins for wav encoding (it depends on the Linux distro used)
|
||||
- python musicbrainz2, for metadata lookup
|
||||
- python-setuptools, for plugin support
|
||||
- python-cddb, for showing but not using disc info if not in musicbrainz
|
||||
- python-cddb, for showing but not using disc info if not in MusicBrainz
|
||||
- pycdio, for drive identification (optional)
|
||||
- Required for drive offset and caching behaviour to be stored in the config file
|
||||
- Required for drive offset and caching behavior to be stored in the config file
|
||||
|
||||
Additionally, if you're building from a git checkout:
|
||||
- autoconf
|
||||
- automake
|
||||
|
||||
GETTING MORITURI
|
||||
GETTING WHIPPER
|
||||
----------------
|
||||
If you are building from a source tarball or checkout, you can choose to
|
||||
use morituri installed or uninstalled.
|
||||
use whipper installed or uninstalled.
|
||||
|
||||
- getting:
|
||||
- Change to a directory where you want to put the morituri source code
|
||||
- Change to a directory where you want to put the whipper source code
|
||||
(For example, `$HOME/dev/ext` or `$HOME/prefix/src`)
|
||||
- source: download tarball, unpack, and change to its directory
|
||||
- checkout:
|
||||
|
||||
git clone git://github.com/thomasvs/morituri.git
|
||||
git clone -b fork --single-branch git://github.com/JoeLametta/morituri.git
|
||||
cd morituri
|
||||
git submodule init
|
||||
git submodule update
|
||||
@@ -66,14 +88,13 @@ use morituri installed or uninstalled.
|
||||
|
||||
make install
|
||||
|
||||
- running uninstalled:
|
||||
- running uninstalled (within the make directory):
|
||||
|
||||
ln -sf `pwd`/misc/morituri-uninstalled $HOME/bin/morituri-git
|
||||
morituri-git # this drops you in a shell where everything is set up to use morituri
|
||||
./misc/morituri-uninstalled rip <commands>
|
||||
|
||||
RUNNING MORITURI
|
||||
RUNNING WHIPPER
|
||||
----------------
|
||||
morituri currently only has a command-line interface called 'rip'
|
||||
whipper currently only has a command-line interface called 'rip'
|
||||
|
||||
rip is self-documenting.
|
||||
`rip -h` gives you the basic instructions.
|
||||
@@ -96,8 +117,7 @@ Check the man page (rip(1)) for more information.
|
||||
|
||||
RUNNING UNINSTALLED
|
||||
-------------------
|
||||
|
||||
To make it easier for developers, you can run morituri straight from the
|
||||
To make it easier for developers, you can run whipper straight from the
|
||||
source checkout:
|
||||
|
||||
./autogen.sh
|
||||
@@ -127,8 +147,10 @@ The simplest way to get started making accurate rips is:
|
||||
|
||||
FILING BUGS
|
||||
-----------
|
||||
morituri's bug tracker is at [https://thomas.apestaart.org/morituri/trac/](
|
||||
https://thomas.apestaart.org/morituri/trac/).
|
||||
whipper's bug tracker is still TBD.
|
||||
|
||||
morituri's bug tracker is at [http://thomas.apestaart.org/morituri/trac/](
|
||||
http://thomas.apestaart.org/morituri/trac/).
|
||||
When filing bugs, please run the failing command with the environment variable
|
||||
`RIP_DEBUG` set; for example:
|
||||
|
||||
@@ -141,10 +163,10 @@ KNOWN ISSUES
|
||||
------------
|
||||
- no GUI yet
|
||||
- only AccurateRip V1 CRCs are computed and checked against the online database
|
||||
- `rip offset find` fails to delete the temporary .wav files it creates if error occurs while ripping (thomasvs/morituri#75)
|
||||
- `rip offset find` fails to delete the temporary .wav files it creates if an error occurs while ripping
|
||||
- morituri detects the pre-emphasis flag in the TOC but doesn't add it to the cue sheet
|
||||
- To improve the accuracy of the detection the sub-channel data should be scanned too
|
||||
- CD-Text is not used when ripping CDs not available in MusicBrainz DB
|
||||
- To improve the accuracy of the detection, the sub-channel data should be scanned too
|
||||
- cd-text isn't read from the CD (useful when the CD informations are not available in the MusicBrainz DB)
|
||||
|
||||
GOALS
|
||||
-----
|
||||
@@ -155,7 +177,6 @@ GOALS
|
||||
|
||||
CONFIGURATION FILE
|
||||
------------------
|
||||
|
||||
The configuration file is stored according to [XDG Base Directory Specification](
|
||||
http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html)
|
||||
when possible.
|
||||
@@ -163,21 +184,33 @@ when possible.
|
||||
It lives in `$XDG_CONFIG_HOME/morituri/morituri.conf`
|
||||
|
||||
The configuration file follows python's ConfigParser syntax.
|
||||
There is a "main" section and zero or more sections starting with "drive:"
|
||||
|
||||
- main section:
|
||||
The possible sections are:
|
||||
|
||||
- main section: [main]
|
||||
- `path_filter_fat`: whether to filter path components for FAT file systems
|
||||
- `path_filter_special`: whether to filter path components for special
|
||||
characters
|
||||
|
||||
- drive section:
|
||||
- drive section: [drive:IDENTIFIER], one for each configured drive
|
||||
All these values are probed by morituri and should not be edited by hand.
|
||||
- `defeats_cache`: whether this drive can defeat the audio cache
|
||||
- `read_offset`: the read offset of the drive
|
||||
|
||||
- rip command section: [rip.COMMAND.SUBCOMMAND]
|
||||
Can be used to change the command options default values.
|
||||
|
||||
Example section to configure "rip cd rip" defaults:
|
||||
|
||||
[rip.cd.rip]
|
||||
unknown = True
|
||||
output_directory = ~/My Music
|
||||
track_template = new/%%A/%%y - %%d/%%t - %%n
|
||||
disc_template = %(track_template)s
|
||||
profile = flac
|
||||
|
||||
Note: to get a literal '%' character it must be doubled.
|
||||
|
||||
CONTRIBUTING
|
||||
------------
|
||||
- Please send pull requests through github.
|
||||
- You can always [flattr morituri to donate](https://flattr.com/submit/auto?%20%20user_id=thomasvs&url=https://thomas.apestaart.org/morituri/trac/&%20%20title=morituri&%20%20description=morituri&%20%20language=en_GB&tags=flattr,morituri,software&category=software)
|
||||
|
||||
|
||||
- Please send pull requests through GitHub.
|
||||
|
||||
@@ -311,16 +311,18 @@ class AccurateRipChecksumTask(ChecksumTask):
|
||||
self.debug('skipping frame %d', self._discFrameCounter)
|
||||
return checksum
|
||||
|
||||
# self._bytes is updated after do_checksum_buffer
|
||||
factor = self._bytes / 4 + 1
|
||||
values = struct.unpack("<%dI" % (len(buf) / 4), buf)
|
||||
for i, value in enumerate(values):
|
||||
# self._bytes is updated after do_checksum_buffer
|
||||
checksum += (self._bytes / 4 + i + 1) * value
|
||||
checksum &= 0xFFFFFFFF
|
||||
for value in values:
|
||||
checksum += factor * value
|
||||
factor += 1
|
||||
# offset = self._bytes / 4 + i + 1
|
||||
# if offset % common.SAMPLES_PER_FRAME == 0:
|
||||
# print 'frame %d, ends before %d, last value %08x, CRC %08x' % (
|
||||
# offset / common.SAMPLES_PER_FRAME, offset, value, sum)
|
||||
|
||||
checksum &= 0xFFFFFFFF
|
||||
return checksum
|
||||
|
||||
|
||||
|
||||
@@ -34,6 +34,33 @@ class LogCommand(command.Command, log.Loggable):
|
||||
command.Command.__init__(self, parentCommand, **kwargs)
|
||||
self.logCategory = self.name
|
||||
|
||||
def parse(self, argv):
|
||||
cmd = self.getRootCommand()
|
||||
if hasattr(cmd, 'config'):
|
||||
config = cmd.config
|
||||
# find section name
|
||||
cmd = self
|
||||
section = []
|
||||
while cmd is not None:
|
||||
section.insert(0, cmd.name)
|
||||
cmd = cmd.parentCommand
|
||||
section = '.'.join(section)
|
||||
# get defaults from config
|
||||
defaults = {}
|
||||
for opt in self.parser.option_list:
|
||||
if opt.dest is None:
|
||||
continue
|
||||
if 'string' == opt.type:
|
||||
val = config.get(section, opt.dest)
|
||||
elif opt.action in ('store_false', 'store_true'):
|
||||
val = config.getboolean(section, opt.dest)
|
||||
else:
|
||||
val = None
|
||||
if val is not None:
|
||||
defaults[opt.dest] = val
|
||||
self.parser.set_defaults(**defaults)
|
||||
command.Command.parse(self, argv)
|
||||
|
||||
# command.Command has a fake debug method, so choose the right one
|
||||
|
||||
def debug(self, format, *args):
|
||||
|
||||
@@ -144,7 +144,7 @@ class _Credit(list):
|
||||
joinString=";")
|
||||
|
||||
|
||||
def _getMetadata(releaseShort, release, discid):
|
||||
def _getMetadata(releaseShort, release, discid, country=None):
|
||||
"""
|
||||
@type release: C{dict}
|
||||
@param release: a release dict as returned in the value for key release
|
||||
@@ -160,6 +160,10 @@ def _getMetadata(releaseShort, release, discid):
|
||||
|
||||
assert release['id'], 'Release does not have an id'
|
||||
|
||||
if 'country' in release and country and release['country'] != country:
|
||||
log.warning('program', '%r was not released in %r', release, country)
|
||||
return None
|
||||
|
||||
discMD = DiscMetadata()
|
||||
|
||||
discMD.releaseType = releaseShort.get('release-group', {}).get('type')
|
||||
@@ -251,7 +255,7 @@ def _getMetadata(releaseShort, release, discid):
|
||||
# ripper.py
|
||||
|
||||
|
||||
def musicbrainz(discid, 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.
|
||||
@@ -305,7 +309,7 @@ def musicbrainz(discid, record=False):
|
||||
formatted = json.dumps(releaseDetail, sort_keys=False, indent=4)
|
||||
log.debug('program', 'release %s' % formatted)
|
||||
|
||||
md = _getMetadata(release, releaseDetail, discid)
|
||||
md = _getMetadata(release, releaseDetail, discid, country)
|
||||
if md:
|
||||
log.debug('program', 'duration %r', md.duration)
|
||||
ret.append(md)
|
||||
|
||||
@@ -151,7 +151,7 @@ class Program(log.Loggable):
|
||||
assert toc.hasTOC()
|
||||
return toc
|
||||
|
||||
def getTable(self, runner, cddbdiscid, mbdiscid, device):
|
||||
def getTable(self, runner, cddbdiscid, mbdiscid, device, offset):
|
||||
"""
|
||||
Retrieve the Table either from the cache or the drive.
|
||||
|
||||
@@ -159,21 +159,31 @@ class Program(log.Loggable):
|
||||
"""
|
||||
tcache = cache.TableCache()
|
||||
ptable = tcache.get(cddbdiscid, mbdiscid)
|
||||
itable = None
|
||||
tdict = {}
|
||||
|
||||
if not ptable.object:
|
||||
self.debug('getTable: cddbdiscid %s, mbdiscid %s not in cache, '
|
||||
# Ingore old cache, since we do not know what offset it used.
|
||||
if type(ptable.object) is dict:
|
||||
tdict = ptable.object
|
||||
|
||||
if offset in tdict:
|
||||
itable = tdict[offset]
|
||||
|
||||
if not itable:
|
||||
self.debug('getTable: cddbdiscid %s, mbdiscid %s not in cache for offset %s, '
|
||||
'reading table' % (
|
||||
cddbdiscid, mbdiscid))
|
||||
cddbdiscid, mbdiscid, offset))
|
||||
t = cdrdao.ReadTableTask(device=device)
|
||||
runner.run(t)
|
||||
ptable.persist(t.table)
|
||||
self.debug('getTable: read table %r' % t.table)
|
||||
itable = t.table
|
||||
tdict[offset] = itable
|
||||
ptable.persist(tdict)
|
||||
self.debug('getTable: read table %r' % itable)
|
||||
else:
|
||||
self.debug('getTable: cddbdiscid %s, mbdiscid %s in cache' % (
|
||||
cddbdiscid, mbdiscid))
|
||||
ptable.object.unpickled()
|
||||
self.debug('getTable: loaded table %r' % ptable.object)
|
||||
itable = ptable.object
|
||||
self.debug('getTable: cddbdiscid %s, mbdiscid %s in cache for offset %s' % (
|
||||
cddbdiscid, mbdiscid, offset))
|
||||
self.debug('getTable: loaded table %r' % itable)
|
||||
|
||||
assert itable.hasTOC()
|
||||
|
||||
self.result.table = itable
|
||||
@@ -182,8 +192,6 @@ class Program(log.Loggable):
|
||||
itable.getMusicBrainzDiscId())
|
||||
return itable
|
||||
|
||||
# FIXME: the cache should be model/offset specific
|
||||
|
||||
def getRipResult(self, cddbdiscid):
|
||||
"""
|
||||
Retrieve the persistable RipResult either from our cache (from a
|
||||
@@ -314,7 +322,7 @@ class Program(log.Loggable):
|
||||
|
||||
return None
|
||||
|
||||
def getMusicBrainz(self, ittoc, mbdiscid, release=None):
|
||||
def getMusicBrainz(self, ittoc, mbdiscid, release=None, country=None, prompt=False):
|
||||
"""
|
||||
@type ittoc: L{morituri.image.table.Table}
|
||||
"""
|
||||
@@ -332,6 +340,7 @@ class Program(log.Loggable):
|
||||
for _ in range(0, 4):
|
||||
try:
|
||||
metadatas = mbngs.musicbrainz(mbdiscid,
|
||||
country=country,
|
||||
record=self._record)
|
||||
except mbngs.NotFoundException, e:
|
||||
break
|
||||
@@ -364,12 +373,29 @@ class Program(log.Loggable):
|
||||
self._stdout.write('URL : %s\n' % metadata.url)
|
||||
self._stdout.write('Release : %s\n' % metadata.mbid)
|
||||
self._stdout.write('Type : %s\n' % metadata.releaseType)
|
||||
if metadata.barcode:
|
||||
self._stdout.write("Barcode : %s\n" % metadata.barcode)
|
||||
if metadata.catalogNumber:
|
||||
self._stdout.write("Cat no : %s\n" % metadata.catalogNumber)
|
||||
|
||||
delta = abs(metadata.duration - ittoc.duration())
|
||||
if not delta in deltas:
|
||||
deltas[delta] = []
|
||||
deltas[delta].append(metadata)
|
||||
|
||||
lowest = None
|
||||
|
||||
if not release and len(metadatas) > 1:
|
||||
# Select the release that most closely matches the duration.
|
||||
lowest = min(deltas.keys())
|
||||
|
||||
if prompt:
|
||||
guess = (deltas[lowest])[0].mbid
|
||||
release = raw_input("\nPlease select a release [%s]: " % guess)
|
||||
|
||||
if not release:
|
||||
release = guess
|
||||
|
||||
if release:
|
||||
metadatas = [m for m in metadatas if m.url.endswith(release)]
|
||||
self.debug('Asked for release %r, only kept %r',
|
||||
@@ -388,12 +414,10 @@ class Program(log.Loggable):
|
||||
"but none of the found releases match\n" % release)
|
||||
return
|
||||
else:
|
||||
# Select the release that most closely matches the duration.
|
||||
lowest = min(deltas.keys())
|
||||
|
||||
# If we have multiple, make sure they match
|
||||
metadatas = deltas[lowest]
|
||||
if lowest:
|
||||
metadatas = deltas[lowest]
|
||||
|
||||
# If we have multiple, make sure they match
|
||||
if len(metadatas) > 1:
|
||||
artist = metadatas[0].artist
|
||||
releaseTitle = metadatas[0].releaseTitle
|
||||
@@ -471,7 +495,7 @@ class Program(log.Loggable):
|
||||
# gst-python 0.10.15.1 does not handle unicode -> utf8 string
|
||||
# conversion
|
||||
# see http://bugzilla.gnome.org/show_bug.cgi?id=584445
|
||||
if self.metadata and self.metadata.various:
|
||||
if self.metadata and not self.metadata.various:
|
||||
ret["album-artist"] = albumArtist.encode('utf-8')
|
||||
ret[gst.TAG_ARTIST] = trackArtist.encode('utf-8')
|
||||
ret[gst.TAG_TITLE] = title.encode('utf-8')
|
||||
@@ -558,7 +582,7 @@ class Program(log.Loggable):
|
||||
return ret
|
||||
|
||||
def ripTrack(self, runner, trackResult, offset, device, profile, taglist,
|
||||
what=None):
|
||||
overread, what=None):
|
||||
"""
|
||||
Ripping the track may change the track's filename as stored in
|
||||
trackResult.
|
||||
@@ -582,7 +606,7 @@ class Program(log.Loggable):
|
||||
what='track %d' % (trackResult.number, )
|
||||
|
||||
t = cdparanoia.ReadVerifyTrackTask(trackResult.filename,
|
||||
self.result.table, start, stop,
|
||||
self.result.table, start, stop, overread,
|
||||
offset=offset,
|
||||
device=device,
|
||||
profile=profile,
|
||||
|
||||
@@ -79,7 +79,10 @@ class Image(object, log.Loggable):
|
||||
|
||||
# CD's have a standard lead-in time of 2 seconds;
|
||||
# checksums that use it should add it there
|
||||
offset = self.cue.table.tracks[0].getIndex(1).relative
|
||||
if verify.lengths.has_key(0):
|
||||
offset = verify.lengths[0]
|
||||
else:
|
||||
offset = self.cue.table.tracks[0].getIndex(1).relative
|
||||
|
||||
tracks = []
|
||||
|
||||
@@ -211,6 +214,18 @@ class ImageVerifyTask(log.Loggable, task.MultiSeparateTask):
|
||||
self._tasks = []
|
||||
self.lengths = {}
|
||||
|
||||
try:
|
||||
htoa = cue.table.tracks[0].indexes[0]
|
||||
track = cue.table.tracks[0]
|
||||
path = image.getRealPath(htoa.path)
|
||||
assert type(path) is unicode, "%r is not unicode" % path
|
||||
self.debug('schedule scan of audio length of %r', path)
|
||||
taskk = AudioLengthTask(path)
|
||||
self.addTask(taskk)
|
||||
self._tasks.append((0, track, taskk))
|
||||
except (KeyError, IndexError):
|
||||
self.debug('no htoa track')
|
||||
|
||||
for trackIndex, track in enumerate(cue.table.tracks):
|
||||
self.debug('verifying track %d', trackIndex + 1)
|
||||
index = track.indexes[1]
|
||||
|
||||
@@ -514,7 +514,7 @@ class Table(object, log.Loggable):
|
||||
discId1[-1], discId1[-2], discId1[-3],
|
||||
self.getAudioTracks(), discId1, discId2, self.getCDDBDiscId())
|
||||
|
||||
def cue(self, cuePath='', program='Morituri'):
|
||||
def cue(self, cuePath='', program='morituri'):
|
||||
"""
|
||||
@param cuePath: path to the cue file to be written. If empty,
|
||||
will treat paths as if in current directory.
|
||||
|
||||
@@ -216,8 +216,8 @@ class ReadTrackTask(log.Loggable, task.Task):
|
||||
|
||||
_MAXERROR = 100 # number of errors detected by parser
|
||||
|
||||
def __init__(self, path, table, start, stop, offset=0, device=None,
|
||||
action="Reading", what="track"):
|
||||
def __init__(self, path, table, start, stop, overread, offset=0,
|
||||
device=None, action="Reading", what="track"):
|
||||
"""
|
||||
Read the given track.
|
||||
|
||||
@@ -248,6 +248,7 @@ class ReadTrackTask(log.Loggable, task.Task):
|
||||
self._parser = ProgressParser(start, stop)
|
||||
self._device = device
|
||||
self._start_time = None
|
||||
self._overread = overread
|
||||
|
||||
self._buffer = "" # accumulate characters
|
||||
self._errors = []
|
||||
@@ -278,8 +279,12 @@ class ReadTrackTask(log.Loggable, task.Task):
|
||||
stopTrack, stopOffset)
|
||||
|
||||
bufsize = 1024
|
||||
argv = ["cdparanoia", "--stderr-progress",
|
||||
"--sample-offset=%d" % self._offset, ]
|
||||
if self._overread:
|
||||
argv = ["cdparanoia", "--stderr-progress",
|
||||
"--sample-offset=%d" % self._offset, "--force-overread", ]
|
||||
else:
|
||||
argv = ["cdparanoia", "--stderr-progress",
|
||||
"--sample-offset=%d" % self._offset, ]
|
||||
if self._device:
|
||||
argv.extend(["--force-cdrom-device", self._device, ])
|
||||
argv.extend(["%d[%s]-%d[%s]" % (
|
||||
@@ -422,8 +427,8 @@ class ReadVerifyTrackTask(log.Loggable, task.MultiSeparateTask):
|
||||
_tmpwavpath = None
|
||||
_tmppath = None
|
||||
|
||||
def __init__(self, path, table, start, stop, offset=0, device=None,
|
||||
profile=None, taglist=None, what="track"):
|
||||
def __init__(self, path, table, start, stop, overread, offset=0,
|
||||
device=None, profile=None, taglist=None, what="track"):
|
||||
"""
|
||||
@param path: where to store the ripped track
|
||||
@type path: str
|
||||
@@ -445,7 +450,6 @@ class ReadVerifyTrackTask(log.Loggable, task.MultiSeparateTask):
|
||||
task.MultiSeparateTask.__init__(self)
|
||||
|
||||
self.debug('Creating read and verify task on %r', path)
|
||||
self.path = path
|
||||
|
||||
if taglist:
|
||||
self.debug('read and verify with taglist %r', taglist)
|
||||
@@ -460,19 +464,26 @@ class ReadVerifyTrackTask(log.Loggable, task.MultiSeparateTask):
|
||||
|
||||
self.tasks = []
|
||||
self.tasks.append(
|
||||
ReadTrackTask(tmppath, table, start, stop,
|
||||
ReadTrackTask(tmppath, table, start, stop, overread,
|
||||
offset=offset, device=device, what=what))
|
||||
self.tasks.append(checksum.CRC32Task(tmppath))
|
||||
t = ReadTrackTask(tmppath, table, start, stop,
|
||||
t = ReadTrackTask(tmppath, table, start, stop, overread,
|
||||
offset=offset, device=device, action="Verifying", what=what)
|
||||
self.tasks.append(t)
|
||||
self.tasks.append(checksum.CRC32Task(tmppath))
|
||||
|
||||
fd, tmpoutpath = tempfile.mkstemp(suffix='.morituri.%s' %
|
||||
profile.extension)
|
||||
tmpoutpath = unicode(tmpoutpath)
|
||||
os.close(fd)
|
||||
# encode to the final path + '.part'
|
||||
try:
|
||||
tmpoutpath = path + u'.part'
|
||||
open(tmpoutpath, 'wb').close()
|
||||
except IOError, e:
|
||||
if errno.ENAMETOOLONG != e.errno:
|
||||
raise
|
||||
path = common.shrinkPath(path)
|
||||
tmpoutpath = path + u'.part'
|
||||
open(tmpoutpath, 'wb').close()
|
||||
self._tmppath = tmpoutpath
|
||||
self.path = path
|
||||
|
||||
# here to avoid import gst eating our options
|
||||
from morituri.common import encode
|
||||
@@ -484,10 +495,6 @@ class ReadVerifyTrackTask(log.Loggable, task.MultiSeparateTask):
|
||||
|
||||
self.checksum = None
|
||||
|
||||
umask = os.umask(0)
|
||||
os.umask(umask)
|
||||
self.file_mode = 0666 - umask
|
||||
|
||||
def stop(self):
|
||||
# FIXME: maybe this kind of try-wrapping to make sure
|
||||
# we chain up should be handled by a parent class function ?
|
||||
@@ -521,16 +528,10 @@ class ReadVerifyTrackTask(log.Loggable, task.MultiSeparateTask):
|
||||
# delete the unencoded file
|
||||
os.unlink(self._tmpwavpath)
|
||||
|
||||
os.chmod(self._tmppath, self.file_mode)
|
||||
|
||||
if not self.exception:
|
||||
try:
|
||||
self.debug('Moving to final path %r', self.path)
|
||||
shutil.move(self._tmppath, self.path)
|
||||
except IOError, e:
|
||||
if e.errno == errno.ENAMETOOLONG:
|
||||
self.path = common.shrinkPath(self.path)
|
||||
shutil.move(self._tmppath, self.path)
|
||||
os.rename(self._tmppath, self.path)
|
||||
except Exception, e:
|
||||
self.debug('Exception while moving to final path %r: '
|
||||
'%r',
|
||||
|
||||
@@ -1,26 +1,5 @@
|
||||
# -*- Mode: Python; test-case-name: morituri.test.test_result_logger -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
# Morituri - for those about to RIP
|
||||
|
||||
# Copyright (C) 2009 Thomas Vander Stichele
|
||||
|
||||
# This file is part of morituri.
|
||||
#
|
||||
# morituri is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# morituri is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with morituri. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import time
|
||||
import hashlib
|
||||
|
||||
from morituri.common import common
|
||||
from morituri.configure import configure
|
||||
@@ -29,119 +8,191 @@ from morituri.result import result
|
||||
|
||||
class MorituriLogger(result.Logger):
|
||||
|
||||
_accuratelyRipped = 0
|
||||
_inARDatabase = 0
|
||||
_errors = False
|
||||
|
||||
def _framesToMSF(self, frames):
|
||||
# format specifically for EAC log; examples (5:39.57)
|
||||
f = frames % common.FRAMES_PER_SECOND
|
||||
frames -= f
|
||||
s = (frames / common.FRAMES_PER_SECOND) % 60
|
||||
frames -= s * 60
|
||||
m = frames / common.FRAMES_PER_SECOND / 60
|
||||
return "%2d:%02d.%02d" % (m, s, f)
|
||||
|
||||
def _framesToHMSH(self, frames):
|
||||
# format specifically for EAC log; examples (0:00.00.70)
|
||||
f = frames % common.FRAMES_PER_SECOND
|
||||
frames -= f
|
||||
s = (frames / common.FRAMES_PER_SECOND) % 60
|
||||
frames -= s * 60
|
||||
m = frames / common.FRAMES_PER_SECOND / 60
|
||||
frames -= m * 60
|
||||
h = frames / common.FRAMES_PER_SECOND / 60 / 60
|
||||
return "%2d:%02d:%02d.%02d" % (h, m, s, f)
|
||||
|
||||
def log(self, ripResult, epoch=time.time()):
|
||||
"""
|
||||
@type ripResult: L{morituri.result.result.RipResult}
|
||||
"""
|
||||
lines = self.logRip(ripResult, epoch=epoch)
|
||||
return '\n'.join(lines)
|
||||
return "\n".join(lines)
|
||||
|
||||
def logRip(self, ripResult, epoch):
|
||||
|
||||
lines = []
|
||||
|
||||
### global
|
||||
|
||||
lines.append("Logfile created by: morituri %s" % configure.version)
|
||||
# FIXME: when we localize this, see #49 to handle unicode properly.
|
||||
import locale
|
||||
old = locale.getlocale(locale.LC_TIME)
|
||||
locale.setlocale(locale.LC_TIME, 'C')
|
||||
date = time.strftime("%b %d %H:%M:%S", time.localtime(epoch))
|
||||
locale.setlocale(locale.LC_TIME, old)
|
||||
lines.append("Logfile created on: %s" % date)
|
||||
lines.append("Ripper: morituri %s" % configure.version)
|
||||
date = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(epoch)).strip()
|
||||
lines.append("Ripped at: %s" % date)
|
||||
lines.append("Drive: %s%s (revision %s)" %
|
||||
(ripResult.vendor, ripResult.model, ripResult.release))
|
||||
defeat = "Unknown"
|
||||
if ripResult.cdparanoiaDefeatsCache is True:
|
||||
defeat = "Yes"
|
||||
if ripResult.cdparanoiaDefeatsCache is False:
|
||||
defeat = "No"
|
||||
lines.append("Defeat audio cache: %s" % defeat)
|
||||
lines.append("")
|
||||
|
||||
# album
|
||||
lines.append("Album: %s - %s" % (ripResult.artist, ripResult.title))
|
||||
lines.append("Read offset correction: %d" % ripResult.offset)
|
||||
# Currently unsupported by the official cdparanoia package
|
||||
lines.append("Overread: No")
|
||||
# Fully working only using the patched cdparanoia package
|
||||
# lines.append("Fill up missing offset samples with silence: Yes")
|
||||
lines.append("Gap detection: cdrdao %s" % ripResult.cdrdaoVersion)
|
||||
lines.append("")
|
||||
|
||||
lines.append("CDDB disc id: %s" % ripResult. table.getCDDBDiscId())
|
||||
lines.append("MusicBrainz disc id: %s" % ripResult. table.getMusicBrainzDiscId())
|
||||
lines.append("MusicBrainz lookup URL: %s" % ripResult. table.getMusicBrainzSubmitURL())
|
||||
lines.append("Used output format: %s" % ripResult.profileName)
|
||||
lines.append("GStreamer:")
|
||||
lines.append(" Pipeline: %s" % ripResult.profilePipeline)
|
||||
lines.append(" Version: %s" % ripResult.gstreamerVersion)
|
||||
lines.append(" Python version: %s" % ripResult.gstPythonVersion)
|
||||
lines.append(" Encoder plugin version: %s" % ripResult.encoderVersion)
|
||||
lines.append("")
|
||||
|
||||
# drive
|
||||
lines.append(
|
||||
"Drive: vendor %s, model %s" % (
|
||||
ripResult.vendor, ripResult.model))
|
||||
lines.append("")
|
||||
|
||||
lines.append("Read offset correction: %d" %
|
||||
ripResult.offset)
|
||||
lines.append("")
|
||||
|
||||
# toc
|
||||
lines.append("Table of Contents:")
|
||||
lines.append("")
|
||||
lines.append(
|
||||
" Track | Start | Length")
|
||||
lines.append(
|
||||
" ------------------------------------------------")
|
||||
lines.append("TOC:")
|
||||
table = ripResult.table
|
||||
|
||||
|
||||
htoa = None
|
||||
try:
|
||||
htoa = table.tracks[0].getIndex(0)
|
||||
except KeyError:
|
||||
pass
|
||||
if htoa and htoa.path:
|
||||
htoastart = htoa.absolute
|
||||
htoaend = table.getTrackEnd(0)
|
||||
htoalength = table.tracks[0].getIndex(1).absolute - htoastart + 1
|
||||
lines.append(" 0:")
|
||||
lines.append(" Start: %s" % self._framesToMSF(htoastart))
|
||||
lines.append(" Length: %s" % self._framesToMSF(htoalength))
|
||||
lines.append(" Start sector: %d" % htoastart)
|
||||
lines.append(" End sector: %d" % htoaend)
|
||||
for t in table.tracks:
|
||||
# FIXME: what happens to a track start over 60 minutes ?
|
||||
start = t.getIndex(1).absolute
|
||||
length = table.getTrackLength(t.number)
|
||||
lines.append(
|
||||
" %2d | %6d - %s | %6d - %s" % (
|
||||
t.number,
|
||||
start, common.framesToMSF(start),
|
||||
length, common.framesToMSF(length)))
|
||||
end = table.getTrackEnd(t.number)
|
||||
lines.append(" %d:" % t.number)
|
||||
lines.append(" Start: %s" % self._framesToMSF(start))
|
||||
lines.append(" Length: %s" % self._framesToMSF(length))
|
||||
lines.append(" Start sector: %d" % start)
|
||||
lines.append(" End sector: %d" % end)
|
||||
lines.append("")
|
||||
|
||||
lines.append("")
|
||||
lines.append("")
|
||||
|
||||
### per-track
|
||||
lines.append("Tracks:")
|
||||
duration = 0.0
|
||||
for t in ripResult.tracks:
|
||||
if not t.filename:
|
||||
continue
|
||||
lines.extend(self.trackLog(t))
|
||||
lines.append('')
|
||||
lines.append("")
|
||||
duration += t.testduration + t.copyduration
|
||||
|
||||
lines.append("AccurateRip Summary:")
|
||||
if self._inARDatabase == 0:
|
||||
lines.append(" None of the tracks are present in "
|
||||
"the AccurateRip database")
|
||||
else:
|
||||
nonHTOA = len(ripResult.tracks)
|
||||
if ripResult.tracks[0].number == 0:
|
||||
nonHTOA -= 1
|
||||
if self._accuratelyRipped == 0:
|
||||
lines.append(" No tracks could be verified as accurate")
|
||||
lines.append(" You may have a different pressing "
|
||||
"from the one(s) in the database")
|
||||
elif self._accuratelyRipped < nonHTOA:
|
||||
lines.append(" %d track(s) accurately ripped" %
|
||||
self._accuratelyRipped)
|
||||
lines.append(" %d track(s) could not be verified as"
|
||||
"accurate" % (nonHTOA - self._accuratelyRipped))
|
||||
lines.append("")
|
||||
lines.append(" Some tracks could not be verified as accurate")
|
||||
else:
|
||||
lines.append(" All tracks accurately ripped")
|
||||
lines.append("")
|
||||
|
||||
lines.append("Errors:")
|
||||
if self._errors:
|
||||
lines.append(" There were errors")
|
||||
else:
|
||||
lines.append(" No errors occurred")
|
||||
lines.append("")
|
||||
lines.append("End of status report")
|
||||
lines.append("")
|
||||
|
||||
hasher = hashlib.sha256()
|
||||
hasher.update("\n".join(lines).encode("utf-8"))
|
||||
lines.append("Log checksum: %s" % hasher.hexdigest())
|
||||
lines.append("")
|
||||
return lines
|
||||
|
||||
def trackLog(self, trackResult):
|
||||
|
||||
lines = []
|
||||
|
||||
lines.append('Track %2d' % trackResult.number)
|
||||
lines.append('')
|
||||
lines.append(' Filename %s' % trackResult.filename)
|
||||
lines.append('')
|
||||
if trackResult.pregap:
|
||||
lines.append(' Pre-gap: %s' % common.framesToMSF(
|
||||
trackResult.pregap))
|
||||
lines.append('')
|
||||
|
||||
lines.append(' Peak level %.1f %%' % (trackResult.peak * 100.0))
|
||||
lines.append(" %d:" % trackResult.number)
|
||||
lines.append(" Filename: %s" % trackResult.filename)
|
||||
# EAC adds the 2 seconds to the first track pregap
|
||||
pregap = trackResult.pregap
|
||||
# if trackResult.number == 1:
|
||||
# pregap += 2 * common.FRAMES_PER_SECOND
|
||||
if pregap:
|
||||
lines.append(" Pre-gap length: %s" % self._framesToHMSH(pregap))
|
||||
# EAC seems to format peak differently, truncating to the 3rd digit,
|
||||
# and also calculating it against a max of 32767
|
||||
# MBV - Feed me with your kiss: replaygain 0.809875,
|
||||
# EAC's peak level 80.9 % instead of 90.0 %
|
||||
peak = trackResult.peak
|
||||
# lines.append(' Peak level %r' % peak)
|
||||
lines.append(" Peak level: %.6f %%" % peak)
|
||||
# level = "%.2f" % (trackResult.peak * 100.0)
|
||||
# level = level[:-1]
|
||||
# lines.append(' Peak level %s %%' % level)
|
||||
if trackResult.copyspeed:
|
||||
lines.append(' Extraction Speed (Copy) %.4f X' % (
|
||||
lines.append(" Extraction speed: %.1f X" % (
|
||||
trackResult.copyspeed))
|
||||
if trackResult.testspeed:
|
||||
lines.append(' Extraction Speed (Test) %.4f X' % (
|
||||
trackResult.testspeed))
|
||||
|
||||
if trackResult.copycrc is not None:
|
||||
lines.append(' Copy CRC %08X' % trackResult.copycrc)
|
||||
# Track quality is shown in secure mode
|
||||
if trackResult.quality and trackResult.quality > 0.001:
|
||||
lines.append(" Track quality: %.1f %%" %
|
||||
(trackResult.quality * 100.0, ))
|
||||
if trackResult.testcrc is not None:
|
||||
lines.append(' Test CRC %08X' % trackResult.testcrc)
|
||||
if trackResult.testcrc == trackResult.copycrc:
|
||||
lines.append(' Copy OK')
|
||||
else:
|
||||
lines.append(" WARNING: CRCs don't match!")
|
||||
else:
|
||||
lines.append(" WARNING: no CRC check done")
|
||||
|
||||
|
||||
lines.append(" Test CRC: %08X" % trackResult.testcrc)
|
||||
if trackResult.copycrc is not None:
|
||||
lines.append(" Copy CRC: %08X" % trackResult.copycrc)
|
||||
lines.append(" AccurateRip v1:")
|
||||
if trackResult.accurip:
|
||||
lines.append(' Accurately ripped (confidence %d) [%08X]' % (
|
||||
trackResult.ARDBConfidence, trackResult.ARCRC))
|
||||
else:
|
||||
if trackResult.ARDBCRC:
|
||||
lines.append(' Cannot be verified as accurate '
|
||||
'[%08X], AccurateRip returned [%08X]' % (
|
||||
trackResult.ARCRC, trackResult.ARDBCRC))
|
||||
self._inARDatabase += 1
|
||||
if trackResult.ARCRC == trackResult.ARDBCRC:
|
||||
lines.append(" Confidence: %d" %
|
||||
trackResult.ARDBConfidence)
|
||||
lines.append(" Checksum: %08X" % trackResult.ARCRC)
|
||||
self._accuratelyRipped += 1
|
||||
else:
|
||||
lines.append(' Track not present in AccurateRip database')
|
||||
lines.append(" Cannot be verified as accurate "
|
||||
"(confidence %d), [%08X], "
|
||||
"AccurateRip returned [%08x]" % (
|
||||
trackResult.ARDBConfidence,
|
||||
trackResult.ARCRC, trackResult.ARDBCRC))
|
||||
else:
|
||||
lines.append(" Track not present in AccurateRip database")
|
||||
|
||||
if trackResult.testcrc == trackResult.copycrc:
|
||||
lines.append(" Copy OK")
|
||||
else:
|
||||
self._errors = True
|
||||
lines.append(" Error: CRC mismatch!")
|
||||
return lines
|
||||
|
||||
@@ -93,6 +93,7 @@ class RipResult:
|
||||
"""
|
||||
|
||||
offset = 0
|
||||
overread = None
|
||||
table = None
|
||||
artist = None
|
||||
title = None
|
||||
|
||||
@@ -38,6 +38,7 @@ from morituri.rip import common as rcommon
|
||||
from morituri.extern.command import command
|
||||
|
||||
|
||||
SILENT = 1e-10
|
||||
MAX_TRIES = 5
|
||||
|
||||
|
||||
@@ -58,6 +59,12 @@ class _CD(logcommand.LogCommand):
|
||||
self.parser.add_option('-R', '--release-id',
|
||||
action="store", dest="release_id",
|
||||
help="MusicBrainz release id to match to (if there are multiple)")
|
||||
self.parser.add_option('-p', '--prompt',
|
||||
action="store_true", dest="prompt",
|
||||
help="Prompt if there are multiple matching releases")
|
||||
self.parser.add_option('-c', '--country',
|
||||
action="store", dest="country",
|
||||
help="Filter releases by country")
|
||||
|
||||
|
||||
def do(self, args):
|
||||
@@ -89,7 +96,9 @@ class _CD(logcommand.LogCommand):
|
||||
|
||||
self.program.metadata = self.program.getMusicBrainz(self.ittoc,
|
||||
self.mbdiscid,
|
||||
release=self.options.release_id)
|
||||
release=self.options.release_id,
|
||||
country=self.options.country,
|
||||
prompt=self.options.prompt)
|
||||
|
||||
if not self.program.metadata:
|
||||
# fall back to FreeDB for lookup
|
||||
@@ -108,7 +117,7 @@ class _CD(logcommand.LogCommand):
|
||||
|
||||
self.itable = self.program.getTable(self.runner,
|
||||
self.ittoc.getCDDBDiscId(),
|
||||
self.ittoc.getMusicBrainzDiscId(), self.device)
|
||||
self.ittoc.getMusicBrainzDiscId(), self.device, self.options.offset)
|
||||
|
||||
assert self.itable.getCDDBDiscId() == self.ittoc.getCDDBDiscId(), \
|
||||
"full table's id %s differs from toc id %s" % (
|
||||
@@ -203,16 +212,25 @@ Log files will log the path to tracks relative to this directory.
|
||||
self.parser.add_option('-o', '--offset',
|
||||
action="store", dest="offset",
|
||||
help="sample read offset (defaults to configured value, or 0)")
|
||||
self.parser.add_option('-x', '--force-overread',
|
||||
action="store_true", dest="overread",
|
||||
help="Force overreading into the lead-out portion of the disc. "
|
||||
"Works only if the patched cdparanoia package is installed "
|
||||
"and the drive supports this feature. "
|
||||
"The default value is: %default",
|
||||
default=False)
|
||||
self.parser.add_option('-O', '--output-directory',
|
||||
action="store", dest="output_directory",
|
||||
help="output directory; will be included in file paths in result "
|
||||
"files "
|
||||
"(defaults to absolute path to current directory; set to "
|
||||
"empty if you want paths to be relative instead) ")
|
||||
"empty if you want paths to be relative instead; "
|
||||
"configured value: %default) ")
|
||||
self.parser.add_option('-W', '--working-directory',
|
||||
action="store", dest="working_directory",
|
||||
help="working directory; morituri will change to this directory "
|
||||
"and files will be created relative to it when not absolute ")
|
||||
"and files will be created relative to it when not absolute "
|
||||
"(configured value: %default) ")
|
||||
|
||||
rcommon.addTemplate(self)
|
||||
|
||||
@@ -223,8 +241,8 @@ Log files will log the path to tracks relative to this directory.
|
||||
|
||||
self.parser.add_option('', '--profile',
|
||||
action="store", dest="profile",
|
||||
help="profile for encoding (default '%s', choices '%s')" % (
|
||||
default, "', '".join(encode.PROFILES.keys())),
|
||||
help="profile for encoding (default '%%default', choices '%s')" % (
|
||||
"', '".join(encode.PROFILES.keys())),
|
||||
default=default)
|
||||
self.parser.add_option('-U', '--unknown',
|
||||
action="store_true", dest="unknown",
|
||||
@@ -254,6 +272,11 @@ Install pycdio and run 'rip offset find' to detect your drive's offset.
|
||||
options.offset)
|
||||
if self.options.output_directory is None:
|
||||
self.options.output_directory = os.getcwd()
|
||||
else:
|
||||
self.options.output_directory = os.path.expanduser(self.options.output_directory)
|
||||
|
||||
if self.options.working_directory is not None:
|
||||
self.options.working_directory = os.path.expanduser(self.options.working_directory)
|
||||
|
||||
if self.options.logger:
|
||||
try:
|
||||
@@ -281,6 +304,7 @@ Install pycdio and run 'rip offset find' to detect your drive's offset.
|
||||
self.program.setWorkingDirectory(self.options.working_directory)
|
||||
self.program.outdir = self.options.output_directory.decode('utf-8')
|
||||
self.program.result.offset = int(self.options.offset)
|
||||
self.program.result.overread = self.options.overread
|
||||
|
||||
### write disc files
|
||||
disambiguate = False
|
||||
@@ -377,6 +401,7 @@ Install pycdio and run 'rip offset find' to detect your drive's offset.
|
||||
device=self.parentCommand.options.device,
|
||||
profile=profile,
|
||||
taglist=self.program.getTagList(number),
|
||||
overread=self.options.overread,
|
||||
what='track %d of %d%s' % (
|
||||
number, len(self.itable.tracks), extra))
|
||||
break
|
||||
@@ -405,8 +430,18 @@ Install pycdio and run 'rip offset find' to detect your drive's offset.
|
||||
# overlay this rip onto the Table
|
||||
if number == 0:
|
||||
# HTOA goes on index 0 of track 1
|
||||
self.itable.setFile(1, 0, trackResult.filename,
|
||||
self.ittoc.getTrackStart(1), number)
|
||||
# ignore silence in PREGAP
|
||||
if trackResult.peak <= SILENT:
|
||||
self.debug('HTOA peak %r is below SILENT threshold, disregarding', trackResult.peak)
|
||||
self.itable.setFile(1, 0, None,
|
||||
self.ittoc.getTrackStart(1), number)
|
||||
self.debug('Unlinking %r', trackResult.filename)
|
||||
os.unlink(trackResult.filename)
|
||||
trackResult.filename = None
|
||||
self.stdout.write('HTOA discarded, contains digital silence\n')
|
||||
else:
|
||||
self.itable.setFile(1, 0, trackResult.filename,
|
||||
self.ittoc.getTrackStart(1), number)
|
||||
else:
|
||||
self.itable.setFile(number, 1, trackResult.filename,
|
||||
self.ittoc.getTrackLength(number), number)
|
||||
|
||||
@@ -107,6 +107,12 @@ class Retag(logcommand.LogCommand):
|
||||
self.parser.add_option('-R', '--release-id',
|
||||
action="store", dest="release_id",
|
||||
help="MusicBrainz release id to match to (if there are multiple)")
|
||||
self.parser.add_option('-p', '--prompt',
|
||||
action="store_true", dest="prompt",
|
||||
help="Prompt if there are multiple matching releases")
|
||||
self.parser.add_option('-c', '--country',
|
||||
action="store", dest="country",
|
||||
help="Filter releases by country")
|
||||
|
||||
|
||||
def do(self, args):
|
||||
@@ -128,12 +134,16 @@ class Retag(logcommand.LogCommand):
|
||||
self.stdout.write("MusicBrainz lookup URL %s\n" %
|
||||
cueImage.table.getMusicBrainzSubmitURL())
|
||||
prog.metadata = prog.getMusicBrainz(cueImage.table, mbdiscid,
|
||||
release=self.options.release_id)
|
||||
release=self.options.release_id,
|
||||
country=self.options.country,
|
||||
prompt=self.options.prompt)
|
||||
|
||||
if not prog.metadata:
|
||||
print 'Not in MusicBrainz database, skipping'
|
||||
continue
|
||||
|
||||
prog.metadata.discid = mbdiscid
|
||||
|
||||
# FIXME: this feels like we're poking at internals.
|
||||
prog.cuePath = arg
|
||||
prog.result = result.RipResult()
|
||||
|
||||
@@ -218,7 +218,7 @@ CD in the AccurateRip database."""
|
||||
|
||||
t = cdparanoia.ReadTrackTask(path, table,
|
||||
table.getTrackStart(track), table.getTrackEnd(track),
|
||||
offset=offset, device=self.options.device)
|
||||
overread=False, offset=offset, device=self.options.device)
|
||||
t.description = 'Ripping track %d with read offset %d' % (
|
||||
track, offset)
|
||||
runner.run(t)
|
||||
|
||||
Reference in New Issue
Block a user