Merge pull request #1 from thomasvs/master

Catch JDLH fork up with thomasvs:master
This commit is contained in:
Jim DeLaHunt
2014-01-25 23:34:22 -08:00
66 changed files with 2737 additions and 682 deletions

1
.gitignore vendored
View File

@@ -14,3 +14,4 @@ install-sh
missing
morituri.spec
py-compile
REVISION

View File

@@ -41,3 +41,8 @@ CDROMS
PLEXTOR CD-R PX-W8432T Read offset of device is: 355.
Test discs
----------
The Strokes - Someday (promo): has 1 frame silence marked as SILENCE
The Pixies - Surfer Rosa/Come on Pilgrim: has pre-gap, and INDEX 02 on TRACK 11
Florence & The Machine - Lungs: data track

View File

@@ -5,7 +5,7 @@ ACLOCAL_AMFLAGS = -I m4
SUBDIRS = morituri bin etc doc m4 misc
EXTRA_DIST = morituri.spec morituri.doap RELEASE README HACKING
EXTRA_DIST = morituri.spec morituri.doap RELEASE README.md HACKING REVISION
SOURCES = $(top_srcdir)/morituri/*.py $(top_srcdir)/morituri/*/*.py
@@ -38,6 +38,9 @@ PYCHECKER_BLACKLIST = \
release: dist
make $(PACKAGE)-$(VERSION).tar.bz2.md5
REVISION: $(top_srcdir)/.git
$(PYTHON) -c 'from morituri.configure import configure; print configure.revision' > REVISION
# generate md5 sum files
%.md5: %
md5sum $< > $@

57
NEWS
View File

@@ -1,6 +1,59 @@
This is morituri 0.2.0, "ears"
This is morituri 0.2.2, "my bad"
Coverage in 0.2.0: 67 % (1890 / 2807), 95 python tests
Coverage in 0.2.2: 67 % (1972 / 2904), 109 python tests
Bugs fixed in 0.2.2:
in github:
- 38: No matching offset found
- 35: 'rip cd info' should not eject the disc
- 34: Use track instead of the release artist MBID for the 'Musicbrainz artist ID'
- 33: "rip offset find" fails to initialise program.Program
- 19: Set album artist tag
Features added in 0.2.1:
- added "%X" template variable for uppercase filename extension
- added rip cd info
- added storing catalog number and barcode
- disambiguate releases with same name but different catno/barcode
- use all but last track to find offset
- add support to filter path names for better file system support
- add config options for path filtering
- fixes for older pyxdg and some versions of pycdio
Bugs fixed in 0.2.1:
in trac:
- 44: Optionally strip special characters from file names
- 121: ImportError: No module named CDDB
- 126: pycdio is no more optional : pkg_resources.DistributionNotFound: pycdio
- 135: rip drive analyze report "Cannot analyze the drive. Is there a CD in it?" when not able to defeat audio cache
- 137: pycdio returns an error when analyzing drive
- 138: Error when trying to rip with pycdio .19
- 124: Checking of runtime dependencies
in github:
- 31: Cryptic error message if xdg module is too old
- 30: AttributeError: Values instance has no attribute 'unknown'
- 26: Convert values returned from pycdio to str (workaround for upstream bug)
- 24: Filenames from musicbrainz may contain invalid characters for windows filesystems
- 23: Convert drive path from unicode to str when calling cdio.Device (pycdio 0.19 / Arch Linux)
- 22: Compare AccurateRip to num tracks -1, as last track not being checked
- 21: break up logger line
- 18: Crash if no path specified for '-O' option
- 17: Use XDG cache directory
- 16: Work with older versions of python-xdg
- 14: Use with statement to open files
- 13: Use os.path.join instead of hardcoded paths.
- 11: Ignore bash-compgen, to clean up git-status.
- 9: Ask which release to use if DiscID returns several matches
- 8: abort if invalid logger specified
- 7: Warn if no offset specified and no stored offset found/pycdio not available
- 6: Add "%X" template variable for uppercase filename extension.
- 3: (Optional) dependency on cddb should be documented
- 2: No module named moap.util -- dependency shoud be documented
- 1: No module named log -- use of submodules should be documented
Features added in 0.2.0:

157
README
View File

@@ -1,157 +0,0 @@
morituri is a CD ripper aiming for accuracy over speed.
Its features are modeled to compare with Exact Audio Copy on Windows.
RATIONALE
---------
For a more detailed rationale, see my wiki page 'The Art of the Rip'
at https://thomas.apestaart.org/thomas/trac/wiki/DAD/Rip
FEATURES
--------
* support for MusicBrainz for metadata lookup
* support for AccurateRip verification
* detects sample read offset and ability to defeat cache of drives
* performs test and copy rip
* detects and rips Hidden Track One Audio
* 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
* retagging of images
* plugins for logging
* for now, only a command line client (rip) is shipped
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
- python musicbrainz2, for metadata lookup
- python-setuptools, for plugin support
- python-cddb, for showing but not using disc info if not in musicbrainz
- pycdio, for drive identification (optional)
GETTING MORITURI
----------------
If you are building from a source tarball or checkout, you can choose to
use morituri installed or uninstalled.
- getting:
- Change to a directory where you want to put the morituri 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
cd morituri
git submodule init
git submodule update
./autogen.sh
- building:
./configure
make
- you can now choose to install it or run it uninstalled.
- installing:
make install
- running uninstalled:
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)
RUNNING MORITURI
----------------
morituri currently only has a command-line interface called 'rip'
rip is self-documenting.
rip -h gives you the basic instructions.
rip implements a tree of commands; for example, the top-level 'changelog'
command has a number of sub-commands.
Positioning of arguments is important;
rip cd -d (device) rip
is correct, while
rip cd rip -d (device)
is not, because the -d argument applies to the rip command.
Check the man page (rip(1)) for more information.
RUNNING UNINSTALLED
-------------------
To make it easier for developers, you can run morituri straight from the
source checkout:
./autogen.sh
make
misc/morituri-uninstalled
GETTING STARTED
---------------
The simplest way to get started making accurate rips is:
- pick a relatively popular CD that has a good change of being in the
AccurateRip database
- find the drive's offset by running
rip offset find
- wait for it to complete; this might take a while
- optionally, confirm this offset with two more discs
- analyze the drive's caching behaviour
rip drive analyze
- rip the disc by running
rip cd rip --offset (the number you got before)
FILING BUGS
-----------
morituri's bug tracker is at https://thomas.apestaart.org/morituri/trac/
When filing bugs, please run the failing command with the environment variable
RIP_DEBUG set; for example:
RIP_DEBUG=5 rip offset find > morituri.log 2>&1
gzip morituri.log
And attach the gzipped log file to your bug report.
KNOWN ISSUES
------------
- no GUI yet
GOALS
-----
- quality over speed
- support one-command automatic ripping
- support offline ripping (doing metadata lookup and log rewriting later)
- separate the info/result about the rip from the metadata/file generation/...
rip command tree
----------------
rip
accurip
show
show accuraterip data
offset
find
find drive's read offset using AccurateRip
verify
verify drive's read offset using AccurateRip
cd
rip
rip the cd
debug
encode
encode a file
htoa
find
rip
rip the htoa if it's there
image
verify
verify the cd image
encode
encode to a different codec
retag
retag the image with current MusicBrainz data

1
README Symbolic link
View File

@@ -0,0 +1 @@
README.md

193
README.md Normal file
View File

