Merge pull request #6 from JoeLametta/fork

Fork
This commit is contained in:
JoeLametta
2015-12-12 00:39:41 +01:00
14 changed files with 444 additions and 206 deletions

35
.travis.yml Normal file
View 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
View 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 [![Build Status](https://travis-ci.org/JoeLametta/whipper.svg?branch=fork)](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.

View File

@@ -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

View File

@@ -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):

View File

@@ -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)

View File

@@ -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,

View File

@@ -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]

View File

@@ -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.

View File

@@ -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',

View File

@@ -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

View File

@@ -93,6 +93,7 @@ class RipResult:
"""
offset = 0
overread = None
table = None
artist = None
title = None

View File

@@ -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)

View File

@@ -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()

View File

@@ -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)