@@ -0,0 +1,193 @@
morituri is a CD ripper aiming for accuracy over speed.
Its features are modeled to compare with Exact Audio Copy on Windows.
RATIONALE
---------
For a more detailed rationale, see my wiki page ['The Art of the Rip'](
https://thomas.apestaart.org/thomas/trac/wiki/DAD/Rip).
FEATURES
--------
* support for MusicBrainz for metadata lookup
* support for AccurateRip verification
* detects sample read offset and ability to defeat cache of drives
* performs test and copy rip
* detects and rips Hidden Track One Audio
* 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
* retagging of images
* plugins for logging
* for now, only a command line client (rip) is shipped
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
- python musicbrainz2, for metadata lookup
- python-setuptools, for plugin support
- python-cddb, for showing but not using disc info if not in musicbrainz
- pycdio, for drive identification (optional)
GETTING MORITURI
----------------
If you are building from a source tarball or checkout, you can choose to
use morituri installed or uninstalled.
- getting:
- Change to a directory where you want to put the morituri 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
cd morituri
git submodule init
git submodule update
./autogen.sh
- building:
./configure
make
- you can now choose to install it or run it uninstalled.
- installing:
make install
- running uninstalled:
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
RUNNING MORITURI
----------------
morituri currently only has a command-line interface called 'rip'
rip is self-documenting.
`rip -h` gives you the basic instructions.
rip implements a tree of commands; for example, the top-level 'changelog'
command has a number of sub-commands.
Positioning of arguments is important;
rip cd -d (device) rip
is correct, while
rip cd rip -d (device)
is not, because the `-d` argument applies to the rip command.
Check the man page (rip(1)) for more information.
RUNNING UNINSTALLED
-------------------
To make it easier for developers, you can run morituri straight from the
source checkout:
./autogen.sh
make
misc/morituri-uninstalled
GETTING STARTED
---------------
The simplest way to get started making accurate rips is:
- pick a relatively popular CD that has a good change of being in the
AccurateRip database
- find the drive's offset by running
rip offset find
- wait for it to complete; this might take a while
- optionally, confirm this offset with two more discs
- analyze the drive's caching behaviour
rip drive analyze
- rip the disc by running one of
rip cd rip # uses the offset from configuration file
rip cd rip --offset (the number you got before) # manually specified offset
FILING BUGS
-----------
morituri's bug tracker is at [https://thomas.apestaart.org/morituri/trac/](
https://thomas.apestaart.org/morituri/trac/).
When filing bugs, please run the failing command with the environment variable
`RIP_DEBUG` set; for example:
RIP_DEBUG=5 rip offset find > morituri.log 2>&1
gzip morituri.log
And attach the gzipped log file to your bug report.
KNOWN ISSUES
------------
- no GUI yet
GOALS
-----
- quality over speed
- support one-command automatic ripping
- support offline ripping (doing metadata lookup and log rewriting later)
- separate the info/result about the rip from the metadata/file generation/...
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.
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:
- `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:
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
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)
rip command tree
----------------
rip
* accurip
* show (show accuraterip data)
* offset
* find (find drive's read offset using AccurateRip)
* verify (verify drive's read offset using AccurateRip)
* cd
* rip (rip the cd)
* debug
* encode (encode a file)
* htoa
* find
* rip (rip the htoa if it's there)
* image
* verify (verify the cd image)
* encode (encode to a different codec)
* retag (retag the image with current MusicBrainz data)

48
RELEASE
View File

@@ -1,41 +1,27 @@
morituri is a CD ripper aiming for accuracy over speed.
morituri runs on Linux and possibly other Unix-based systems.
Its features are modeled to compare with Exact Audio Copy on Windows.
For more information, see http://thomas.apestaart.org/morituri/trac/
This is morituri 0.2.0 "ears".
This is morituri 0.2.2, "my bad"
This is intended as a release for daring and curious people who've had enough
of the fact that Windows has a more accurate CD ripper than Linux.
Coverage in 0.2.2: 67 % (1972 / 2904), 109 python tests
Coverage in 0.2.0: 67 % (1890 / 2807), 95 python tests
Bugs fixed in 0.2.2:
Features added in 0.2.0:
in github:
- 38: No matching offset found
- 35: 'rip cd info' should not eject the disc
- 34: Use track instead of the release artist MBID for the 'Musicbrainz artist ID'
- 33: "rip offset find" fails to initialise program.Program
- 19: Set album artist tag
- added plugins system for logger
- added rip cd rip --logger to specify logger
- added reading speed, cdparanoia and cdrdao version to logger
- added rip drive analyze to detect whether we can defeat audio cache behaviour
- store drive offsets and cache defeating in config file
- rip drive list shows configured offset and audio cache defeating
- added rip image retag --release-id to specify the release id to tag with
- added %r/%R for release type to use in track/disc template
- added %x for extension to release template
Bugs fixed in 0.2.0:
morituri 0.2.2 is brought to you by:
- 89: Fails to rip track with \ in its name
- 105: Backslash in track names causes "Cannot find file" during rip
- 108: Unable to find offset / rip
- 109: KeyError when running "rip offset find"
- 111: Python traceback when config has no read offset for CD
- 76: morituri should allow for a configuration file
- 96: rip image retag: allow specification of release ID
- 107: Backslash in track name confuses AR step
- 112: add MusicBrainz lookup URL to generated logfile
morituri 0.2.0 is brought to you by:
Loïc Minier
Ross Burton
Christophe Fergeau
Thomas Vander Stichele
mustbenice
Velo Superman
Nicolas Cornu
dioltas
Frederik "Freso" S. Olesen
Jonas Smedegaard

11
TODO
View File

@@ -1,6 +1,7 @@
TODO:
- add drive analysis mode
- use cdparanoia -A from 10.2 on for caching behaviour
- after fixing relative, pregaps, and index 02, check when htoa is 0,
and add a setSilence to table to set a counter 0 with no path, and test
that the cue file puts a SILENCE/PREGAP
- store drive features in a database
- try http://www.ime.usp.br/~pjssilva/secure-cdparanoia.py and see if it
is better at handling some bad cd's
@@ -16,13 +17,10 @@ TODO:
or not putting the disk in)
- check if it's simple to listen to each track in a multitrack completing
- save trms to a pickle, after finishing each track
- add a way to store configuration data per drive, like offset
- rip the data session
- add AccurateRip validation for ripped images to rip command
- add GUI
- persist RipResult so rips can be aborted and continued too; needs verification
of previously ripped files
- write moovida plugin
- write moovida/xbmc plugin
- cache results of MusicBrainz lookups
- on ana, Goldfrapp tells me I have offset 0!
- don't keep short HTOA's if their peak level is low
@@ -63,4 +61,3 @@ TODO:
right now it is; maybe split in to base and output ?
- rip task should abort on task 4 if checksums don't match
- retry cdrdao a few times when it had to load the tray
- when it detects the target dir is already there, but the files would be different names, complain or customize the name with further info (see GLB - mockingbirds singles)

View File

@@ -30,6 +30,8 @@ and assure it doesn't raise an exception.
sys.exit(1)
# now load the main function
h = None
try:
from morituri.common import deps
from morituri.extern.deps import deps as edeps
@@ -38,6 +40,9 @@ try:
from morituri.rip import main
sys.exit(main.main(sys.argv[1:]))
except ImportError, e:
if not h:
# we couldn't even import deps, so reraise
raise
h.handleImportError(e)
sys.exit(1)
except edeps.DependencyError:

View File

@@ -1,7 +1,7 @@
dnl initialize autoconf
dnl when going to/from release please remove/add the nano (fourth number)
dnl releases only do Wall, trunk and prerelease does Werror too
AC_INIT(morituri, 0.2.0.1,
AC_INIT(morituri, 0.2.2.1,
http://thomas.apestaart.org/morituri/trac/newticket,
morituri)
@@ -38,6 +38,10 @@ AC_SUBST(PYTHONLIBDIR)
AS_AC_EXPAND(PLUGINSDIR, "\${libdir}/morituri/plugins")
AC_MSG_NOTICE(Setting plugins directory to $PLUGINSDIR)
dnl get git revision for installed.py.in
AC_SUBST(REVISION, `$PYTHON -c 'from morituri.configure import configure; print configure.revision'`)
AC_MSG_NOTICE(Setting revision to $REVISION)
dnl check for epydoc
AC_CHECK_PROG(EPYDOC, epydoc, yes, no)
AM_CONDITIONAL(HAVE_EPYDOC, test "x$EPYDOC" = "xyes")

View File

@@ -16,11 +16,13 @@ pre-release checklist
- Verify the program runs:
- normal run
- --unknown run
- rip offset find
- add new milestone to trac and make it the default
- verify with ticket query that all fixed tickets for this milestone are
correct:
https://thomas.apestaart.org/morituri/trac/query?order=priority&col=id&col=summary&col=status&col=type&col=priority&col=milestone&col=component&milestone=0.1.1
- remilestone still open tickets to next release
- FIXME: same on github
release
-------
@@ -37,11 +39,21 @@ release
- Add list of new features to NEWS
- Update bugs fixed in NEWS:
moap doap bug query -f "- %(id)3s: %(summary)s" "milestone=$VERSION"
FIXME: same on github
- Update README and home page with relevant new features, as well as version
- Update RELEASE, copying sections from NEWS, and adding contributors with
moap cl cont
or from git:
git log --format='%aN' | sort -u
- Run moap cl prep and add comment about release
- Update ChangeLog; add === release x.y.z === line
- commit locally
git commit -a -m "Releasing $VERSION"
- tag the release:
git tag -a v$VERSION -m "Releasing $VERSION"
- make sure we build installed.py with the correct tag
autoregen.sh
cat morituri/configure/installed.py
- make distcheck
- make release
- build rpm using rpmbuild and mach
@@ -55,9 +67,8 @@ release
cp morituri-$VERSION.tar* /home/thomas/www/thomas.apestaart.org/data/download/morituri
cp /var/tmp/mach/fedora-*/morituri-$VERSION-*/*.rpm /home/thomas/www/thomas.apestaart.org/data/download/morituri
tao-put
- commit to master
- create release tag:
git tag -a v$VERSION
- push to master
git push
git push origin v$VERSION
- announce to freshmeat:
moap doap -v $VERSION freshmeat

View File

@@ -1 +1,2 @@
rip
bash-compgen

View File

@@ -40,6 +40,44 @@ Morituri is a CD ripper aiming for maximum quality.
</foaf:Person>
</maintainer>
<release>
<Version>
<revision>0.2.2</revision>
<branch>master</branch>
<name>my bad</name>
<created>2013-07-30</created>
<file-release rdf:resource="http://thomas.apestaart.org/download/morituri/morituri-0.2.2.tar.bz2" />
<file-release rdf:resource="http://thomas.apestaart.org/download/morituri/morituri-0.2.2-1.noarch.rpm" />
<dc:description>
- fixed rip offset find
- set album and track artist id's properly
- rip cd info no longer ejects
</dc:description>
</Version>
</release>
<release>
<Version>
<revision>0.2.1</revision>
<branch>master</branch>
<name>married</name>
<created>2013-07-14</created>
<file-release rdf:resource="http://thomas.apestaart.org/download/morituri/morituri-0.2.1.tar.bz2" />
<file-release rdf:resource="http://thomas.apestaart.org/download/morituri/morituri-0.2.1-1.noarch.rpm" />
<dc:description>
- added "%X" template variable for uppercase filename extension
- added rip cd info
- added storing catalog number and barcode
- disambiguate releases with same name but different catno/barcode
- use all but last track to find offset
- add support to filter path names for better file system support
- add config options for path filtering
- fixes for older pyxdg and some versions of pycdio
</dc:description>
</Version>
</release>
<release>
<Version>
<revision>0.2.0</revision>

View File

@@ -49,7 +49,7 @@ rm -rf $RPM_BUILD_ROOT
%files
%defattr(-,root,root)
%doc README morituri.doap NEWS RELEASE ChangeLog
%doc README.md morituri.doap NEWS RELEASE ChangeLog
%{_bindir}/rip
%{_libdir}/morituri/plugins
%{_mandir}/man1/rip.1*

View File

@@ -16,7 +16,8 @@ morituri_PYTHON = \
gstreamer.py \
log.py \
logcommand.py \
musicbrainzngs.py \
mbngs.py \
path.py \
program.py \
renamer.py \
task.py

View File

@@ -115,7 +115,7 @@ class Persister(log.Loggable):
os.unlink(self._path)
class PersistedCache(object):
class PersistedCache(log.Loggable):
"""
I wrap a directory of persisted objects.
"""
@@ -138,11 +138,15 @@ class PersistedCache(object):
Returns the persister for the given key.
"""
persister = Persister(self._getPath(key))
if persister.object:
if hasattr(persister.object, 'instanceVersion'):
o = persister.object
if o.instanceVersion < o.__class__.classVersion:
self.debug(
'key %r persisted object version %d is outdated',
key, o.instanceVersion)
persister.object = None
# FIXME: don't delete old objects atm
# if persister.object:
# if hasattr(persister.object, 'instanceVersion'):
# o = persister.object
# if o.instanceVersion < o.__class__.classVersion:
# persister.delete()
return persister
@@ -230,6 +234,9 @@ class TableCache(log.Loggable):
self.debug('cached table is for different mb id %r' % (
ptable.object.getMusicBrainzDiscId()))
ptable.object = None
else:
self.debug('no valid cached table found for %r' %
cddbdiscid)
if not ptable.object:
# get an empty persistable from the writable location

View File

@@ -101,49 +101,65 @@ class ChecksumTask(log.Loggable, gstreamer.GstPipelineTask):
appsink name=sink sync=False emit-signals=True
''' % gstreamer.quoteParse(self._path).encode('utf-8')
def _getSampleLength(self):
# get length in samples of file
sink = self.pipeline.get_by_name('sink')
self.debug('query duration')
try:
length, qformat = sink.query_duration(gst.FORMAT_DEFAULT)
except gst.QueryError, e:
self.setException(e)
return None
# wavparse 0.10.14 returns in bytes
if qformat == gst.FORMAT_BYTES:
self.debug('query returned in BYTES format')
length /= 4
self.debug('total sample length of file: %r', length)
return length
def paused(self):
sink = self.pipeline.get_by_name('sink')
if self._sampleLength < 0:
self.debug('query duration')
try:
length, qformat = sink.query_duration(gst.FORMAT_DEFAULT)
except gst.QueryError, e:
self.setException(e)
return
length = self._getSampleLength()
if length is None:
return
# wavparse 0.10.14 returns in bytes
if qformat == gst.FORMAT_BYTES:
self.debug('query returned in BYTES format')
length /= 4
self.debug('total sample length of file: %r', length)
if self._sampleLength < 0:
self._sampleLength = length - self._sampleStart
self.debug('sampleLength is queried as %d samples',
self._sampleLength)
else:
self.debug('sampleLength is known, and is %d samples' %
self._sampleLength)
self._sampleEnd = self._sampleStart + self._sampleLength - 1
self.debug('sampleEnd is sample %d' % self._sampleEnd)
self.debug('event')
# the segment end only is respected since -good 0.10.14.1
event = gst.event_new_seek(1.0, gst.FORMAT_DEFAULT,
gst.SEEK_FLAG_FLUSH,
gst.SEEK_TYPE_SET, self._sampleStart,
gst.SEEK_TYPE_SET, self._sampleEnd + 1) # half-inclusive interval
self.debug('CRCing %r from sector %d to sector %d' % (
self._path,
self._sampleStart / common.SAMPLES_PER_FRAME,
(self._sampleEnd + 1) / common.SAMPLES_PER_FRAME))
# FIXME: sending it with sampleEnd set screws up the seek, we don't get
# everything for flac; fixed in recent -good
result = sink.send_event(event)
self.debug('event sent, result %r', result)
if not result:
self.error('Failed to select samples with GStreamer seek event')
if self._sampleStart == 0 and self._sampleEnd + 1 == length:
self.debug('No need to seek, crcing full file')
else:
# the segment end only is respected since -good 0.10.14.1
event = gst.event_new_seek(1.0, gst.FORMAT_DEFAULT,
gst.SEEK_FLAG_FLUSH,
gst.SEEK_TYPE_SET, self._sampleStart,
gst.SEEK_TYPE_SET, self._sampleEnd + 1) # half-inclusive
self.debug('CRCing %r from frame %d to frame %d (excluded)' % (
self._path,
self._sampleStart / common.SAMPLES_PER_FRAME,
(self._sampleEnd + 1) / common.SAMPLES_PER_FRAME))
# FIXME: sending it with sampleEnd set screws up the seek, we
# don't get # everything for flac; fixed in recent -good
result = sink.send_event(event)
self.debug('event sent, result %r', result)
if not result:
self.error('Failed to select samples with GStreamer seek event')
sink.connect('new-buffer', self._new_buffer_cb)
sink.connect('eos', self._eos_cb)
@@ -183,7 +199,7 @@ class ChecksumTask(log.Loggable, gstreamer.GstPipelineTask):
msg = 'did not get all samples, %d of %d missing' % (
self._sampleEnd - last, self._sampleEnd)
self.warning(msg)
self.setException(common.MissingFrames(msg))
self.setExceptionAndTraceback(common.MissingFrames(msg))
return
self.checksum = self._checksum
@@ -193,6 +209,13 @@ class ChecksumTask(log.Loggable, gstreamer.GstPipelineTask):
def do_checksum_buffer(self, buf, checksum):
"""
Subclasses should implement this.
@param buf: a byte buffer containing two 16-bit samples per
channel.
@type buf: C{str}
@param checksum: the checksum so far, as returned by the
previous call.
@type checksum: C{int}
"""
raise NotImplementedError
@@ -224,7 +247,7 @@ class ChecksumTask(log.Loggable, gstreamer.GstPipelineTask):
sample = self._first + self._bytes / 4
samplesDone = sample - self._sampleStart
progress = float(samplesDone) / float((self._sampleLength))
# marshall to the main thread
# marshal to the main thread
self.schedule(0, self.setProgress, progress)
def _eos_cb(self, sink):
@@ -364,3 +387,20 @@ class TRMTask(task.GstPipelineTask):
def stopped(self):
self.trm = self._trm
class MaxSampleTask(ChecksumTask):
"""
I check for the biggest sample value.
"""
description = 'Finding highest sample value'
def do_checksum_buffer(self, buf, checksum):
values = struct.unpack("<%dh" % (len(buf) / 2), buf)
absvalues = [abs(v) for v in values]
m = max(absvalues)
if checksum < m:
checksum = m
return checksum

View File

@@ -23,14 +23,16 @@
import os
import os.path
import commands
import math
import subprocess
from morituri.extern import asyncsub
from morituri.extern.log import log
FRAMES_PER_SECOND = 75
SAMPLES_PER_FRAME = 588
SAMPLES_PER_FRAME = 588 # a sample is 2 16-bit values, left and right channel
WORDS_PER_FRAME = SAMPLES_PER_FRAME * 2
BYTES_PER_FRAME = SAMPLES_PER_FRAME * 4
@@ -288,3 +290,78 @@ def getRelativePath(targetPath, collectionPath):
'getRelativePath: target and collection in different dir, %r' %
rel)
return os.path.join(rel, os.path.basename(targetPath))
class VersionGetter(object):
"""
I get the version of a program by looking for it in command output
according to a regexp.
"""
def __init__(self, dependency, args, regexp, expander):
"""
@param dependency: name of the dependency providing the program
@param args: the arguments to invoke to show the version
@type args: list of str
@param regexp: the regular expression to get the version
@param expander: the expansion string for the version using the
regexp group dict
"""
self._dep = dependency
self._args = args
self._regexp = regexp
self._expander = expander
def get(self):
version = "(Unknown)"
try:
p = asyncsub.Popen(self._args,
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, close_fds=True)
p.wait()
output = asyncsub.recv_some(p, e=0, stderr=1)
vre = self._regexp.search(output)
if vre:
version = self._expander % vre.groupdict()
except OSError, e:
import errno
if e.errno == errno.ENOENT:
raise MissingDependencyException(self._dep)
raise
return version
def getRevision():
"""
Get a revision tag for the current git source tree.
Appends -modified in case there are local modifications.
If this is not a git tree, return the top-level REVISION contents instead.
Finally, return unknown.
"""
topsrcdir = os.path.join(os.path.dirname(__file__), '..', '..')
# only use git if our src directory looks like a git checkout
# if you run git regardless, it recurses up until it finds a .git,
# which may be higher than your current source tree
if os.path.exists(os.path.join(topsrcdir, '.git')):
status, describe = commands.getstatusoutput('git describe')
if status == 0:
if commands.getoutput('git diff-index --name-only HEAD --'):
describe += '-modified'
return describe
# check for a top-level REIVISION file
path = os.path.join(topsrcdir, 'REVISION')
if os.path.exists(path):
revision = open(path).read().strip()
return revision
return '(unknown)'

View File

@@ -54,6 +54,32 @@ class Config(log.Loggable):
self.info('Loaded %d sections from config file' %
len(self._parser.sections()))
def write(self):
fd, path = tempfile.mkstemp(suffix=u'.moriturirc')
handle = os.fdopen(fd, 'w')
self._parser.write(handle)
handle.close()
shutil.move(path, self._path)
### any section
def _getter(self, suffix, section, option):
methodName = 'get' + suffix
method = getattr(self._parser, methodName)
try:
return method(section, option)
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
return None
def get(self, section, option):
return self._getter('', section, option)
def getboolean(self, section, option):
return self._getter('boolean', section, option)
### drive sections
def setReadOffset(self, vendor, model, release, offset):
"""
Set a read offset for the given drive.
@@ -96,13 +122,6 @@ class Config(log.Loggable):
raise KeyError("Could not find defeats_cache for %s/%s/%s" % (
vendor, model, release))
def write(self):
fd, path = tempfile.mkstemp(suffix=u'.moriturirc')
handle = os.fdopen(fd, 'w')
self._parser.write(handle)
handle.close()
shutil.move(path, self._path)
def _findDriveSection(self, vendor, model, release):
for name in self._parser.sections():
if not name.startswith('drive:'):

View File

@@ -34,7 +34,7 @@ class Directory(log.Loggable):
path = os.path.join(directory, 'morituri.conf')
self.info('Using XDG, configuration file is %s' % path)
except ImportError:
path = os.path.expanduser('~/.moriturirc')
path = os.path.join(os.path.expanduser('~'), '.moriturirc')
self.info('Not using XDG, configuration file is %s' % path)
return path
@@ -44,8 +44,9 @@ class Directory(log.Loggable):
from xdg import BaseDirectory
path = BaseDirectory.save_cache_path('morituri')
self.info('Using XDG, cache directory is %s' % path)
except ImportError:
path = os.path.expanduser('~/.morituri/cache')
except (ImportError, AttributeError):
# save_cache_path was added in pyxdg 0.25
path = os.path.join(os.path.expanduser('~'), '.morituri', 'cache')
if not os.path.exists(path):
os.makedirs(path)
self.info('Not using XDG, cache directory is %s' % path)
@@ -65,10 +66,11 @@ class Directory(log.Loggable):
path = BaseDirectory.save_cache_path('morituri')
self.info('For XDG, read cache directory is %s' % path)
paths.append(path)
except ImportError:
except (ImportError, AttributeError):
# save_cache_path was added in pyxdg 0.21
pass
path = os.path.expanduser('~/.morituri/cache')
path = os.path.join(os.path.expanduser('~'), '.morituri', 'cache')
if os.path.exists(path):
self.info('From before XDG, read cache directory is %s' % path)
paths.append(path)

View File

@@ -34,7 +34,8 @@ def _listify(listOrString):
def getAllDevicePaths():
try:
return _getAllDevicePathsPyCdio()
# see https://savannah.gnu.org/bugs/index.php?38477
return [str(dev) for dev in _getAllDevicePathsPyCdio()]
except ImportError:
log.info('drive', 'Cannot import pycdio')
return _getAllDevicePathsStatic()

View File

@@ -32,7 +32,8 @@ from morituri.common import task as ctask
from morituri.extern.task import task, gstreamer
class Profile(object):
class Profile(log.Loggable):
name = None
extension = None
pipeline = None
@@ -99,20 +100,45 @@ class WavpackProfile(Profile):
lossless = True
class MP3Profile(Profile):
class _LameProfile(Profile):
extension = 'mp3'
lossless = False
def test(self):
version = cgstreamer.elementFactoryVersion('lamemp3enc')
self.debug('lamemp3enc version: %r', version)
if version:
t = tuple([int(s) for s in version.split('.')])
if t >= (0, 10, 19):
self.pipeline = self._lamemp3enc_pipeline
return True
version = cgstreamer.elementFactoryVersion('lame')
self.debug('lame version: %r', version)
if version:
self.pipeline = self._lame_pipeline
return True
return False
class MP3Profile(_LameProfile):
name = 'mp3'
extension = 'mp3'
pipeline = 'lame name=tagger quality=0 ! id3v2mux'
lossless = False
_lame_pipeline = 'lame name=tagger quality=0 ! id3v2mux'
_lamemp3enc_pipeline = \
'lamemp3enc name=tagger target=bitrate cbr=true bitrate=320 ! ' \
'xingmux ! id3v2mux'
class MP3VBRProfile(Profile):
class MP3VBRProfile(_LameProfile):
name = 'mp3vbr'
extension = 'mp3'
pipeline = 'lame name=tagger ' \
'vbr-quality=0 vbr=new vbr-mean-bitrate=192 ! ' \
'id3v2mux'
lossless = False
_lame_pipeline = 'lame name=tagger ' \
'vbr-quality=0 vbr=new vbr-mean-bitrate=192 ! ' \
'id3v2mux'
_lamemp3enc_pipeline = 'lamemp3enc name=tagger quality=0 ' \
'! xingmux ! id3v2mux'
class VorbisProfile(Profile):
@@ -167,6 +193,7 @@ class EncodeTask(ctask.GstPipelineTask):
self._inpath = inpath
self._outpath = outpath
self._taglist = taglist
self._length = 0 # in samples
self._level = None
self._peakdB = None
@@ -178,14 +205,19 @@ class EncodeTask(ctask.GstPipelineTask):
cgstreamer.removeAudioParsers()
def getPipelineDesc(self):
# start with an emit interval of one frame, because we end up setting
# the final interval after paused and after processing some samples
# already, which is too late
interval = int(self.gst.SECOND / 75.0)
return '''
filesrc location="%s" !
decodebin name=decoder !
audio/x-raw-int,width=16,depth=16,channels=2 !
level name=level !
level name=level interval=%d !
%s ! identity name=identity !
filesink location="%s" name=sink''' % (
gstreamer.quoteParse(self._inpath).encode('utf-8'),
interval,
self._profile.pipeline,
gstreamer.quoteParse(self._outpath).encode('utf-8'))
@@ -239,9 +271,11 @@ class EncodeTask(ctask.GstPipelineTask):
# set an interval that is smaller than the duration
# FIXME: check level and make sure it emits level up to the last
# sample, even if input is small
interval = 1000000000L
if interval < duration:
interval = self.gst.SECOND
if interval > duration:
interval = duration / 2
self.debug('Setting level interval to %s, duration %s',
self.gst.TIME_ARGS(interval), self.gst.TIME_ARGS(duration))
self._level.set_property('interval', interval)
# add a probe so we can track progress
# we connect to level because this gives us offset in samples
@@ -291,10 +325,16 @@ class EncodeTask(ctask.GstPipelineTask):
if self._peakdB is not None:
self.debug('peakdB %r', self._peakdB)
self.peak = math.sqrt(math.pow(10, self._peakdB / 10.0))
else:
self.warning('No peak found, something went wrong!')
return
self.warning('No peak found.')
if self._duration:
self.warning('GStreamer level element did not send messages.')
# workaround for when the file is too short to have volume ?
# self.peak = 0.0
if self._length == common.SAMPLES_PER_FRAME:
self.warning('only one frame of audio, setting peak to 0.0')
self.peak = 0.0
class TagReadTask(ctask.GstPipelineTask):

View File

@@ -42,7 +42,7 @@ def removeAudioParsers():
plugin = registry.find_plugin("audioparsers")
if plugin:
log.debug('gstreamer', 'Found audioparsers plugin from %s %s',
log.debug('gstreamer', 'removing audioparsers plugin from %s %s',
plugin.get_source(), plugin.get_version())
# the query bug was fixed after 0.10.30 and before 0.10.31

View File

@@ -1,4 +1,4 @@
# -*- Mode: Python; test-case-name: morituri.test.test_common_musicbrainzngs -*-
# -*- Mode: Python; test-case-name: morituri.test.test_common_mbngs -*-
# vi:si:et:sw=4:sts=4:ts=4
# Morituri - for those about to RIP
@@ -56,10 +56,13 @@ class TrackMetadata(object):
class DiscMetadata(object):
"""
@param artist: artist(s) name
@param sortName: album artist sort name
@param release: earliest release date, in YYYY-MM-DD
@type release: unicode
@param title: title of the disc (with disambiguation)
@param releaseTitle: title of the release (without disambiguation)
@type tracks: C{list} of L{TrackMetadata}
"""
artist = None
sortName = None
@@ -90,7 +93,55 @@ def _record(record, which, name, what):
handle = open(filename, 'w')
handle.write(json.dumps(what))
handle.close()
log.info('musicbrainzngs', 'Wrote %s %s to %s', which, name, filename)
log.info('mbngs', 'Wrote %s %s to %s', which, name, filename)
# credit is of the form [dict, str, dict, ... ]
# e.g. [
# {'artist': {
# 'sort-name': 'Sukilove',
# 'id': '5f4af6cf-a1b8-4e51-a811-befed399a1c6',
# 'name': 'Sukilove'
# }}, ' & ', {
# 'artist': {
# 'sort-name': 'Blackie and the Oohoos',
# 'id': '028a9dc7-f5ef-43c2-866b-08d69ffff363',
# 'name': 'Blackie & the Oohoos'}}]
# or
# [{'artist':
# {'sort-name': 'Pixies',
# 'id': 'b6b2bb8d-54a9-491f-9607-7b546023b433', 'name': 'Pixies'}}]
class _Credit(list):
"""
I am a representation of an artist-credit in musicbrainz for a disc
or track.
"""
def joiner(self, attributeGetter, joinString=None):
res = []
for item in self:
if isinstance(item, dict):
res.append(attributeGetter(item))
else:
if not joinString:
res.append(item)
else:
res.append(joinString)
return "".join(res)
def getSortName(self):
return self.joiner(lambda i: i.get('artist').get('sort-name', None))
def getName(self):
return self.joiner(lambda i: i.get('artist').get('name', None))
def getIds(self):
return self.joiner(lambda i: i.get('artist').get('id', None),
joinString=";")
def _getMetadata(releaseShort, release, discid):
@@ -109,45 +160,39 @@ def _getMetadata(releaseShort, release, discid):
assert release['id'], 'Release does not have an id'
metadata = DiscMetadata()
discMD = DiscMetadata()
metadata.releaseType = releaseShort.get('release-group', {}).get('type')
credit = release['artist-credit']
discMD.releaseType = releaseShort.get('release-group', {}).get('type')
discCredit = _Credit(release['artist-credit'])
artist = credit[0]['artist']
# FIXME: is there a better way to check for VA ?
discMD.various = False
if discCredit[0]['artist']['id'] == VA_ID:
discMD.various = True
if len(credit) > 1:
log.debug('musicbrainzngs', 'artist-credit more than 1: %r', credit)
for i, c in enumerate(credit):
if isinstance(c, dict):
credit[i] = c.get(
'name', c['artist'].get('name', None))
if len(discCredit) > 1:
log.debug('mbngs', 'artist-credit more than 1: %r', discCredit)
albumArtistName = "".join(credit)
# FIXME: is there a better way to check for VA
metadata.various = False
if artist['id'] == VA_ID:
metadata.various = True
albumArtistName = discCredit.getName()
# getUniqueName gets disambiguating names like Muse (UK rock band)
metadata.artist = albumArtistName
metadata.sortName = artist['sort-name']
discMD.artist = albumArtistName
discMD.sortName = discCredit.getSortName()
# FIXME: is format str ?
if not 'date' in release:
log.warning('musicbrainzngs', 'Release %r does not have date', release)
log.warning('mbngs', 'Release %r does not have date', release)
else:
metadata.release = release['date']
discMD.release = release['date']
metadata.mbid = release['id']
metadata.mbidArtist = artist['id']
metadata.url = 'http://musicbrainz.org/release/' + release['id']
discMD.mbid = release['id']
discMD.mbidArtist = discCredit.getIds()
discMD.url = 'http://musicbrainz.org/release/' + release['id']
metadata.barcode = release.get('barcode', None)
discMD.barcode = release.get('barcode', None)
lil = release.get('label-info-list', [{}])
if lil:
metadata.catalogNumber = lil[0].get('catalog-number')
discMD.catalogNumber = lil[0].get('catalog-number')
tainted = False
duration = 0
@@ -156,7 +201,7 @@ def _getMetadata(releaseShort, release, discid):
for disc in medium['disc-list']:
if disc['id'] == discid:
title = release['title']
metadata.releaseTitle = title
discMD.releaseTitle = title
if 'disambiguation' in release:
title += " (%s)" % release['disambiguation']
count = len(release['medium-list'])
@@ -165,31 +210,19 @@ def _getMetadata(releaseShort, release, discid):
int(medium['position']), count)
if 'title' in medium:
title += ": %s" % medium['title']
metadata.title = title
discMD.title = title
for t in medium['track-list']:
track = TrackMetadata()
credit = t['recording']['artist-credit']
if len(credit) > 1:
log.debug('musicbrainzngs',
'artist-credit more than 1: %r', credit)
# credit is of the form [dict, str, dict, ... ]
for i, c in enumerate(credit):
if isinstance(c, dict):
credit[i] = c.get(
'name', c['artist'].get('name', None))
trackCredit = _Credit(t['recording']['artist-credit'])
if len(trackCredit) > 1:
log.debug('mbngs',
'artist-credit more than 1: %r', trackCredit)
trackArtistName = "".join(credit)
if not artist:
track.artist = metadata.artist
track.sortName = metadata.sortName
track.mbidArtist = metadata.mbidArtist
else:
# various artists discs can have tracks with no artist
track.artist = trackArtistName
track.sortName = artist['sort-name']
track.mbidArtist = artist['id']
# FIXME: leftover comment, need an example
# various artists discs can have tracks with no artist
track.artist = trackCredit.getName()
track.sortName = trackCredit.getSortName()
track.mbidArtist = trackCredit.getIds()
track.title = t['recording']['title']
track.mbid = t['recording']['id']
@@ -204,14 +237,14 @@ def _getMetadata(releaseShort, release, discid):
else:
duration += track.duration
metadata.tracks.append(track)
discMD.tracks.append(track)
if not tainted:
metadata.duration = duration
discMD.duration = duration
else:
metadata.duration = 0
discMD.duration = 0
return metadata
return discMD
# see http://bugs.musicbrainz.org/browser/python-musicbrainz2/trunk/examples/

68
morituri/common/path.py Normal file
View File

@@ -0,0 +1,68 @@
# -*- Mode: Python; test-case-name: morituri.test.test_common_path -*-
# 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 re
class PathFilter(object):
"""
I filter path components for safe storage on file systems.
"""
def __init__(self, slashes=True, quotes=True, fat=True, special=False):
"""
@param slashes: whether to convert slashes to dashes
@param quotes: whether to normalize quotes
@param fat: whether to strip characters illegal on FAT filesystems
@param special: whether to strip special characters
"""
self._slashes = slashes
self._quotes = quotes
self._fat = fat
self._special = special
def filter(self, path):
if self._slashes:
path = re.sub(r'[/\\]', '-', path, re.UNICODE)
def separators(path):
# replace separators with a space-hyphen or hyphen
path = re.sub(r'[:]', ' -', path, re.UNICODE)
path = re.sub(r'[\|]', '-', path, re.UNICODE)
return path
# change all fancy single/double quotes to normal quotes
if self._quotes:
path = re.sub(ur'[\xc2\xb4\u2018\u2019\u201b]', "'", path,
re.UNICODE)
path = re.sub(ur'[\u201c\u201d\u201f]', '"', path, re.UNICODE)
if self._special:
path = separators(path)
path = re.sub(r'[\*\?&!\'\"\$\(\)`{}\[\]<>]', '_', path, re.UNICODE)
if self._fat:
path = separators(path)
# : and | already gone, but leave them here for reference
path = re.sub(r'[:\*\?"<>|"]', '_', path, re.UNICODE)
return path

View File

@@ -28,13 +28,12 @@ import os
import sys
import time
from morituri.common import common, log, musicbrainzngs, cache
from morituri.common import common, log, mbngs, cache, path
from morituri.program import cdrdao, cdparanoia
from morituri.image import image
def filterForPath(text):
return "-".join(text.split("/"))
from morituri.extern.task import task, gstreamer
from morituri.extern.musicbrainzngs import musicbrainz
# FIXME: should Program have a runner ?
@@ -45,10 +44,11 @@ class Program(log.Loggable):
I maintain program state and functionality.
@ivar metadata:
@type metadata: L{musicbrainz.DiscMetadata}
@type metadata: L{mbngs.DiscMetadata}
@ivar result: the rip's result
@type result: L{result.RipResult}
@type outdir: unicode
@type config: L{morituri.common.config.Config}
"""
cuePath = None
@@ -59,13 +59,29 @@ class Program(log.Loggable):
_stdout = None
def __init__(self, record=False, stdout=sys.stdout):
def __init__(self, config, record=False, stdout=sys.stdout):
"""
@param record: whether to record results of API calls for playback.
"""
self._record = record
self._cache = cache.ResultCache()
self._stdout = stdout
self._config = config
d = {}
for key, default in {
'fat': True,
'special': False
}.items():
value = None
value = self._config.getboolean('main', 'path_filter_'+ key)
if value is None:
value = default
d[key] = value
self._filter = path.PathFilter(**d)
def setWorkingDirectory(self, workingDirectory):
if workingDirectory:
@@ -101,8 +117,9 @@ class Program(log.Loggable):
def getFastToc(self, runner, toc_pickle, device):
"""
Retrieve the normal TOC table from a toc pickle or the drive.
Also retrieves the cdrdao version
@rtype: L{table.Table}
@rtype: tuple of L{table.Table}, str
"""
def function(r, t):
r.run(t)
@@ -150,8 +167,12 @@ class Program(log.Loggable):
t = cdrdao.ReadTableTask(device=device)
runner.run(t)
ptable.persist(t.table)
self.debug('getTable: read table %r' % t.table)
else:
self.debug('getTable: cddbdiscid %s in cache' % cddbdiscid)
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
assert itable.hasTOC()
@@ -218,6 +239,7 @@ class Program(log.Loggable):
v['C'] = '' # catalog number
v['x'] = profile and profile.extension or 'unknown'
v['X'] = v['x'].upper()
v['y'] = '0000'
v['a'] = v['A']
if i == 0:
@@ -229,9 +251,9 @@ class Program(log.Loggable):
if self.metadata:
release = self.metadata.release or '0000'
v['y'] = release[:4]
v['A'] = filterForPath(self.metadata.artist)
v['S'] = filterForPath(self.metadata.sortName)
v['d'] = filterForPath(self.metadata.title)
v['A'] = self._filter.filter(self.metadata.artist)
v['S'] = self._filter.filter(self.metadata.sortName)
v['d'] = self._filter.filter(self.metadata.title)
v['B'] = self.metadata.barcode
v['C'] = self.metadata.catalogNumber
if self.metadata.releaseType:
@@ -239,16 +261,16 @@ class Program(log.Loggable):
v['r'] = self.metadata.releaseType.lower()
if i > 0:
try:
v['a'] = filterForPath(self.metadata.tracks[i - 1].artist)
v['s'] = filterForPath(
v['a'] = self._filter.filter(self.metadata.tracks[i - 1].artist)
v['s'] = self._filter.filter(
self.metadata.tracks[i - 1].sortName)
v['n'] = filterForPath(self.metadata.tracks[i - 1].title)
v['n'] = self._filter.filter(self.metadata.tracks[i - 1].title)
except IndexError, e:
print 'ERROR: no track %d found, %r' % (i, e)
raise
else:
# htoa defaults to disc's artist
v['a'] = filterForPath(self.metadata.artist)
v['a'] = self._filter.filter(self.metadata.artist)
# when disambiguating, use catalogNumber then barcode
if disambiguate:
@@ -277,10 +299,18 @@ class Program(log.Loggable):
"""
# FIXME: convert to nonblocking?
import CDDB
code, md = CDDB.query(cddbdiscid)
self.debug('CDDB query result: %r, %r', code, md)
if code == 200:
return md['title']
try:
code, md = CDDB.query(cddbdiscid)
self.debug('CDDB query result: %r, %r', code, md)
if code == 200:
return md['title']
except IOError, e:
# FIXME: for some reason errno is a str ?
if e.errno == 'socket error':
self._stdout.write("Warning: network error: %r\n" % (e, ))
else:
raise
return None
@@ -301,11 +331,14 @@ class Program(log.Loggable):
for _ in range(0, 4):
try:
metadatas = musicbrainzngs.musicbrainz(mbdiscid,
metadatas = mbngs.musicbrainz(mbdiscid,
record=self._record)
except musicbrainzngs.NotFoundException, e:
except mbngs.NotFoundException, e:
break
except musicbrainzngs.MusicBrainzException, e:
except musicbrainz.NetworkError, e:
self._stdout.write("Warning: network error: %r\n" % (e, ))
break
except mbngs.MusicBrainzException, e:
self._stdout.write("Warning: %r\n" % (e, ))
time.sleep(5)
continue
@@ -351,7 +384,8 @@ class Program(log.Loggable):
metadatas[0].title.encode('utf-8'))
elif not metadatas:
self._stdout.write(
'Requested release id %s but none match' % release)
"Requested release id '%s', "
"but none of the found releases match\n" % release)
return
else:
# Select the release that most closely matches the duration.
@@ -503,7 +537,17 @@ class Program(log.Loggable):
t = checksum.CRC32Task(trackResult.filename)
runner.run(t)
try:
runner.run(t)
except task.TaskException, e:
if isinstance(e.exception, common.MissingFrames):
self.warning('missing frames for %r' % trackResult.filename)
return False
elif isinstance(e.exception, gstreamer.GstException):
self.warning('GstException %r' % (e.exception, ))
return False
else:
raise
ret = trackResult.testcrc == t.checksum
log.debug('program',
@@ -698,7 +742,8 @@ class Program(log.Loggable):
def writeLog(self, discName, logger):
logPath = '%s.log' % discName
handle = open(logPath, 'w')
handle.write(logger.log(self.result).encode('utf-8'))
log = logger.log(self.result)
handle.write(log.encode('utf-8'))
handle.close()
self.logPath = logPath

View File

@@ -49,24 +49,23 @@ class Operator(object):
Verifies the state.
"""
todo = os.path.join(self._statePath, self._key + '.todo')
handle = open(todo, 'r')
lines = []
for line in handle.readlines():
lines.append(line)
name, data = line.split(' ', 1)
cls = globals()[name]
operation = cls.deserialize(data)
self._todo.append(operation)
with open(todo, 'r') as handle:
for line in handle.readlines():
lines.append(line)
name, data = line.split(' ', 1)
cls = globals()[name]
operation = cls.deserialize(data)
self._todo.append(operation)
done = os.path.join(self._statePath, self._key + '.done')
i = 0
if os.path.exists(done):
handle = open(done, 'r')
for i, line in enumerate(handle.readlines()):
assert line == lines[i], "line %s is different than %s" % (
line, lines[i])
self._done.append(self._todo[i])
with open(done, 'r') as handle:
for i, line in enumerate(handle.readlines()):
assert line == lines[i], "line %s is different than %s" % (
line, lines[i])
self._done.append(self._todo[i])
# last task done is i; check if the next one might have gotten done.
self._resuming = True
@@ -78,21 +77,19 @@ class Operator(object):
# only save todo first time
todo = os.path.join(self._statePath, self._key + '.todo')
if not os.path.exists(todo):
handle = open(todo, 'w')
for o in self._todo:
name = o.__class__.__name__
data = o.serialize()
handle.write('%s %s\n' % (name, data))
handle.close()
with open(todo, 'w') as handle:
for o in self._todo:
name = o.__class__.__name__
data = o.serialize()
handle.write('%s %s\n' % (name, data))
# save done every time
done = os.path.join(self._statePath, self._key + '.done')
handle = open(done, 'w')
for o in self._done:
name = o.__class__.__name__
data = o.serialize()
handle.write('%s %s\n' % (name, data))
handle.close()
with open(done, 'w') as handle:
for o in self._done:
name = o.__class__.__name__
data = o.serialize()
handle.write('%s %s\n' % (name, data))
def start(self):
"""
@@ -203,15 +200,14 @@ class RenameInFile(Operation):
# check if the source exists in the given file
def do(self):
handle = open(self._path)
(fd, name) = tempfile.mkstemp(suffix='.morituri')
with open(self._path) as handle:
(fd, name) = tempfile.mkstemp(suffix='.morituri')
for s in handle:
os.write(fd, s.replace(self._source, self._destination))
for s in handle:
os.write(fd, s.replace(self._source, self._destination))
handle.close()
os.close(fd)
os.rename(name, self._path)
os.close(fd)
os.rename(name, self._path)
def serialize(self):
return '"%s" "%s" "%s"' % (self._path, self._source, self._destination)

View File

@@ -22,14 +22,18 @@ import os
# where am I on the disk ?
__thisdir = os.path.dirname(os.path.abspath(__file__))
revision = "$Revision$"
if os.path.exists(os.path.join(__thisdir, 'uninstalled.py')):
from morituri.configure import uninstalled
config_dict = uninstalled.get()
else:
elif os.path.exists(os.path.join(__thisdir, 'installed.py')):
from morituri.configure import installed
config_dict = installed.get()
else:
# hack on fresh checkout, no make run yet, and configure needs revision
from morituri.common import common
config_dict = {
'revision': common.getRevision(),
}
for key, value in config_dict.items():
dictionary = locals()

View File

@@ -7,4 +7,5 @@ def get():
'isinstalled': True,
'pluginsdir': '@PLUGINSDIR@',
'version': '@VERSION@',
'revision': '@REVISION@',
}

View File

@@ -1,7 +1,9 @@
# -*- Mode: Python -*-
# vi:si:et:sw=4:sts=4:ts=4
import os
import os.path
from morituri.common import common
__thisdir = os.path.dirname(os.path.abspath(__file__))
@@ -13,4 +15,5 @@ def get():
'pluginsdir': os.path.abspath(os.path.join(
__thisdir, '..', '..', 'plugins')),
'version': '@VERSION@',
'revision': common.getRevision(),
}

View File

@@ -44,4 +44,15 @@ musicbrainzngs_PYTHON = \
EXTRA_DIST = python-command/scripts/help2man
musicbrainzngs/musicbrainz.py: all
log:
make git-submodule
musicbrainzngs:
make git-submodule
git-submodule:
cd $(top_srcdir) && git submodule init && git submodule sync && git submodule update
.PHONY: git-submodule
all-local: log musicbrainzngs

View File

@@ -95,6 +95,12 @@ class Track:
return self.indexes[number]
def getFirstIndex(self):
"""
Get the first chronological index for this track.
Typically this is INDEX 01; but it could be INDEX 00 if there's
a pre-gap.
"""
indexes = self.indexes.keys()
indexes.sort()
return self.indexes[indexes[0]]
@@ -162,7 +168,7 @@ class Table(object, log.Loggable):
catalog = None # catalog number; FIXME: is this UPC ?
cdtext = None
classVersion = 2
classVersion = 4
def __init__(self, tracks=None):
if not tracks:
@@ -170,10 +176,14 @@ class Table(object, log.Loggable):
self.tracks = tracks
self.cdtext = {}
self.logName = "Table 0x%08X" % id(self)
# done this way because just having a class-defined instance var
# gets overridden when unpickling
self.instanceVersion = self.classVersion
self.unpickled()
def unpickled(self):
self.logName = "Table 0x%08x v%d" % (id(self), self.instanceVersion)
self.debug('set logName')
def getTrackStart(self, number):
"""
@@ -510,12 +520,18 @@ class Table(object, log.Loggable):
Dump our internal representation to a .cue file content.
@rtype: C{unicode}
"""
self.debug('generating .cue for cuePath %r', cuePath)
lines = []
def writeFile(path):
targetPath = common.getRelativePath(path, cuePath)
lines.append('FILE "%s" WAVE' % targetPath)
line = 'FILE "%s" WAVE' % targetPath
lines.append(line)
self.debug('writeFile: %r' % line)
# header
main = ['PERFORMER', 'TITLE']
@@ -535,41 +551,91 @@ class Table(object, log.Loggable):
if key in self.cdtext:
lines.append('%s "%s"' % (key, self.cdtext[key]))
# add the first FILE line
path = self.tracks[0].getFirstIndex().path
counter = self.tracks[0].getFirstIndex().counter
writeFile(path)
# FIXME:
# - the first FILE statement goes before the first TRACK, even if
# there is a non-file-using PREGAP
# - the following FILE statements come after the last INDEX that
# use that FILE; so before a next TRACK, PREGAP silence, ...
# add the first FILE line; EAC always puts the first FILE
# statement before TRACK 01 and any possible PRE-GAP
firstTrack = self.tracks[0]
index = firstTrack.getFirstIndex()
indexOne = firstTrack.getIndex(1)
counter = index.counter
track = firstTrack
while not index.path:
t, i = self.getNextTrackIndex(track.number, index.number)
track = self.tracks[t - 1]
index = track.getIndex(i)
counter = index.counter
if index.path:
self.debug('counter %d, writeFile' % counter)
writeFile(index.path)
for i, track in enumerate(self.tracks):
self.debug('track i %r, track %r' % (i, track))
# FIXME: skip data tracks for now
if not track.audio:
continue
# if there is no index 0, but there is a new file, advance
# FILE line here
if not 0 in track.indexes:
index = track.indexes[1]
if index.counter != counter:
writeFile(index.path)
counter = index.counter
lines.append(" TRACK %02d %s" % (i + 1, 'AUDIO'))
for key in CDTEXT_FIELDS:
if key in track.cdtext:
lines.append(' %s "%s"' % (key, track.cdtext[key]))
if track.isrc is not None:
lines.append(" ISRC %s" % track.isrc)
indexes = track.indexes.keys()
indexes.sort()
wroteTrack = False
for number in indexes:
index = track.indexes[number]
if index.counter != counter:
writeFile(index.path)
self.debug('index %r, %r' % (number, index))
# any time the source counter changes to a higher value,
# write a FILE statement
# it has to be higher, because we can run into the HTOA
# at counter 0 here
if index.counter > counter:
if index.path:
self.debug('counter %d, writeFile' % counter)
writeFile(index.path)
self.debug('setting counter to index.counter %r' %
index.counter)
counter = index.counter
lines.append(" INDEX %02d %s" % (number,
common.framesToMSF(index.relative)))
# any time we hit the first index, write a TRACK statement
if not wroteTrack:
wroteTrack = True
line = " TRACK %02d %s" % (i + 1, 'AUDIO')
lines.append(line)
self.debug('%r' % line)
for key in CDTEXT_FIELDS:
if key in track.cdtext:
lines.append(' %s "%s"' % (
key, track.cdtext[key]))
if track.isrc is not None:
lines.append(" ISRC %s" % track.isrc)
# handle TRACK 01 INDEX 00 specially
if 0 in indexes:
index00 = track.indexes[0]
if i == 0:
# if we have a silent pre-gap, output it
if not index00.path:
length = indexOne.absolute - index00.absolute
lines.append(" PREGAP %s" %
common.framesToMSF(length))
continue
# handle any other INDEX 00 after its TRACK
lines.append(" INDEX %02d %s" % (0,
common.framesToMSF(index00.relative)))
if number > 0:
# index 00 is output after TRACK up above
lines.append(" INDEX %02d %s" % (number,
common.framesToMSF(index.relative)))
lines.append("")
@@ -607,6 +673,9 @@ class Table(object, log.Loggable):
to adjust the path.
Assumes all indexes have an absolute offset and will raise if not.
@type track: C{int}
@type index: C{int}
"""
self.debug('setFile: track %d, index %d, path %r, '
'length %r, counter %r', track, index, path, length, counter)

View File

@@ -22,6 +22,8 @@
"""
Reading .toc files
The .toc file format is described in the man page of cdrdao
"""
import re
@@ -62,7 +64,7 @@ _FILE_RE = re.compile(r"""
^FILE # FILE
\s+"(?P<name>.*)" # 'file name' in quotes
\s+(?P<start>.+) # start offset
\s(?P<length>.+)$ # stop offset
\s(?P<length>.+)$ # length in frames of section
""", re.VERBOSE)
_DATAFILE_RE = re.compile(r"""
@@ -86,6 +88,48 @@ _INDEX_RE = re.compile(r"""
""", re.VERBOSE)
class Sources(log.Loggable):
"""
I represent the list of sources used in the .toc file.
Each SILENCE and each FILE is a source.
If the filename for FILE doesn't change, the counter is not increased.
"""
def __init__(self):
self._sources = []
def append(self, counter, offset, source):
"""
@param counter: the source counter; updates for each different
data source (silence or different file path)
@type counter: int
@param offset: the absolute disc offset where this source starts
"""
self.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.
"""
for i, (c, o, s) in enumerate(self._sources):
if offset < o:
return self._sources[i - 1]
return self._sources[-1]
def getCounterStart(self, counter):
"""
Retrieve the absolute offset of the first source for this counter
"""
for i, (c, o, s) in enumerate(self._sources):
if c == counter:
return self._sources[i][1]
return self._sources[-1][1]
class TocFile(object, log.Loggable):
def __init__(self, path):
@@ -98,6 +142,27 @@ class TocFile(object, log.Loggable):
self.table = table.Table()
self.logName = '<TocFile %08x>' % id(self)
self._sources = Sources()
def _index(self, currentTrack, i, absoluteOffset, trackOffset):
absolute = absoluteOffset + trackOffset
# this may be in a new source, so calculate relative
c, o, s = self._sources.get(absolute)
self.debug('at abs offset %d, we are in source %r' % (
absolute, s))
counterStart = self._sources.getCounterStart(c)
relative = absolute - counterStart
currentTrack.index(i, path=s.path,
absolute=absolute,
relative=relative,
counter=c)
self.debug(
'[track %02d index %02d] trackOffset %r, added %r',
currentTrack.number, i, trackOffset,
currentTrack.getIndex(i))
def parse(self):
# these two objects start as None then get set as real objects,
# so no need to complain about them here
@@ -106,15 +171,16 @@ class TocFile(object, log.Loggable):
currentTrack = None
state = 'HEADER'
counter = 0
counter = 0 # counts sources for audio data; SILENCE/ZERO/FILE
trackNumber = 0
indexNumber = 0
absoluteOffset = 0 # running absolute offset of where each track starts
relativeOffset = 0 # running relative offset, relative to counter src
currentLength = 0 # accrued during TRACK record parsing, current track
currentLength = 0 # accrued during TRACK record parsing;
# length of current track as parsed so far;
# reset on each TRACK statement
totalLength = 0 # accrued during TRACK record parsing, total disc
pregapLength = 0 # length of the pre-gap, current track
pregapLength = 0 # length of the pre-gap, current track in for loop
# the first track's INDEX 1 can only be gotten from the .toc
# file once the first pregap is calculated; so we add INDEX 1
@@ -160,28 +226,29 @@ class TocFile(object, log.Loggable):
# set index 1 of previous track if there was one, using
# pregapLength if applicable
if currentTrack:
# FIXME: why not set absolute offsets too ?
currentTrack.index(1, path=currentFile.path,
absolute=absoluteOffset + pregapLength,
relative=relativeOffset + pregapLength,
counter=counter)
self.debug('track %d, added index %r',
currentTrack.number, currentTrack.getIndex(1))
self._index(currentTrack, 1, absoluteOffset, pregapLength)
# create a new track to be filled by later lines
trackNumber += 1
absoluteOffset += currentLength
relativeOffset += currentLength
totalLength += currentLength
currentLength = 0
indexNumber = 1
trackMode = m.group('mode')
pregapLength = 0
# FIXME: track mode
self.debug('found track %d, mode %s', trackNumber, trackMode)
audio = trackMode == 'AUDIO'
currentTrack = table.Track(trackNumber, audio=audio)
self.table.tracks.append(currentTrack)
# update running totals
absoluteOffset += currentLength
relativeOffset += currentLength
totalLength += currentLength
# FIXME: track mode
self.debug('found track %d, mode %s, at absoluteOffset %d',
trackNumber, trackMode, absoluteOffset)
# reset counters relative to a track
currentLength = 0
indexNumber = 1
pregapLength = 0
continue
# look for ISRC lines
@@ -196,9 +263,11 @@ class TocFile(object, log.Loggable):
if m:
length = m.group('length')
self.debug('SILENCE of %r', length)
self._sources.append(counter, absoluteOffset, None)
if currentFile is not None:
self.debug('SILENCE after FILE, increasing counter')
counter += 1
relativeOffset = 0
currentFile = None
currentLength += common.msfToFrames(length)
@@ -208,6 +277,7 @@ class TocFile(object, log.Loggable):
if currentFile is not None:
self.debug('ZERO after FILE, increasing counter')
counter += 1
relativeOffset = 0
currentFile = None
length = m.group('length')
currentLength += common.msfToFrames(length)
@@ -227,7 +297,10 @@ class TocFile(object, log.Loggable):
self.debug('track %d, switched to new FILE, '
'increased counter to %d',
trackNumber, counter)
currentFile = File(filePath, start, length)
currentFile = File(filePath, common.msfToFrames(start),
common.msfToFrames(length))
self._sources.append(counter, absoluteOffset + currentLength,
currentFile)
#absoluteOffset += common.msfToFrames(start)
currentLength += common.msfToFrames(length)
@@ -246,7 +319,9 @@ class TocFile(object, log.Loggable):
'increased counter to %d',
trackNumber, counter)
# FIXME: assume that a MODE2_FORM_MIX track always starts at 0
currentFile = File(filePath, 0, length)
currentFile = File(filePath, 0, common.msfToFrames(length))
self._sources.append(counter, absoluteOffset + currentLength,
currentFile)
#absoluteOffset += common.msfToFrames(start)
currentLength += common.msfToFrames(length)
@@ -260,10 +335,16 @@ class TocFile(object, log.Loggable):
continue
length = common.msfToFrames(m.group('length'))
currentTrack.index(0, path=currentFile.path,
c, o, s = self._sources.get(absoluteOffset)
self.debug('at abs offset %d, we are in source %r' % (
absoluteOffset, s))
counterStart = self._sources.getCounterStart(c)
relativeOffset = absoluteOffset - counterStart
currentTrack.index(0, path=s and s.path or None,
absolute=absoluteOffset,
relative=relativeOffset, counter=counter)
self.debug('track %d, added index %r',
relative=relativeOffset, counter=c)
self.debug('[track %02d index 00] added %r',
currentTrack.number, currentTrack.getIndex(0))
# store the pregapLength to add it when we index 1 for this
# track on the next iteration
@@ -279,22 +360,15 @@ class TocFile(object, log.Loggable):
indexNumber += 1
offset = common.msfToFrames(m.group('offset'))
currentTrack.index(indexNumber, path=currentFile.path,
relative=offset, counter=counter)
self.debug('track %d, added index %r',
currentTrack.number, currentTrack.getIndex(indexNumber))
self._index(currentTrack, indexNumber, absoluteOffset, offset)
# handle index 1 of final track, if any
if currentTrack:
currentTrack.index(1, path=currentFile.path,
absolute=absoluteOffset + pregapLength,
relative=relativeOffset + pregapLength, counter=counter)
self.debug('track %d, added index %r',
currentTrack.number, currentTrack.getIndex(1))
self._index(currentTrack, 1, absoluteOffset, pregapLength)
# totalLength was added up to the penultimate track
self.table.leadout = totalLength + currentLength
self.debug('leadout: %r', self.table.leadout)
self.debug('parse: leadout: %r', self.table.leadout)
def message(self, number, message):
"""
@@ -305,6 +379,10 @@ class TocFile(object, log.Loggable):
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
"""
# returns track length in frames, or -1 if can't be determined and
# complete file should be assumed
# FIXME: this assumes a track can only be in one file; is this true ?
@@ -340,13 +418,16 @@ class File:
def __init__(self, path, start, length):
"""
@type path: unicode
@type path: C{unicode}
@type start: C{int}
@param start: starting point for the track in this file, in frames
@param length: length for the track in this file, in frames
"""
assert type(path) is unicode, "%r is not unicode" % path
self.path = path
#self.start = start
#self.length = length
self.start = start
self.length = length
def __repr__(self):
return '<File %r>' % (self.path, )

View File

@@ -64,19 +64,21 @@ class ChecksumException(Exception):
pass
# example:
# ##: 0 [read] @ 24696
_PROGRESS_RE = re.compile(r"""
^\#\#: (?P<code>.+)\s # function code
\[(?P<function>.*)\]\s@\s # function name
(?P<offset>\d+) # offset
^\#\#: (?P<code>.+)\s # function code
\[(?P<function>.*)\]\s@\s # [function name] @
(?P<offset>\d+) # offset in words (2-byte one channel value)
""", re.VERBOSE)
_ERROR_RE = re.compile("^scsi_read error:")
# from reading cdparanoia source code, it looks like offset is reported in
# number of single-channel samples, ie. 2 bytes per unit, and absolute
# number of single-channel samples, ie. 2 bytes (word) per unit, and absolute
class ProgressParser(object):
class ProgressParser(log.Loggable):
read = 0 # last [read] frame
wrote = 0 # last [wrote] frame
errors = 0 # count of number of scsi errors
@@ -128,13 +130,15 @@ class ProgressParser(object):
# set nframes if not yet set
if self._nframes is None and self.read != 0:
self._nframes = frameOffset - self.read
self.debug('set nframes to %r', self._nframes)
# set firstFrames if not yet set
if self._firstFrames is None:
self._firstFrames = frameOffset - self.start
self.debug('set firstFrames to %r', self._firstFrames)
markStart = None
markEnd = None
markEnd = None # the next unread frame (half-inclusive)
# verify it either read nframes more or went back for verify
if frameOffset > self.read:
@@ -165,10 +169,11 @@ class ProgressParser(object):
# cdparanoia reads quite a bit beyond the current track before it
# goes back to verify; don't count those
if markEnd > self.stop:
markEnd = self.stop
if markStart > self.stop:
markStart = self.stop
# markStart, markEnd of 0, 21 with stop 0 should give 1 read
if markEnd > self.stop + 1:
markEnd = self.stop + 1
if markStart > self.stop + 1:
markStart = self.stop + 1
self.reads += markEnd - markStart
@@ -185,8 +190,9 @@ class ProgressParser(object):
Each frame gets read twice.
More than two reads for a frame reduce track quality.
"""
frames = self.stop - self.start + 1
frames = self.stop - self.start + 1 # + 1 since stop is inclusive
reads = self.reads
self.debug('getTrackQuality: frames %d, reads %d' % (frames, reads))
# don't go over a 100%; we know cdparanoia reads each frame at least
# twice
@@ -544,25 +550,12 @@ _VERSION_RE = re.compile(
def getCdParanoiaVersion():
version = "(Unknown)"
getter = common.VersionGetter('cdparanoia',
["cdparanoia", "-V"],
_VERSION_RE,
"%(version)s %(release)s")
try:
p = asyncsub.Popen(["cdparanoia", "-V"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, close_fds=True)
version = asyncsub.recv_some(p, e=0, stderr=1)
vre = _VERSION_RE.search(version)
if vre and len(vre.groups()) == 2:
version = "%s %s" % (
vre.groupdict().get('version'),
vre.groupdict().get('release'))
except OSError, e:
import errno
if e.errno == errno.ENOENT:
raise common.MissingDependencyException('cdparanoia')
raise
return version
return getter.get()
_OK_RE = re.compile(r'Drive tests OK with Paranoia.')

View File

@@ -290,7 +290,7 @@ class DiscInfoTask(CDRDAOTask):
@param device: the device to rip from
@type device: str
"""
self.debug('creating DiscInfoTask')
self.debug('creating DiscInfoTask for device %r', device)
CDRDAOTask.__init__(self)
self.options = ['disk-info', ]
@@ -342,6 +342,8 @@ class ReadSessionTask(CDRDAOTask):
@param device: the device to rip from
@type device: str
"""
self.debug('Creating ReadSessionTask for session %d on device %r',
session, device)
CDRDAOTask.__init__(self)
self.parser = OutputParser(self)
(fd, self._tocfilepath) = tempfile.mkstemp(
@@ -505,3 +507,16 @@ class ProgramFailedException(Exception):
def __init__(self, code):
self.code = code
self.args = (code, )
_VERSION_RE = re.compile(
"^Cdrdao version (?P<version>.+) -")
def getCDRDAOVersion():
getter = common.VersionGetter('cdrdao',
["cdrdao"],
_VERSION_RE,
"%(version)s")
return getter.get()

View File

@@ -23,17 +23,18 @@
import os
import math
import glob
import urllib2
import socket
import gobject
gobject.threads_init()
from morituri.common import logcommand, common, accurip, gstreamer
from morituri.common import drive, program, cache
from morituri.common import drive, program, task
from morituri.result import result
from morituri.program import cdrdao, cdparanoia
from morituri.rip import common as rcommon
from morituri.extern.task import task
from morituri.extern.command import command
@@ -42,6 +43,13 @@ MAX_TRIES = 5
class _CD(logcommand.LogCommand):
"""
@type program: L{program.Program}
@ivar eject: whether to eject the drive after completing
"""
eject = True
def addOptions(self):
# FIXME: have a cache of these pickles somewhere
self.parser.add_option('-T', '--toc-pickle',
@@ -53,7 +61,8 @@ class _CD(logcommand.LogCommand):
def do(self, args):
self.program = program.Program(record=self.getRootCommand().record,
self.program = program.Program(self.getRootCommand().config,
record=self.getRootCommand().record,
stdout=self.stdout)
self.runner = task.SyncRunner()
@@ -64,8 +73,6 @@ class _CD(logcommand.LogCommand):
self.program.loadDevice(self.device)
self.program.unmountDevice(self.device)
version = None
# first, read the normal TOC, which is fast
self.ittoc = self.program.getFastToc(self.runner,
self.options.toc_pickle,
@@ -80,7 +87,8 @@ class _CD(logcommand.LogCommand):
self.stdout.write("MusicBrainz lookup URL %s\n" %
self.ittoc.getMusicBrainzSubmitURL())
self.program.metadata = self.program.getMusicBrainz(self.ittoc, self.mbdiscid,
self.program.metadata = self.program.getMusicBrainz(self.ittoc,
self.mbdiscid,
release=self.options.release_id)
if not self.program.metadata:
@@ -90,8 +98,10 @@ class _CD(logcommand.LogCommand):
if cddbmd:
self.stdout.write('FreeDB identifies disc as %s\n' % cddbmd)
if not self.options.unknown:
self.program.ejectDevice(self.device)
# also used by rip cd info
if not getattr(self.options, 'unknown', False):
if self.eject:
self.program.ejectDevice(self.device)
return -1
# now, read the complete index table, which is slower
@@ -103,32 +113,39 @@ class _CD(logcommand.LogCommand):
assert self.itable.getCDDBDiscId() == self.ittoc.getCDDBDiscId(), \
"full table's id %s differs from toc id %s" % (
self.itable.getCDDBDiscId(), self.ittoc.getCDDBDiscId())
assert self.itable.getMusicBrainzDiscId() == self.ittoc.getMusicBrainzDiscId(), \
assert self.itable.getMusicBrainzDiscId() == \
self.ittoc.getMusicBrainzDiscId(), \
"full table's mb id %s differs from toc id mb %s" % (
self.itable.getMusicBrainzDiscId(), self.ittoc.getMusicBrainzDiscId())
assert self.itable.getAccurateRipURL() == self.ittoc.getAccurateRipURL(), \
self.itable.getMusicBrainzDiscId(),
self.ittoc.getMusicBrainzDiscId())
assert self.itable.getAccurateRipURL() == \
self.ittoc.getAccurateRipURL(), \
"full table's AR URL %s differs from toc AR URL %s" % (
self.itable.getAccurateRipURL(), self.ittoc.getAccurateRipURL())
# result
self.program.result.cdrdaoVersion = version
self.program.result.cdparanoiaVersion = cdparanoia.getCdParanoiaVersion()
self.program.result.cdrdaoVersion = cdrdao.getCDRDAOVersion()
self.program.result.cdparanoiaVersion = \
cdparanoia.getCdParanoiaVersion()
info = drive.getDeviceInfo(self.parentCommand.options.device)
if info:
try:
self.program.result.cdparanoiaDefeatsCache = self.getRootCommand(
).config.getDefeatsCache(*info)
self.program.result.cdparanoiaDefeatsCache = \
self.getRootCommand().config.getDefeatsCache(*info)
except KeyError, e:
self.debug('Got key error: %r' % (e, ))
self.program.result.artist = self.program.metadata and self.program.metadata.artist \
self.program.result.artist = self.program.metadata \
and self.program.metadata.artist \
or 'Unknown Artist'
self.program.result.title = self.program.metadata and self.program.metadata.title \
self.program.result.title = self.program.metadata \
and self.program.metadata.title \
or 'Unknown Title'
# cdio is optional for now
try:
import cdio
_, self.program.result.vendor, self.program.result.model, self.program.result.release = \
_, self.program.result.vendor, self.program.result.model, \
self.program.result.release = \
cdio.Device(self.device).get_hwinfo()
except ImportError:
self.stdout.write(
@@ -139,7 +156,8 @@ class _CD(logcommand.LogCommand):
self.doCommand()
self.program.ejectDevice(self.device)
if self.eject:
self.program.ejectDevice(self.device)
def doCommand(self):
pass
@@ -148,6 +166,8 @@ class _CD(logcommand.LogCommand):
class Info(_CD):
summary = "retrieve information about the currently inserted CD"
eject = False
class Rip(_CD):
summary = "rip CD"
@@ -225,7 +245,9 @@ Log files will log the path to tracks relative to this directory.
if options.offset is None:
options.offset = 0
self.stdout.write("Using fallback read offset %d\n" %
self.stdout.write("""WARNING: using default offset %d.
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()
@@ -260,16 +282,18 @@ Log files will log the path to tracks relative to this directory.
### write disc files
disambiguate = False
while True:
discName = self.program.getPath(self.program.outdir, self.options.disc_template,
self.mbdiscid, 0, profile=profile, disambiguate=disambiguate)
discName = self.program.getPath(self.program.outdir,
self.options.disc_template, self.mbdiscid, 0,
profile=profile, disambiguate=disambiguate)
dirname = os.path.dirname(discName)
if os.path.exists(dirname):
self.stdout.write("Output directory %s already exists\n" %
dirname)
dirname.encode('utf-8'))
logs = glob.glob(os.path.join(dirname, '*.log'))
if logs:
self.stdout.write("Output directory %s is a finished rip\n" %
dirname)
self.stdout.write(
"Output directory %s is a finished rip\n" %
dirname.encode('utf-8'))
if not disambiguate:
disambiguate = True
continue
@@ -278,7 +302,8 @@ Log files will log the path to tracks relative to this directory.
break
else:
self.stdout.write("Creating output directory %s\n" % dirname)
self.stdout.write("Creating output directory %s\n" %
dirname.encode('utf-8'))
os.makedirs(dirname)
break
@@ -299,8 +324,11 @@ Log files will log the path to tracks relative to this directory.
self.debug('ripIfNotRipped have trackresult, path %r' %
trackResult.filename)
path = self.program.getPath(self.program.outdir, self.options.track_template,
self.mbdiscid, number, profile=profile, disambiguate=disambiguate) + '.' + profile.extension
path = self.program.getPath(self.program.outdir,
self.options.track_template,
self.mbdiscid, number,
profile=profile, disambiguate=disambiguate) \
+ '.' + profile.extension
self.debug('ripIfNotRipped: path %r' % path)
trackResult.number = number
@@ -330,11 +358,14 @@ Log files will log the path to tracks relative to this directory.
# we reset durations for test and copy here
trackResult.testduration = 0.0
trackResult.copyduration = 0.0
self.stdout.write('Ripping track %d of %d: %s\n' % (
number, len(self.itable.tracks),
os.path.basename(path).encode('utf-8')))
extra = ""
while tries < MAX_TRIES:
tries += 1
if tries > 1:
extra = " (try %d)" % tries
self.stdout.write('Ripping track %d of %d%s: %s\n' % (
number, len(self.itable.tracks), extra,
os.path.basename(path).encode('utf-8')))
try:
self.debug('ripIfNotRipped: track %d, try %d',
number, tries)
@@ -343,8 +374,8 @@ Log files will log the path to tracks relative to this directory.
device=self.parentCommand.options.device,
profile=profile,
taglist=self.program.getTagList(number),
what='track %d of %d' % (
number, len(self.itable.tracks)))
what='track %d of %d%s' % (
number, len(self.itable.tracks), extra))
break
except Exception, e:
self.debug('Got exception %r on try %d',
@@ -406,8 +437,9 @@ Log files will log the path to tracks relative to this directory.
ripIfNotRipped(i + 1)
### write disc files
discName = self.program.getPath(self.program.outdir, self.options.disc_template,
self.mbdiscid, 0, profile=profile, disambiguate=disambiguate)
discName = self.program.getPath(self.program.outdir,
self.options.disc_template, self.mbdiscid, 0,
profile=profile, disambiguate=disambiguate)
dirname = os.path.dirname(discName)
if not os.path.exists(dirname):
os.makedirs(dirname)
@@ -437,8 +469,10 @@ Log files will log the path to tracks relative to this directory.
if not track.audio:
continue
path = self.program.getPath(self.program.outdir, self.options.track_template,
self.mbdiscid, i + 1, profile=profile, disambiguate=disambiguate) + '.' + profile.extension
path = self.program.getPath(self.program.outdir,
self.options.track_template, self.mbdiscid, i + 1,
profile=profile,
disambiguate=disambiguate) + '.' + profile.extension
writeFile(handle, path,
self.itable.getTrackLength(i + 1) / common.FRAMES_PER_SECOND)
@@ -449,7 +483,18 @@ Log files will log the path to tracks relative to this directory.
self.stdout.write("AccurateRip URL %s\n" % url)
accucache = accurip.AccuCache()
responses = accucache.retrieve(url)
try:
responses = accucache.retrieve(url)
except urllib2.URLError, e:
if isinstance(e.args[0], socket.gaierror):
if e.args[0].errno == -2:
self.stdout.write("Warning: network error: %r\n" % (
e.args[0], ))
responses = None
else:
raise
else:
raise
if not responses:
self.stdout.write('Album not found in AccurateRip database\n')
@@ -466,7 +511,8 @@ Log files will log the path to tracks relative to this directory.
self.program.verifyImage(self.runner, responses)
self.stdout.write("\n".join(self.program.getAccurateRipResults()) + "\n")
self.stdout.write("\n".join(
self.program.getAccurateRipResults()) + "\n")
self.program.saveRipResult()

View File

@@ -28,13 +28,13 @@ disc and track template are:
'''
def addTemplate(self):
def addTemplate(obj):
# FIXME: get from config
self.parser.add_option('', '--track-template',
obj.parser.add_option('', '--track-template',
action="store", dest="track_template",
help="template for track file naming (default %default)",
default=DEFAULT_TRACK_TEMPLATE)
self.parser.add_option('', '--disc-template',
obj.parser.add_option('', '--disc-template',
action="store", dest="disc_template",
help="template for disc file naming (default %default)",
default=DEFAULT_DISC_TEMPLATE)

View File

@@ -25,6 +25,23 @@ from morituri.result import result
from morituri.common import task, cache
class RCCue(logcommand.LogCommand):
name = "cue"
summary = "write a cue file for the cached result"
def do(self, args):
self._cache = cache.ResultCache()
persisted = self._cache.getRipResult(args[0], create=False)
if not persisted:
self.stderr.write(
'Could not find a result for cddb disc id %s\n' % args[0])
return 3
self.stdout.write(persisted.object.table.cue().encode('utf-8'))
class RCList(logcommand.LogCommand):
@@ -49,7 +66,7 @@ class RCList(logcommand.LogCommand):
self.stdout.write('%s: %s - %s\n' % (
cddbid, artist.encode('utf-8'), title.encode('utf-8')))
class RCLog(logcommand.LogCommand):
@@ -85,14 +102,14 @@ class RCLog(logcommand.LogCommand):
logger = klazz()
self.stdout.write(logger.log(persisted.object).encode('utf-8'))
class ResultCache(logcommand.LogCommand):
summary = "debug result cache"
aliases = ['rc', ]
subCommandClasses = [RCList, RCLog, ]
subCommandClasses = [RCCue, RCList, RCLog, ]
class Checksum(logcommand.LogCommand):
@@ -100,21 +117,22 @@ class Checksum(logcommand.LogCommand):
summary = "run a checksum task"
def do(self, args):
try:
fromPath = unicode(args[0])
except IndexError:
self.stdout.write('Please specify an input file.\n')
if not args:
self.stdout.write('Please specify one or more input files.\n')
return 3
runner = task.SyncRunner()
# here to avoid import gst eating our options
from morituri.common import checksum
checksumtask = checksum.CRC32Task(fromPath)
runner.run(checksumtask)
for arg in args:
fromPath = unicode(arg)
self.stdout.write('Checksum: %08x\n' % checksumtask.checksum)
checksumtask = checksum.CRC32Task(fromPath)
runner.run(checksumtask)
self.stdout.write('Checksum: %08x\n' % checksumtask.checksum)
class Encode(logcommand.LogCommand):
@@ -129,10 +147,13 @@ class Encode(logcommand.LogCommand):
self.parser.add_option('', '--profile',
action="store", dest="profile",
help="profile for encoding (default '%s', choices '%s')" % (
default, "', '".join(encode.PROFILES.keys())),
default, "', '".join(encode.ALL_PROFILES.keys())),
default=default)
def do(self, args):
from morituri.common import encode
profile = encode.ALL_PROFILES[self.options.profile]()
try:
fromPath = unicode(args[0])
except IndexError:
@@ -142,12 +163,10 @@ class Encode(logcommand.LogCommand):
try:
toPath = unicode(args[1])
except IndexError:
toPath = fromPath + '.' + self.options.profile
toPath = fromPath + '.' + profile.extension
runner = task.SyncRunner()
from morituri.common import encode
profile = encode.PROFILES[self.options.profile]()
self.debug('Encoding %s to %s',
fromPath.encode('utf-8'),
toPath.encode('utf-8'))
@@ -155,6 +174,35 @@ class Encode(logcommand.LogCommand):
runner.run(encodetask)
self.stdout.write('Peak level: %r\n' % encodetask.peak)
self.stdout.write('Encoded to %s\n' % toPath.encode('utf-8'))
class MaxSample(logcommand.LogCommand):
summary = "run a max sample task"
def do(self, args):
if not args:
self.stdout.write('Please specify one or more input files.\n')
return 3
runner = task.SyncRunner()
# here to avoid import gst eating our options
from morituri.common import checksum
for arg in args:
fromPath = unicode(arg.decode('utf-8'))
checksumtask = checksum.MaxSampleTask(fromPath)
runner.run(checksumtask)
self.stdout.write('%s\n' % arg)
self.stdout.write('Biggest absolute sample: %04x\n' %
checksumtask.checksum)
class Tag(logcommand.LogCommand):
summary = "run a tag reading task"
@@ -184,6 +232,8 @@ class MusicBrainzNGS(logcommand.LogCommand):
summary = "examine MusicBrainz NGS info"
description = """Look up a MusicBrainz disc id and output information.
You can get the MusicBrainz disc id with rip cd info.
Example disc id: KnpGsLhvH.lPrNc1PBL21lb9Bg4-"""
def do(self, args):
@@ -193,8 +243,9 @@ Example disc id: KnpGsLhvH.lPrNc1PBL21lb9Bg4-"""
self.stdout.write('Please specify a MusicBrainz disc id.\n')
return 3
from morituri.common import musicbrainzngs
metadatas = musicbrainzngs.musicbrainz(discId)
from morituri.common import mbngs
metadatas = mbngs.musicbrainz(discId,
record=self.getRootCommand().record)
self.stdout.write('%d releases\n' % len(metadatas))
for i, md in enumerate(metadatas):
@@ -215,8 +266,32 @@ Example disc id: KnpGsLhvH.lPrNc1PBL21lb9Bg4-"""
track.title.encode('utf-8')))
class CDParanoia(logcommand.LogCommand):
def do(self, args):
from morituri.program import cdparanoia
version = cdparanoia.getCdParanoiaVersion()
self.stdout.write("cdparanoia version: %s\n" % version)
class CDRDAO(logcommand.LogCommand):
def do(self, args):
from morituri.program import cdrdao
version = cdrdao.getCDRDAOVersion()
self.stdout.write("cdrdao version: %s\n" % version)
class Version(logcommand.LogCommand):
summary = "debug version getting"
subCommandClasses = [CDParanoia, CDRDAO]
class Debug(logcommand.LogCommand):
summary = "debug internals"
subCommandClasses = [Checksum, Encode, Tag, MusicBrainzNGS, ResultCache]
subCommandClasses = [Checksum, Encode, MaxSample, Tag, MusicBrainzNGS,
ResultCache, Version]

View File

@@ -22,7 +22,7 @@
import os
from morituri.common import logcommand, accurip, program, encode, renamer
from morituri.common import logcommand, accurip, program
from morituri.image import image
from morituri.result import result
@@ -51,7 +51,7 @@ class Encode(logcommand.LogCommand):
default=default)
def do(self, args):
prog = program.Program()
prog = program.Program(self.getRootCommand().config)
prog.outdir = (self.options.output_directory or os.getcwd())
prog.outdir = prog.outdir.decode('utf-8')
@@ -110,7 +110,10 @@ class Retag(logcommand.LogCommand):
def do(self, args):
prog = program.Program(stdout=self.stdout)
# here to avoid import gst eating our options
from morituri.common import encode
prog = program.Program(self.getRootCommand().config, stdout=self.stdout)
runner = task.SyncRunner()
for arg in args:
@@ -147,63 +150,18 @@ class Retag(logcommand.LogCommand):
print '%s already tagged correctly' % path
print
class Rename(logcommand.LogCommand):
summary = "rename image and all files based on metadata"
def addOptions(self):
self.parser.add_option('-R', '--release-id',
action="store", dest="release_id",
help="MusicBrainz release id to match to (if there are multiple)")
def do(self, args):
prog = program.Program(stdout=self.stdout)
runner = task.SyncRunner()
for arg in args:
self.stdout.write('Renaming image %r\n' % arg)
arg = arg.decode('utf-8')
cueImage = image.Image(arg)
cueImage.setup(runner)
mbdiscid = cueImage.table.getMusicBrainzDiscId()
operator = renamer.Operator(statePath, mbdiscid)
self.stdout.write('MusicBrainz disc id is %s\n' % mbdiscid)
prog.metadata = prog.getMusicBrainz(cueImage.table, mbdiscid,
release=self.options.release_id)
if not prog.metadata:
print 'Not in MusicBrainz database, skipping'
continue
# FIXME: this feels like we're poking at internals.
prog.cuePath = arg
prog.result = result.RipResult()
for track in cueImage.table.tracks:
path = cueImage.getRealPath(track.indexes[1].path)
taglist = prog.getTagList(track.number)
self.debug(
'possibly retagging %r from cue path %r with taglist %r',
path, arg, taglist)
t = encode.SafeRetagTask(path, taglist)
runner.run(t)
path = os.path.basename(path)
if t.changed:
print 'Retagged %s' % path
else:
print '%s already tagged correctly' % path
print
class Verify(logcommand.LogCommand):
usage = '[CUEFILE]...'
summary = "verify image"
description = '''
Verifies the image from the given .cue files against the AccurateRip database.
'''
def do(self, args):
prog = program.Program()
prog = program.Program(self.getRootCommand().config())
runner = task.SyncRunner()
cache = accurip.AccuCache()

View File

@@ -26,7 +26,8 @@ import tempfile
import gobject
gobject.threads_init()
from morituri.common import logcommand, accurip, drive, program
from morituri.common import logcommand, accurip, drive, program, common
from morituri.common import task as ctask
from morituri.program import cdrdao, cdparanoia
from morituri.extern.task import task
@@ -87,8 +88,8 @@ CD in the AccurateRip database."""
# this can be a symlink to another device
def do(self, args):
prog = program.Program()
runner = task.SyncRunner()
prog = program.Program(self.getRootCommand().config)
runner = ctask.SyncRunner()
device = self.options.device
@@ -150,11 +151,18 @@ CD in the AccurateRip database."""
try:
archecksum = self._arcs(runner, table, 1, offset)
except task.TaskException, e:
# let MissingDependency fall through
if isinstance(e.exception,
common.MissingDependencyException):
raise e
if isinstance(e.exception, cdparanoia.FileSizeError):
self.stdout.write(
'WARNING: cannot rip with offset %d...\n' % offset)
continue
self.warning("Unknown exception for offset %d: %r" % (
self.warning("Unknown task exception for offset %d: %r" % (
offset, e))
self.stdout.write(
'WARNING: cannot rip with offset %d...\n' % offset)
@@ -170,8 +178,9 @@ CD in the AccurateRip database."""
'Offset of device is likely %d, confirming ...\n' %
offset)
# now try and rip all other tracks as well
for track in range(2, len(table.tracks) + 1):
# now try and rip all other tracks as well, except for the
# last one (to avoid readers that can't do overread
for track in range(2, (len(table.tracks) + 1) - 1):
try:
archecksum = self._arcs(runner, table, track, offset)
except task.TaskException, e:
@@ -187,7 +196,7 @@ CD in the AccurateRip database."""
track, i))
count += 1
if count == len(table.tracks):
if count == len(table.tracks) - 1:
self._foundOffset(device, offset)
return 0
else:

View File

@@ -8,10 +8,12 @@ EXTRA_DIST = \
test_common_checksum.py \
test_common_common.py \
test_common_config.py \
test_common_directory.py \
test_common_drive.py \
test_common_encode.py \
test_common_gstreamer.py \
test_common_musicbrainzngs.py \
test_common_mbngs.py \
test_common_path.py \
test_common_program.py \
test_common_renamer.py \
test_image_cue.py \
@@ -36,6 +38,9 @@ EXTRA_DIST = \
release.93a6268c-ddf1-4898-bf93-fb862b1c5c5e.xml \
release.c7d919f4-3ea0-4c4b-a230-b3605f069440.xml \
morituri.release.3451f29c-9bb8-4cc5-bfcc-bd50104b94f8.json \
morituri.release.a76714e0-32b1-4ed4-b28e-f86d99642193.json \
morituri.release.61c6fd9b-18f8-4a45-963a-ba3c5d990cae.json \
morituri.release.e32ae79a-336e-4d33-945c-8c5e8206dbd3.json \
kanye.cue \
kings-separate.cue \
kings-single.cue \
@@ -44,8 +49,11 @@ EXTRA_DIST = \
track-single.cue \
cdparanoia.progress \
cdparanoia.progress.error \
cdparanoia.progress.strokes \
cdrdao.readtoc.progress \
silentalarm.result.pickle \
strokes-someday.toc \
surferrosa.toc \
totbl.fast.toc \
track.flac \
cache/result/fe105a11.pickle \

View File

@@ -2,37 +2,37 @@ REM DISCID AD0BE00D
REM COMMENT "Morituri"
FILE "data.wav" WAVE
TRACK 01 AUDIO
INDEX 00 00:00:00
INDEX 01 03:22:70
PREGAP 03:22:70
INDEX 01 00:00:00
TRACK 02 AUDIO
INDEX 01 07:44:69
INDEX 01 04:21:74
TRACK 03 AUDIO
INDEX 01 11:25:07
INDEX 01 08:02:12
TRACK 04 AUDIO
INDEX 01 15:20:40
INDEX 01 11:57:45
TRACK 05 AUDIO
INDEX 00 18:40:70
INDEX 01 18:41:67
INDEX 00 15:18:00
INDEX 01 15:18:72
TRACK 06 AUDIO
INDEX 00 21:28:35
INDEX 01 21:29:01
INDEX 00 18:05:40
INDEX 01 18:06:06
TRACK 07 AUDIO
INDEX 00 24:58:10
INDEX 01 24:58:27
INDEX 00 21:35:15
INDEX 01 21:35:32
TRACK 08 AUDIO
INDEX 00 29:23:69
INDEX 01 29:23:73
INDEX 00 26:00:74
INDEX 01 26:01:03
TRACK 09 AUDIO
INDEX 00 32:59:09
INDEX 01 32:59:20
INDEX 00 29:36:14
INDEX 01 29:36:25
TRACK 10 AUDIO
INDEX 01 37:18:72
INDEX 01 33:56:02
TRACK 11 AUDIO
INDEX 00 41:11:21
INDEX 01 41:11:64
INDEX 00 37:48:26
INDEX 01 37:48:69
TRACK 12 AUDIO
INDEX 00 45:07:40
INDEX 01 45:09:06
INDEX 00 41:44:45
INDEX 01 41:46:11
TRACK 13 AUDIO
INDEX 00 49:19:06
INDEX 01 49:19:28
INDEX 00 45:56:11
INDEX 01 45:56:33

View File

@@ -0,0 +1,111 @@
Sending all callbacks to stderr for wrapper script
cdparanoia III release 10.2 (September 11, 2008)
Ripping from sector 0 (track 0 [0:00.00])
to sector 0 (track 0 [0:00.00])
outputting to cdda.wav
##: 0 [read] @ 24696
##: 0 [read] @ 56448
##: 0 [read] @ 88200
##: 0 [read] @ 119952
##: 0 [read] @ 151704
##: 0 [read] @ 183456
##: 0 [read] @ 215208
##: 0 [read] @ 246960
##: 0 [read] @ 278712
##: 0 [read] @ 310464
##: 0 [read] @ 342216
##: 0 [read] @ 373968
##: 0 [read] @ 405720
##: 0 [read] @ 437472
##: 0 [read] @ 469224
##: 0 [read] @ 500976
##: 0 [read] @ 532728
##: 0 [read] @ 564480
##: 0 [read] @ 596232
##: 0 [read] @ 627984
##: 0 [read] @ 659736
##: 0 [read] @ 691488
##: 0 [read] @ 723240
##: 0 [read] @ 754992
##: 0 [read] @ 786744
##: 0 [read] @ 818496
##: 0 [read] @ 850248
##: 0 [read] @ 882000
##: 0 [read] @ 913752
##: 0 [read] @ 945504
##: 0 [read] @ 977256
##: 0 [read] @ 1009008
##: 0 [read] @ 1040760
##: 0 [read] @ 1072512
##: 0 [read] @ 1104264
##: 0 [read] @ 1136016
##: 0 [read] @ 1167768
##: 0 [read] @ 1199520
##: 0 [read] @ 1231272
##: 0 [read] @ 1263024
##: 0 [read] @ 1294776
##: 0 [read] @ 1326528
##: 0 [read] @ 1358280
##: 0 [read] @ 1390032
##: 0 [read] @ 1410024
##: 0 [read] @ 23520
##: 0 [read] @ 55272
##: 0 [read] @ 87024
##: 0 [read] @ 118776
##: 0 [read] @ 150528
##: 0 [read] @ 182280
##: 0 [read] @ 214032
##: 0 [read] @ 245784
##: 0 [read] @ 277536
##: 0 [read] @ 309288
##: 0 [read] @ 341040
##: 0 [read] @ 372792
##: 0 [read] @ 404544
##: 0 [read] @ 436296
##: 0 [read] @ 468048
##: 0 [read] @ 499800
##: 0 [read] @ 531552
##: 0 [read] @ 563304
##: 0 [read] @ 595056
##: 0 [read] @ 626808
##: 0 [read] @ 658560
##: 0 [read] @ 690312
##: 0 [read] @ 722064
##: 0 [read] @ 753816
##: 0 [read] @ 785568
##: 0 [read] @ 817320
##: 0 [read] @ 849072
##: 0 [read] @ 880824
##: 0 [read] @ 912576
##: 0 [read] @ 944328
##: 0 [read] @ 976080
##: 0 [read] @ 1007832
##: 0 [read] @ 1039584
##: 0 [read] @ 1071336
##: 0 [read] @ 1103088
##: 0 [read] @ 1134840
##: 0 [read] @ 1166592
##: 0 [read] @ 1198344
##: 0 [read] @ 1230096
##: 0 [read] @ 1261848
##: 0 [read] @ 1293600
##: 0 [read] @ 1325352
##: 0 [read] @ 1357104
##: 0 [read] @ 1388856
##: 0 [read] @ 1410024
##: 1 [verify] @ 0
##: 3 [correction] @ 1005459
##: 3 [correction] @ 1005480
##: 1 [verify] @ 1005480
##: 1 [verify] @ 1005480
##: -2 [wrote] @ 1175
##: -2 [wrote] @ 1176
##: -1 [finished] @ 1175
Done.

View File

@@ -26,11 +26,14 @@ def _diff(old, new, desc):
raise AssertionError(
("\nError while comparing strings:\n"
"%s") % (output, ))
"%s") % (output.encode('utf-8'), ))
def diffStrings(orig, new, desc='input'):
assert type(orig) == type(new), 'type %s and %s are different' % (
type(orig), type(new))
def _tolines(s):
return [line + '\n' for line in s.split('\n')]

View File

@@ -0,0 +1 @@
{"release": {"status": "Official", "asin": "B008R78K1Y", "label-info-list": [{"label": {"sort-name": "Brownswood Recordings", "id": "6483a614-d00f-42b0-af39-a602b3ce5daa", "name": "Brownswood Recordings"}, "catalog-number": "BWOOD090CD"}], "title": "Mala in Cuba", "country": "GB", "barcode": "5060180321505", "artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "medium-list": [{"disc-list": [{"id": "u0aKVpO.59JBy6eQRX2vYcoqQZ0-", "sectors": "257868"}], "position": "1", "track-list": [{"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "155000", "artist-credit-phrase": "Mala", "id": "3fa9c442-6ae7-4242-ae3b-0150a3002da4", "title": "Introduction"}, "position": "1"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "195626", "artist-credit-phrase": "Mala", "id": "983ad5e0-c52e-459d-8828-85718ceff2cc", "title": "Mulata"}, "position": "2"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "242826", "artist-credit-phrase": "Mala", "id": "6855abf0-32a3-4fe2-a3fb-858f3157d42b", "title": "Tribal"}, "position": "3"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "263760", "artist-credit-phrase": "Mala", "id": "2f938885-94ad-4b11-b251-f18c3a2a5fa9", "title": "Changuito"}, "position": "4"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "274520", "artist-credit-phrase": "Mala", "id": "a5ecfa15-06d0-44cf-a28e-c748e8270488", "title": "Revolution"}, "position": "5"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}, " feat. ", {"artist": {"sort-name": "Dreiser", "id": "ec07a209-55ff-4084-bc41-9d4d1764e075", "name": "Dreiser"}}, " & ", {"artist": {"sort-name": "Sexto Sentido", "id": "f626b92e-07b1-4a19-ad13-c09d690db66c", "name": "Sexto Sentido"}}], "length": "227800", "artist-credit-phrase": "Mala feat. Dreiser & Sexto Sentido", "id": "cfb3ddaf-584c-4c86-b58c-752c63977bb8", "title": "Como como"}, "position": "6"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "276693", "artist-credit-phrase": "Mala", "id": "90da8ada-21e2-4e7b-ab46-ff04004a3d84", "title": "Cuba Electronic"}, "position": "7"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "267973", "artist-credit-phrase": "Mala", "id": "2bf67b46-30f5-4746-ab91-4c9675221a21", "title": "The Tunnel"}, "position": "8"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "246000", "artist-credit-phrase": "Mala", "id": "0cd61fa9-a97a-41e3-b3c3-db36f633b611", "title": "Ghost"}, "position": "9"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "250000", "artist-credit-phrase": "Mala", "id": "136989e9-f24f-4872-9026-1487869cc8de", "title": "Curfew"}, "position": "10"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "174000", "artist-credit-phrase": "Mala", "id": "26b6fd89-7021-4239-b6a7-76eca8c0515a", "title": "The Tourist"}, "position": "11"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "270733", "artist-credit-phrase": "Mala", "id": "62f7a892-f63b-4a2b-866f-db2a36533f8c", "title": "Change"}, "position": "12"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "251853", "artist-credit-phrase": "Mala", "id": "4395c91a-d5e9-4fe4-92d2-deee3e0ebb5a", "title": "Calle F"}, "position": "13"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}, " feat. ", {"artist": {"sort-name": "Suarez, Danay", "id": "82f04998-7da8-4259-aa7f-d623e6ea2b91", "name": "Danay Suarez"}}], "length": "338000", "artist-credit-phrase": "Mala feat. Danay Suarez", "id": "e47a4fd9-8359-4a33-add8-e8c690e59055", "title": "Noche sue\u00f1os"}, "position": "14"}], "format": "CD"}], "text-representation": {"language": "eng", "script": "Latn"}, "date": "2012-09-17", "artist-credit-phrase": "Mala", "quality": "normal", "id": "61c6fd9b-18f8-4a45-963a-ba3c5d990cae"}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,13 @@
REM GENRE "Alternative Rock"
REM DATE 2001
REM DISCID 0200BA01
REM COMMENT "ExactAudioCopy v0.99pb4"
PERFORMER "The Strokes"
TITLE "Someday"
FILE "The Strokes - Someday\01 - The Strokes - Someday.wav" WAVE
TRACK 01 AUDIO
TITLE "Someday"
PERFORMER "The Strokes"
FLAGS DCP
PREGAP 00:00:01
INDEX 01 00:00:00

View File

@@ -0,0 +1,12 @@
CD_DA
// Track 1
TRACK AUDIO
COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
SILENCE 00:00:01
FILE "data.wav" 0 03:06:59
START 00:00:01

View File

@@ -0,0 +1,136 @@
REM GENRE Alternative
REM DATE 1987
REM DISCID 350CAA15
REM COMMENT "ExactAudioCopy v0.99pb4"
CATALOG 0000000000000
PERFORMER "Pixies"
TITLE "Surfer Rosa & Come on Pilgrim"
FILE "Pixies - Surfer Rosa & Come on Pilgrim\01 - Pixies - Bone Machine.wav" WAVE
TRACK 01 AUDIO
TITLE "Bone Machine"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 00 00:00:00
INDEX 01 00:00:32
FILE "Pixies - Surfer Rosa & Come on Pilgrim\02 - Pixies - Break My Body.wav" WAVE
TRACK 02 AUDIO
TITLE "Break My Body"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\03 - Pixies - Something Against You.wav" WAVE
TRACK 03 AUDIO
TITLE "Something Against You"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 00 00:00:00
INDEX 01 00:00:45
FILE "Pixies - Surfer Rosa & Come on Pilgrim\04 - Pixies - Broken Face.wav" WAVE
TRACK 04 AUDIO
TITLE "Broken Face"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\05 - Pixies - Gigantic.wav" WAVE
TRACK 05 AUDIO
TITLE "Gigantic"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\06 - Pixies - River Euphrates.wav" WAVE
TRACK 06 AUDIO
TITLE "River Euphrates"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\07 - Pixies - Where Is My Mind .wav" WAVE
TRACK 07 AUDIO
TITLE "Where Is My Mind?"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\08 - Pixies - Cactus.wav" WAVE
TRACK 08 AUDIO
TITLE "Cactus"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\09 - Pixies - Tony's Theme.wav" WAVE
TRACK 09 AUDIO
TITLE "Tony's Theme"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\10 - Pixies - Oh My Golly!.wav" WAVE
TRACK 10 AUDIO
TITLE "Oh My Golly!"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\11 - Pixies - Vamos.wav" WAVE
TRACK 11 AUDIO
TITLE "Vamos"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
INDEX 02 00:44:70
FILE "Pixies - Surfer Rosa & Come on Pilgrim\12 - Pixies - I'm Amazed.wav" WAVE
TRACK 12 AUDIO
TITLE "I'm Amazed"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\13 - Pixies - Brick is Red.wav" WAVE
TRACK 13 AUDIO
TITLE "Brick is Red"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\14 - Pixies - Caribou.wav" WAVE
TRACK 14 AUDIO
TITLE "Caribou"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\15 - Pixies - Vamos.wav" WAVE
TRACK 15 AUDIO
TITLE "Vamos"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\16 - Pixies - Isla de Encanta.wav" WAVE
TRACK 16 AUDIO
TITLE "Isla de Encanta"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\17 - Pixies - Ed is Dead.wav" WAVE
TRACK 17 AUDIO
TITLE "Ed is Dead"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\18 - Pixies - The Holyday Song.wav" WAVE
TRACK 18 AUDIO
TITLE "The Holyday Song"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\19 - Pixies - Nimrod's Son.wav" WAVE
TRACK 19 AUDIO
TITLE "Nimrod's Son"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\20 - Pixies - I've Been Tired.wav" WAVE
TRACK 20 AUDIO
TITLE "I've Been Tired"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\21 - Pixies - Levitate Me.wav" WAVE
TRACK 21 AUDIO
TITLE "Levitate Me"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00

View File

@@ -0,0 +1,136 @@
REM GENRE Alternative
REM DATE 1987
REM DISCID 350CAA15
REM COMMENT "ExactAudioCopy v0.99pb4"
CATALOG 0000000000000
PERFORMER "Pixies"
TITLE "Surfer Rosa & Come on Pilgrim"
FILE "Pixies - Surfer Rosa & Come on Pilgrim\01 - Pixies - Bone Machine.wav" WAVE
TRACK 01 AUDIO
TITLE "Bone Machine"
PERFORMER "Pixies"
ISRC 000000000000
PREGAP 00:00:32
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\02 - Pixies - Break My Body.wav" WAVE
TRACK 02 AUDIO
TITLE "Break My Body"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
TRACK 03 AUDIO
TITLE "Something Against You"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 00 02:05:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\03 - Pixies - Something Against You.wav" WAVE
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\04 - Pixies - Broken Face.wav" WAVE
TRACK 04 AUDIO
TITLE "Broken Face"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\05 - Pixies - Gigantic.wav" WAVE
TRACK 05 AUDIO
TITLE "Gigantic"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\06 - Pixies - River Euphrates.wav" WAVE
TRACK 06 AUDIO
TITLE "River Euphrates"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\07 - Pixies - Where Is My Mind .wav" WAVE
TRACK 07 AUDIO
TITLE "Where Is My Mind?"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\08 - Pixies - Cactus.wav" WAVE
TRACK 08 AUDIO
TITLE "Cactus"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\09 - Pixies - Tony's Theme.wav" WAVE
TRACK 09 AUDIO
TITLE "Tony's Theme"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\10 - Pixies - Oh My Golly!.wav" WAVE
TRACK 10 AUDIO
TITLE "Oh My Golly!"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\11 - Pixies - Vamos.wav" WAVE
TRACK 11 AUDIO
TITLE "Vamos"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
INDEX 02 00:44:70
FILE "Pixies - Surfer Rosa & Come on Pilgrim\12 - Pixies - I'm Amazed.wav" WAVE
TRACK 12 AUDIO
TITLE "I'm Amazed"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\13 - Pixies - Brick is Red.wav" WAVE
TRACK 13 AUDIO
TITLE "Brick is Red"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\14 - Pixies - Caribou.wav" WAVE
TRACK 14 AUDIO
TITLE "Caribou"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\15 - Pixies - Vamos.wav" WAVE
TRACK 15 AUDIO
TITLE "Vamos"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\16 - Pixies - Isla de Encanta.wav" WAVE
TRACK 16 AUDIO
TITLE "Isla de Encanta"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\17 - Pixies - Ed is Dead.wav" WAVE
TRACK 17 AUDIO
TITLE "Ed is Dead"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\18 - Pixies - The Holyday Song.wav" WAVE
TRACK 18 AUDIO
TITLE "The Holyday Song"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\19 - Pixies - Nimrod's Son.wav" WAVE
TRACK 19 AUDIO
TITLE "Nimrod's Son"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\20 - Pixies - I've Been Tired.wav" WAVE
TRACK 20 AUDIO
TITLE "I've Been Tired"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\21 - Pixies - Levitate Me.wav" WAVE
TRACK 21 AUDIO
TITLE "Levitate Me"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00

View File

@@ -0,0 +1,136 @@
REM GENRE Alternative
REM DATE 1987
REM DISCID 350CAA15
REM COMMENT "ExactAudioCopy v0.99pb4"
CATALOG 0000000000000
PERFORMER "Pixies"
TITLE "Surfer Rosa & Come on Pilgrim"
FILE "Pixies - Surfer Rosa & Come on Pilgrim\01 - Pixies - Bone Machine.wav" WAVE
TRACK 01 AUDIO
TITLE "Bone Machine"
PERFORMER "Pixies"
ISRC 000000000000
PREGAP 00:00:32
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\02 - Pixies - Break My Body.wav" WAVE
TRACK 02 AUDIO
TITLE "Break My Body"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\03 - Pixies - Something Against You.wav" WAVE
TRACK 03 AUDIO
TITLE "Something Against You"
PERFORMER "Pixies"
ISRC 000000000000
PREGAP 00:00:45
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\04 - Pixies - Broken Face.wav" WAVE
TRACK 04 AUDIO
TITLE "Broken Face"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\05 - Pixies - Gigantic.wav" WAVE
TRACK 05 AUDIO
TITLE "Gigantic"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\06 - Pixies - River Euphrates.wav" WAVE
TRACK 06 AUDIO
TITLE "River Euphrates"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\07 - Pixies - Where Is My Mind .wav" WAVE
TRACK 07 AUDIO
TITLE "Where Is My Mind?"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\08 - Pixies - Cactus.wav" WAVE
TRACK 08 AUDIO
TITLE "Cactus"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\09 - Pixies - Tony's Theme.wav" WAVE
TRACK 09 AUDIO
TITLE "Tony's Theme"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\10 - Pixies - Oh My Golly!.wav" WAVE
TRACK 10 AUDIO
TITLE "Oh My Golly!"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\11 - Pixies - Vamos.wav" WAVE
TRACK 11 AUDIO
TITLE "Vamos"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
INDEX 02 00:44:70
FILE "Pixies - Surfer Rosa & Come on Pilgrim\12 - Pixies - I'm Amazed.wav" WAVE
TRACK 12 AUDIO
TITLE "I'm Amazed"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\13 - Pixies - Brick is Red.wav" WAVE
TRACK 13 AUDIO
TITLE "Brick is Red"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\14 - Pixies - Caribou.wav" WAVE
TRACK 14 AUDIO
TITLE "Caribou"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\15 - Pixies - Vamos.wav" WAVE
TRACK 15 AUDIO
TITLE "Vamos"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\16 - Pixies - Isla de Encanta.wav" WAVE
TRACK 16 AUDIO
TITLE "Isla de Encanta"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\17 - Pixies - Ed is Dead.wav" WAVE
TRACK 17 AUDIO
TITLE "Ed is Dead"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\18 - Pixies - The Holyday Song.wav" WAVE
TRACK 18 AUDIO
TITLE "The Holyday Song"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\19 - Pixies - Nimrod's Son.wav" WAVE
TRACK 19 AUDIO
TITLE "Nimrod's Son"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\20 - Pixies - I've Been Tired.wav" WAVE
TRACK 20 AUDIO
TITLE "I've Been Tired"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\21 - Pixies - Levitate Me.wav" WAVE
TRACK 21 AUDIO
TITLE "Levitate Me"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00

View File

@@ -0,0 +1,136 @@
REM GENRE Alternative
REM DATE 1987
REM DISCID 350CAA15
REM COMMENT "ExactAudioCopy v0.99pb4"
CATALOG 0000000000000
PERFORMER "Pixies"
TITLE "Surfer Rosa & Come on Pilgrim"
FILE "Pixies - Surfer Rosa & Come on Pilgrim\01 - Pixies - Bone Machine.wav" WAVE
TRACK 01 AUDIO
TITLE "Bone Machine"
PERFORMER "Pixies"
ISRC 000000000000
PREGAP 00:00:32
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\02 - Pixies - Break My Body.wav" WAVE
TRACK 02 AUDIO
TITLE "Break My Body"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
TRACK 03 AUDIO
TITLE "Something Against You"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 00 02:05:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\03 - Pixies - Something Against You.wav" WAVE
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\04 - Pixies - Broken Face.wav" WAVE
TRACK 04 AUDIO
TITLE "Broken Face"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\05 - Pixies - Gigantic.wav" WAVE
TRACK 05 AUDIO
TITLE "Gigantic"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\06 - Pixies - River Euphrates.wav" WAVE
TRACK 06 AUDIO
TITLE "River Euphrates"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\07 - Pixies - Where Is My Mind .wav" WAVE
TRACK 07 AUDIO
TITLE "Where Is My Mind?"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\08 - Pixies - Cactus.wav" WAVE
TRACK 08 AUDIO
TITLE "Cactus"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\09 - Pixies - Tony's Theme.wav" WAVE
TRACK 09 AUDIO
TITLE "Tony's Theme"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\10 - Pixies - Oh My Golly!.wav" WAVE
TRACK 10 AUDIO
TITLE "Oh My Golly!"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\11 - Pixies - Vamos.wav" WAVE
TRACK 11 AUDIO
TITLE "Vamos"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
INDEX 02 00:44:70
FILE "Pixies - Surfer Rosa & Come on Pilgrim\12 - Pixies - I'm Amazed.wav" WAVE
TRACK 12 AUDIO
TITLE "I'm Amazed"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\13 - Pixies - Brick is Red.wav" WAVE
TRACK 13 AUDIO
TITLE "Brick is Red"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\14 - Pixies - Caribou.wav" WAVE
TRACK 14 AUDIO
TITLE "Caribou"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\15 - Pixies - Vamos.wav" WAVE
TRACK 15 AUDIO
TITLE "Vamos"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\16 - Pixies - Isla de Encanta.wav" WAVE
TRACK 16 AUDIO
TITLE "Isla de Encanta"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\17 - Pixies - Ed is Dead.wav" WAVE
TRACK 17 AUDIO
TITLE "Ed is Dead"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\18 - Pixies - The Holyday Song.wav" WAVE
TRACK 18 AUDIO
TITLE "The Holyday Song"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\19 - Pixies - Nimrod's Son.wav" WAVE
TRACK 19 AUDIO
TITLE "Nimrod's Son"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\20 - Pixies - I've Been Tired.wav" WAVE
TRACK 20 AUDIO
TITLE "I've Been Tired"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00
FILE "Pixies - Surfer Rosa & Come on Pilgrim\21 - Pixies - Levitate Me.wav" WAVE
TRACK 21 AUDIO
TITLE "Levitate Me"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 00:00:00

View File

@@ -0,0 +1,116 @@
REM GENRE Alternative
REM DATE 1987
REM DISCID 350CAA15
REM COMMENT "ExactAudioCopy v0.99pb4"
CATALOG 0000000000000
PERFORMER "Pixies"
TITLE "Surfer Rosa & Come on Pilgrim"
FILE "Range.wav" WAVE
TRACK 01 AUDIO
TITLE "Bone Machine"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 00 00:00:00
INDEX 01 00:00:32
TRACK 02 AUDIO
TITLE "Break My Body"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 03:03:42
TRACK 03 AUDIO
TITLE "Something Against You"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 00 05:08:42
INDEX 01 05:09:12
TRACK 04 AUDIO
TITLE "Broken Face"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 06:56:67
TRACK 05 AUDIO
TITLE "Gigantic"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 08:27:00
TRACK 06 AUDIO
TITLE "River Euphrates"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 12:21:70
TRACK 07 AUDIO
TITLE "Where Is My Mind?"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 14:53:60
TRACK 08 AUDIO
TITLE "Cactus"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 18:47:15
TRACK 09 AUDIO
TITLE "Tony's Theme"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 21:03:70
TRACK 10 AUDIO
TITLE "Oh My Golly!"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 22:56:15
TRACK 11 AUDIO
TITLE "Vamos"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 24:43:32
INDEX 02 25:28:27
TRACK 12 AUDIO
TITLE "I'm Amazed"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 29:49:20
TRACK 13 AUDIO
TITLE "Brick is Red"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 31:31:27
TRACK 14 AUDIO
TITLE "Caribou"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 33:32:20
TRACK 15 AUDIO
TITLE "Vamos"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 36:46:45
TRACK 16 AUDIO
TITLE "Isla de Encanta"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 39:40:22
TRACK 17 AUDIO
TITLE "Ed is Dead"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 41:21:47
TRACK 18 AUDIO
TITLE "The Holyday Song"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 43:51:47
TRACK 19 AUDIO
TITLE "Nimrod's Son"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 46:06:10
TRACK 20 AUDIO
TITLE "I've Been Tired"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 48:23:25
TRACK 21 AUDIO
TITLE "Levitate Me"
PERFORMER "Pixies"
ISRC 000000000000
INDEX 01 51:24:07

View File

@@ -0,0 +1,196 @@
CD_DA
CATALOG "0000000000000"
// Track 1
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
ISRC "000000000000"
SILENCE 00:00:32
FILE "data.wav" 0 03:03:10
START 00:00:32
// Track 2
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
ISRC "000000000000"
FILE "data.wav" 03:03:10 02:05:00
// Track 3
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
ISRC "000000000000"
FILE "data.wav" 05:08:10 01:48:25
START 00:00:45
// Track 4
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
ISRC "000000000000"
FILE "data.wav" 06:56:35 01:30:08
// Track 5
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
ISRC "000000000000"
FILE "data.wav" 08:26:43 03:54:70
// Track 6
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
ISRC "000000000000"
FILE "data.wav" 12:21:38 02:31:65
// Track 7
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
ISRC "000000000000"
FILE "data.wav" 14:53:28 03:53:30
// Track 8
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
ISRC "000000000000"
FILE "data.wav" 18:46:58 02:16:55
// Track 9
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
ISRC "000000000000"
FILE "data.wav" 21:03:38 01:52:20
// Track 10
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
ISRC "000000000000"
FILE "data.wav" 22:55:58 01:47:17
// Track 11
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
ISRC "000000000000"
FILE "data.wav" 24:43:00 05:05:63
INDEX 00:44:70
// Track 12
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
ISRC "000000000000"
FILE "data.wav" 29:48:63 01:42:07
// Track 13
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
ISRC "000000000000"
FILE "data.wav" 31:30:70 02:00:68
// Track 14
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
ISRC "000000000000"
FILE "data.wav" 33:31:63 03:14:25
// Track 15
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
ISRC "000000000000"
FILE "data.wav" 36:46:13 02:53:52
// Track 16
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
ISRC "000000000000"
FILE "data.wav" 39:39:65 01:41:25
// Track 17
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
ISRC "000000000000"
FILE "data.wav" 41:21:15 02:30:00
// Track 18
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
ISRC "000000000000"
FILE "data.wav" 43:51:15 02:14:38
// Track 19
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
ISRC "000000000000"
FILE "data.wav" 46:05:53 02:17:15
// Track 20
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
ISRC "000000000000"
FILE "data.wav" 48:22:68 03:00:57
// Track 21
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
ISRC "000000000000"
FILE "data.wav" 51:23:50 02:38:38

View File

@@ -55,5 +55,13 @@ class GetRealPathTestCase(tcommon.TestCase):
fd, path = tempfile.mkstemp(suffix=u'back\\slash.flac')
refPath = os.path.join(os.path.dirname(path), 'fake.cue')
self.assertEquals(common.getRealPath(refPath, path),
path)
# same path, but with wav extension, will point to flac file
wavPath = path[:-4] + 'wav'
self.assertEquals(common.getRealPath(refPath, wavPath),
path)
os.close(fd)
os.unlink(path)

View File

@@ -0,0 +1,21 @@
# -*- Mode: Python; test-case-name: morituri.test.test_common_directory -*-
# vi:si:et:sw=4:sts=4:ts=4
from morituri.common import directory
from morituri.test import common
class DirectoryTestCase(common.TestCase):
def testAll(self):
d = directory.Directory()
path = d.getConfig()
self.failUnless(path.startswith('/home'))
path = d.getCache()
self.failUnless(path.startswith('/home'))
paths = d.getReadCaches()
self.failUnless(paths[0].startswith('/home'))

View File

@@ -0,0 +1,118 @@
# -*- Mode: Python; test-case-name: morituri.test.test_common_mbngs -*-
# vi:si:et:sw=4:sts=4:ts=4
import os
import json
import unittest
from morituri.common import mbngs
class MetadataTestCase(unittest.TestCase):
# Generated with rip -R cd info
def testJeffEverybodySingle(self):
path = os.path.join(os.path.dirname(__file__),
'morituri.release.3451f29c-9bb8-4cc5-bfcc-bd50104b94f8.json')
handle = open(path, "rb")
response = json.loads(handle.read())
handle.close()
discid = "wbjbST2jUHRZaB1inCyxxsL7Eqc-"
metadata = mbngs._getMetadata({}, response['release'], discid)
self.failIf(metadata.release)
def test2MeterSessies10(self):
# various artists, multiple artists per track
path = os.path.join(os.path.dirname(__file__),
'morituri.release.a76714e0-32b1-4ed4-b28e-f86d99642193.json')
handle = open(path, "rb")
response = json.loads(handle.read())
handle.close()
discid = "f7XO36a7n1LCCskkCiulReWbwZA-"
metadata = mbngs._getMetadata({}, response['release'], discid)
self.assertEquals(metadata.artist, u'Various Artists')
self.assertEquals(metadata.release, u'2001-10-15')
self.assertEquals(metadata.mbidArtist,
u'89ad4ac3-39f7-470e-963a-56509c546377')
self.assertEquals(len(metadata.tracks), 18)
track16 = metadata.tracks[15]
self.assertEquals(track16.artist, 'Tom Jones & Stereophonics')
self.assertEquals(track16.mbidArtist,
u'57c6f649-6cde-48a7-8114-2a200247601a'
';0bfba3d3-6a04-4779-bb0a-df07df5b0558'
)
self.assertEquals(track16.sortName,
u'Jones, Tom & Stereophonics')
def testBalladOfTheBrokenSeas(self):
# various artists disc
path = os.path.join(os.path.dirname(__file__),
'morituri.release.e32ae79a-336e-4d33-945c-8c5e8206dbd3.json')
handle = open(path, "rb")
response = json.loads(handle.read())
handle.close()
discid = "xAq8L4ELMW14.6wI6tt7QAcxiDI-"
metadata = mbngs._getMetadata({}, response['release'], discid)
self.assertEquals(metadata.artist, u'Isobel Campbell & Mark Lanegan')
self.assertEquals(metadata.sortName,
u'Campbell, Isobel & Lanegan, Mark')
self.assertEquals(metadata.release, u'2006-01-30')
self.assertEquals(metadata.mbidArtist,
u'd51f3a15-12a2-41a0-acfa-33b5eae71164;'
'a9126556-f555-4920-9617-6e013f8228a7')
self.assertEquals(len(metadata.tracks), 12)
track12 = metadata.tracks[11]
self.assertEquals(track12.artist, u'Isobel Campbell & Mark Lanegan')
self.assertEquals(track12.sortName,
u'Campbell, Isobel'
' & Lanegan, Mark'
)
self.assertEquals(track12.mbidArtist,
u'd51f3a15-12a2-41a0-acfa-33b5eae71164;'
'a9126556-f555-4920-9617-6e013f8228a7')
def testMalaInCuba(self):
# single artist disc, but with multiple artists tracks
# see https://github.com/thomasvs/morituri/issues/19
path = os.path.join(os.path.dirname(__file__),
'morituri.release.61c6fd9b-18f8-4a45-963a-ba3c5d990cae.json')
handle = open(path, "rb")
response = json.loads(handle.read())
handle.close()
discid = "u0aKVpO.59JBy6eQRX2vYcoqQZ0-"
metadata = mbngs._getMetadata({}, response['release'], discid)
self.assertEquals(metadata.artist, u'Mala')
self.assertEquals(metadata.sortName, u'Mala')
self.assertEquals(metadata.release, u'2012-09-17')
self.assertEquals(metadata.mbidArtist,
u'09f221eb-c97e-4da5-ac22-d7ab7c555bbb')
self.assertEquals(len(metadata.tracks), 14)
track6 = metadata.tracks[5]
self.assertEquals(track6.artist, u'Mala feat. Dreiser & Sexto Sentido')
self.assertEquals(track6.sortName,
u'Mala feat. Dreiser & Sexto Sentido')
self.assertEquals(track6.mbidArtist,
u'09f221eb-c97e-4da5-ac22-d7ab7c555bbb'
';ec07a209-55ff-4084-bc41-9d4d1764e075'
';f626b92e-07b1-4a19-ad13-c09d690db66c'
)

View File

@@ -1,24 +0,0 @@
# -*- Mode: Python; test-case-name: morituri.test.test_common_musicbrainzngs -*-
# vi:si:et:sw=4:sts=4:ts=4
import os
import json
import unittest
from morituri.common import musicbrainzngs
class MetadataTestCase(unittest.TestCase):
def testJeffEverybodySingle(self):
path = os.path.join(os.path.dirname(__file__),
'morituri.release.3451f29c-9bb8-4cc5-bfcc-bd50104b94f8.json')
handle = open(path, "rb")
response = json.loads(handle.read())
handle.close()
discid = "wbjbST2jUHRZaB1inCyxxsL7Eqc-"
metadata = musicbrainzngs._getMetadata({}, response['release'], discid)
self.failIf(metadata.release)

View File

@@ -0,0 +1,30 @@
# -*- Mode: Python; test-case-name: morituri.test.test_common_path -*-
# vi:si:et:sw=4:sts=4:ts=4
from morituri.common import path
from morituri.test import common
class FilterTestCase(common.TestCase):
def setUp(self):
self._filter = path.PathFilter(special=True)
def testSlash(self):
part = u'A Charm/A Blade'
self.assertEquals(self._filter.filter(part), u'A Charm-A Blade')
def testFat(self):
part = u'A Word: F**k you?'
self.assertEquals(self._filter.filter(part), u'A Word - F__k you_')
def testSpecial(self):
part = u'<<< $&*!\' "()`{}[]spaceship>>>'
self.assertEquals(self._filter.filter(part),
u'___ _____ ________spaceship___')
def testGreatest(self):
part = u'Greatest Ever! Soul: The Definitive Collection'
self.assertEquals(self._filter.filter(part),
u'Greatest Ever_ Soul - The Definitive Collection')

View File

@@ -1,13 +1,14 @@
# -*- Mode: Python; test-case-name: morituri.test.test_common_program -*-
# vi:si:et:sw=4:sts=4:ts=4
import os
import pickle
import unittest
from morituri.result import result
from morituri.common import program, accurip, musicbrainzngs
from morituri.common import program, accurip, mbngs, config
from morituri.rip import common as rcommon
@@ -28,7 +29,7 @@ class TrackImageVerifyTestCase(unittest.TestCase):
1842579359, 2850056507, 1329730252, 2526965856, 2525886806, 209743350,
3184062337, 2099956663, 2943874164, 2321637196]
prog = program.Program()
prog = program.Program(config.Config())
prog.result = result.RipResult()
# fill it with empty trackresults
for i, c in enumerate(checksums):
@@ -76,7 +77,7 @@ class HTOATestCase(unittest.TestCase):
self._tracks = pickle.load(open(path, 'rb'))
def testGetAccurateRipResults(self):
prog = program.Program()
prog = program.Program(config.Config())
prog.result = result.RipResult()
prog.result.tracks = self._tracks
@@ -86,7 +87,7 @@ class HTOATestCase(unittest.TestCase):
class PathTestCase(unittest.TestCase):
def testStandardTemplateEmpty(self):
prog = program.Program()
prog = program.Program(config.Config())
path = prog.getPath(u'/tmp', rcommon.DEFAULT_DISC_TEMPLATE,
'mbdiscid', 0)
@@ -94,8 +95,8 @@ class PathTestCase(unittest.TestCase):
u'/tmp/unknown/Unknown Artist - mbdiscid/Unknown Artist - mbdiscid')
def testStandardTemplateFilled(self):
prog = program.Program()
md = musicbrainzngs.DiscMetadata()
prog = program.Program(config.Config())
md = mbngs.DiscMetadata()
md.artist = md.sortName = 'Jeff Buckley'
md.title = 'Grace'
prog.metadata = md
@@ -106,8 +107,8 @@ class PathTestCase(unittest.TestCase):
u'/tmp/unknown/Jeff Buckley - Grace/Jeff Buckley - Grace')
def testIssue66TemplateFilled(self):
prog = program.Program()
md = musicbrainzngs.DiscMetadata()
prog = program.Program(config.Config())
md = mbngs.DiscMetadata()
md.artist = md.sortName = 'Jeff Buckley'
md.title = 'Grace'
prog.metadata = md

View File

@@ -7,6 +7,8 @@ import unittest
from morituri.image import table, cue
from morituri.test import common
class KingsSingleTestCase(unittest.TestCase):
@@ -73,7 +75,7 @@ class WriteCueFileTestCase(unittest.TestCase):
it.absolutize()
it.leadout = 3000
self.assertEquals(it.cue(), """REM DISCID 0C002802
common.diffStrings(u"""REM DISCID 0C002802
REM COMMENT "Morituri"
FILE "track01.wav" WAVE
TRACK 01 AUDIO
@@ -82,5 +84,5 @@ FILE "track01.wav" WAVE
INDEX 00 00:13:25
FILE "track02.wav" WAVE
INDEX 01 00:00:00
""")
""", it.cue())
os.unlink(path)

View File

@@ -23,7 +23,10 @@ class CureTestCase(common.TestCase):
def testGetTrackLength(self):
t = self.toc.table.tracks[0]
# first track has known length because the .toc is a single file
self.assertEquals(self.toc.getTrackLength(t), 28324)
# its length is all of track 1 from .toc, plus the INDEX 00 length
# of track 2
self.assertEquals(self.toc.getTrackLength(t),
(((6 * 60) + 16) * 75 + 45) + ((1 * 75) + 4))
# last track has unknown length
t = self.toc.table.tracks[-1]
self.assertEquals(self.toc.getTrackLength(t), -1)
@@ -59,7 +62,7 @@ class CureTestCase(common.TestCase):
self._assertAbsolute(2, 1, 28324)
self._assertPath(1, 1, "data.wav")
self.toc.table.absolutize()
# self.toc.table.absolutize()
self.toc.table.clearFiles()
self._assertAbsolute(1, 1, 0)
@@ -83,10 +86,11 @@ class CureTestCase(common.TestCase):
self._assertRelative(2, 1, None)
def testConvertCue(self):
self.toc.table.absolutize()
# self.toc.table.absolutize()
cue = self.toc.table.cue()
ref = open(os.path.join(os.path.dirname(__file__), 'cure.cue')).read()
common.diffStrings(cue, ref)
ref = open(os.path.join(os.path.dirname(__file__), 'cure.cue')).read(
).decode('utf-8')
common.diffStrings(ref, cue)
# we verify it because it has failed in readdisc in the past
self.assertEquals(self.toc.table.getAccurateRipURL(),
@@ -126,9 +130,28 @@ class BlocTestCase(common.TestCase):
self.assertEquals(self.toc.getTrackLength(t), -1)
def testIndexes(self):
t = self.toc.table.tracks[0]
self.assertEquals(t.getIndex(0).relative, 0)
self.assertEquals(t.getIndex(1).relative, 15220)
track01 = self.toc.table.tracks[0]
index00 = track01.getIndex(0)
self.assertEquals(index00.absolute, 0)
self.assertEquals(index00.relative, 0)
self.assertEquals(index00.counter, 0)
index01 = track01.getIndex(1)
self.assertEquals(index01.absolute, 15220)
self.assertEquals(index01.relative, 0)
self.assertEquals(index01.counter, 1)
track05 = self.toc.table.tracks[4]
index00 = track05.getIndex(0)
self.assertEquals(index00.absolute, 84070)
self.assertEquals(index00.relative, 68850)
self.assertEquals(index00.counter, 1)
index01 = track05.getIndex(1)
self.assertEquals(index01.absolute, 84142)
self.assertEquals(index01.relative, 68922)
self.assertEquals(index01.counter, 1)
# This disc has a pre-gap, so is a good test for .CUE writing
@@ -137,11 +160,11 @@ class BlocTestCase(common.TestCase):
self.failUnless(self.toc.table.hasTOC())
cue = self.toc.table.cue()
ref = open(os.path.join(os.path.dirname(__file__),
'bloc.cue')).read()
self.assertEquals(cue, ref)
'bloc.cue')).read().decode('utf-8')
common.diffStrings(ref, cue)
def testCDDBId(self):
self.toc.table.absolutize()
# self.toc.table.absolutize()
# cd-discid output:
# ad0be00d 13 15370 35019 51532 69190 84292 96826 112527 132448
# 148595 168072 185539 203331 222103 3244
@@ -150,7 +173,7 @@ class BlocTestCase(common.TestCase):
def testAccurateRip(self):
# we verify it because it has failed in readdisc in the past
self.toc.table.absolutize()
# self.toc.table.absolutize()
self.assertEquals(self.toc.table.getAccurateRipURL(),
'http://www.accuraterip.com/accuraterip/'
'e/d/2/dBAR-013-001af2de-0105994e-ad0be00d.bin')
@@ -178,7 +201,7 @@ class BreedersTestCase(common.TestCase):
self.assertEquals(cdt['TITLE'], 'OVERGLAZED')
def testConvertCue(self):
self.toc.table.absolutize()
# self.toc.table.absolutize()
self.failUnless(self.toc.table.hasTOC())
cue = self.toc.table.cue()
ref = open(os.path.join(os.path.dirname(__file__),
@@ -200,7 +223,7 @@ class LadyhawkeTestCase(common.TestCase):
self.failIf(self.toc.table.tracks[-1].audio)
def testCDDBId(self):
self.toc.table.absolutize()
#self.toc.table.absolutize()
self.assertEquals(self.toc.table.getCDDBDiscId(), 'c60af50d')
# output from cd-discid:
# c60af50d 13 150 15687 31841 51016 66616 81352 99559 116070 133243
@@ -249,7 +272,7 @@ class CapitalMergeTestCase(common.TestCase):
self.table.merge(self.toc2.table)
def testCDDBId(self):
self.table.absolutize()
#self.table.absolutize()
self.assertEquals(self.table.getCDDBDiscId(), 'b910140c')
# output from cd-discid:
# b910140c 12 24320 44855 64090 77885 88095 104020 118245 129255 141765
@@ -316,5 +339,110 @@ class TOTBLTestCase(common.TestCase):
self.assertEquals(len(self.toc.table.tracks), 11)
def testCDDBId(self):
self.toc.table.absolutize()
#self.toc.table.absolutize()
self.assertEquals(self.toc.table.getCDDBDiscId(), '810b7b0b')
# The Strokes - Someday has a 1 frame SILENCE marked as such in toc
class StrokesTestCase(common.TestCase):
def setUp(self):
self.path = os.path.join(os.path.dirname(__file__),
u'strokes-someday.toc')
self.toc = toc.TocFile(self.path)
self.toc.parse()
self.assertEquals(len(self.toc.table.tracks), 1)
def testIndexes(self):
t = self.toc.table.tracks[0]
i0 = t.getIndex(0)
self.assertEquals(i0.relative, 0)
self.assertEquals(i0.absolute, 0)
self.assertEquals(i0.counter, 0)
self.assertEquals(i0.path, None)
i1 = t.getIndex(1)
self.assertEquals(i1.relative, 0)
self.assertEquals(i1.absolute, 1)
self.assertEquals(i1.counter, 1)
self.assertEquals(i1.path, u'data.wav')
cue = self._filterCue(self.toc.table.cue())
ref = self._filterCue(open(os.path.join(os.path.dirname(__file__),
'strokes-someday.eac.cue')).read()).decode('utf-8')
common.diffStrings(ref, cue)
def _filterCue(self, output):
# helper to be able to compare our generated .cue with the
# EAC-extracted one
discard = [ 'TITLE', 'PERFORMER', 'FLAGS', 'REM' ]
lines = output.split('\n')
res = []
for line in lines:
found = False
for needle in discard:
if line.find(needle) > -1:
found = True
if line.find('FILE') > -1:
line = 'FILE "data.wav" WAVE'
if not found:
res.append(line)
return '\n'.join(res)
# Surfer Rosa has
# track 00 consisting of 32 frames of SILENCE
# track 11 Vamos with an INDEX 02
# compared to an EAC single .cue file, all our offsets are 32 frames off
# because the toc uses silence for track 01 index 00 while EAC puts it in
# Range.wav
class SurferRosaTestCase(common.TestCase):
def setUp(self):
self.path = os.path.join(os.path.dirname(__file__),
u'surferrosa.toc')
self.toc = toc.TocFile(self.path)
self.toc.parse()
self.assertEquals(len(self.toc.table.tracks), 21)
def testIndexes(self):
# HTOA
t = self.toc.table.tracks[0]
self.assertEquals(len(t.indexes), 2)
i0 = t.getIndex(0)
self.assertEquals(i0.relative, 0)
self.assertEquals(i0.absolute, 0)
self.assertEquals(i0.path, None)
self.assertEquals(i0.counter, 0)
i1 = t.getIndex(1)
self.assertEquals(i1.relative, 0)
self.assertEquals(i1.absolute, 32)
self.assertEquals(i1.path, 'data.wav')
self.assertEquals(i1.counter, 1)
# track 11, Vamos
t = self.toc.table.tracks[10]
self.assertEquals(len(t.indexes), 2)
# 32 frames of silence, and 1483 seconds of data.wav
self.assertEquals(t.getIndex(1).relative, 111225)
self.assertEquals(t.getIndex(1).absolute, 111257)
self.assertEquals(t.getIndex(2).relative, 111225 + 3370)
self.assertEquals(t.getIndex(2).absolute, 111257 + 3370)
# print self.toc.table.cue()

View File

@@ -25,7 +25,23 @@ class ParseTestCase(common.TestCase):
self._parser.parse(line)
q = '%.01f %%' % (self._parser.getTrackQuality() * 100.0, )
self.assertEquals(q, '99.7 %')
self.assertEquals(q, '99.6 %')
class Parse1FrameTestCase(common.TestCase):
def setUp(self):
path = os.path.join(os.path.dirname(__file__),
'cdparanoia.progress.strokes')
self._parser = cdparanoia.ProgressParser(start=0, stop=0)
self._handle = open(path)
def testParse(self):
for line in self._handle.readlines():
self._parser.parse(line)
q = '%.01f %%' % (self._parser.getTrackQuality() * 100.0, )
self.assertEquals(q, '100.0 %')
class ErrorTestCase(common.TestCase):
@@ -49,7 +65,11 @@ class ErrorTestCase(common.TestCase):
class VersionTestCase(common.TestCase):
def testGetVersion(self):
self.failUnless(cdparanoia.getCdParanoiaVersion())
v = cdparanoia.getCdParanoiaVersion()
self.failUnless(v)
# of the form III 10.2
# make sure it ends with a digit
self.failUnless(int(v[-1]), v)
class AnalyzeFileTask(cdparanoia.AnalyzeTask):

View File

@@ -2,10 +2,11 @@
# vi:si:et:sw=4:sts=4:ts=4
import os
import unittest
from morituri.program import cdrdao
from morituri.test import common
class FakeTask:
@@ -13,7 +14,7 @@ class FakeTask:
pass
class ParseTestCase(unittest.TestCase):
class ParseTestCase(common.TestCase):
def setUp(self):
path = os.path.join(os.path.dirname(__file__),
@@ -34,3 +35,12 @@ class ParseTestCase(unittest.TestCase):
self.assertEquals(track.getIndex(1).absolute, offset)
self.assertEquals(self._parser.version, '1.2.2')
class VersionTestCase(common.TestCase):
def testGetVersion(self):
v = cdrdao.getCDRDAOVersion()
self.failUnless(v)
# make sure it starts with a digit
self.failUnless(int(v[0]))