diff --git a/.gitignore b/.gitignore
index ff01510..0fbf43a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,4 @@ install-sh
missing
morituri.spec
py-compile
+REVISION
diff --git a/HACKING b/HACKING
index 44723c3..586a564 100644
--- a/HACKING
+++ b/HACKING
@@ -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
diff --git a/Makefile.am b/Makefile.am
index dc8e9f7..6c09c26 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -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 $< > $@
diff --git a/NEWS b/NEWS
index 216a932..a3a6ce8 100644
--- a/NEWS
+++ b/NEWS
@@ -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:
diff --git a/README b/README
deleted file mode 100644
index e71dd15..0000000
--- a/README
+++ /dev/null
@@ -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
diff --git a/README b/README
new file mode 120000
index 0000000..42061c0
--- /dev/null
+++ b/README
@@ -0,0 +1 @@
+README.md
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8fcf713
--- /dev/null
+++ b/README.md
@@ -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)
+
diff --git a/RELEASE b/RELEASE
index be74f73..5edf18f 100644
--- a/RELEASE
+++ b/RELEASE
@@ -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
diff --git a/TODO b/TODO
index 6198f76..3afb241 100644
--- a/TODO
+++ b/TODO
@@ -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)
diff --git a/bin/rip.in b/bin/rip.in
index ac38958..6f7cccf 100755
--- a/bin/rip.in
+++ b/bin/rip.in
@@ -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:
diff --git a/configure.ac b/configure.ac
index 886a527..3f38cc5 100644
--- a/configure.ac
+++ b/configure.ac
@@ -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")
diff --git a/doc/release b/doc/release
index 9590a3a..d95d6ae 100644
--- a/doc/release
+++ b/doc/release
@@ -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
diff --git a/etc/bash_completion.d/.gitignore b/etc/bash_completion.d/.gitignore
index b6a9088..8981283 100644
--- a/etc/bash_completion.d/.gitignore
+++ b/etc/bash_completion.d/.gitignore
@@ -1 +1,2 @@
rip
+bash-compgen
diff --git a/morituri.doap b/morituri.doap
index 6e935a9..b58e367 100644
--- a/morituri.doap
+++ b/morituri.doap
@@ -40,6 +40,44 @@ Morituri is a CD ripper aiming for maximum quality.
+
+
+ 0.2.2
+ master
+ my bad
+ 2013-07-30
+
+
+
+- fixed rip offset find
+- set album and track artist id's properly
+- rip cd info no longer ejects
+
+
+
+
+
+
+ 0.2.1
+ master
+ married
+ 2013-07-14
+
+
+
+- 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
+
+
+
+
+
0.2.0
diff --git a/morituri.spec.in b/morituri.spec.in
index 1b924b9..2efe03b 100644
--- a/morituri.spec.in
+++ b/morituri.spec.in
@@ -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*
diff --git a/morituri/common/Makefile.am b/morituri/common/Makefile.am
index ce1e3a0..14c8c95 100644
--- a/morituri/common/Makefile.am
+++ b/morituri/common/Makefile.am
@@ -16,7 +16,8 @@ morituri_PYTHON = \
gstreamer.py \
log.py \
logcommand.py \
- musicbrainzngs.py \
+ mbngs.py \
+ path.py \
program.py \
renamer.py \
task.py
diff --git a/morituri/common/cache.py b/morituri/common/cache.py
index b1cdb06..d2ff9fe 100644
--- a/morituri/common/cache.py
+++ b/morituri/common/cache.py
@@ -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
diff --git a/morituri/common/checksum.py b/morituri/common/checksum.py
index 68bf524..e45550a 100644
--- a/morituri/common/checksum.py
+++ b/morituri/common/checksum.py
@@ -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
+
diff --git a/morituri/common/common.py b/morituri/common/common.py
index a647ba0..1a30eaf 100644
--- a/morituri/common/common.py
+++ b/morituri/common/common.py
@@ -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)'
diff --git a/morituri/common/config.py b/morituri/common/config.py
index 125f87f..109dfed 100644
--- a/morituri/common/config.py
+++ b/morituri/common/config.py
@@ -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:'):
diff --git a/morituri/common/directory.py b/morituri/common/directory.py
index 66fb510..47aac11 100644
--- a/morituri/common/directory.py
+++ b/morituri/common/directory.py
@@ -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)
diff --git a/morituri/common/drive.py b/morituri/common/drive.py
index 80c3de6..a206ac0 100644
--- a/morituri/common/drive.py
+++ b/morituri/common/drive.py
@@ -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()
diff --git a/morituri/common/encode.py b/morituri/common/encode.py
index dba0eb1..7e4e0ee 100644
--- a/morituri/common/encode.py
+++ b/morituri/common/encode.py
@@ -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):
diff --git a/morituri/common/gstreamer.py b/morituri/common/gstreamer.py
index f1a2a56..fd5c38a 100644
--- a/morituri/common/gstreamer.py
+++ b/morituri/common/gstreamer.py
@@ -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
diff --git a/morituri/common/musicbrainzngs.py b/morituri/common/mbngs.py
similarity index 66%
rename from morituri/common/musicbrainzngs.py
rename to morituri/common/mbngs.py
index 9a0ecfa..31a8883 100644
--- a/morituri/common/musicbrainzngs.py
+++ b/morituri/common/mbngs.py
@@ -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/
diff --git a/morituri/common/path.py b/morituri/common/path.py
new file mode 100644
index 0000000..cb595ae
--- /dev/null
+++ b/morituri/common/path.py
@@ -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 .
+
+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
diff --git a/morituri/common/program.py b/morituri/common/program.py
index 5c49d86..d09f1c2 100644
--- a/morituri/common/program.py
+++ b/morituri/common/program.py
@@ -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
diff --git a/morituri/common/renamer.py b/morituri/common/renamer.py
index b92f1f4..0373064 100644
--- a/morituri/common/renamer.py
+++ b/morituri/common/renamer.py
@@ -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)
diff --git a/morituri/configure/configure.py b/morituri/configure/configure.py
index f55525a..3d626f4 100644
--- a/morituri/configure/configure.py
+++ b/morituri/configure/configure.py
@@ -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()
diff --git a/morituri/configure/installed.py.in b/morituri/configure/installed.py.in
index c2c536a..c80f431 100644
--- a/morituri/configure/installed.py.in
+++ b/morituri/configure/installed.py.in
@@ -7,4 +7,5 @@ def get():
'isinstalled': True,
'pluginsdir': '@PLUGINSDIR@',
'version': '@VERSION@',
+ 'revision': '@REVISION@',
}
diff --git a/morituri/configure/uninstalled.py.in b/morituri/configure/uninstalled.py.in
index 7e9aea3..a8155f8 100644
--- a/morituri/configure/uninstalled.py.in
+++ b/morituri/configure/uninstalled.py.in
@@ -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(),
}
diff --git a/morituri/extern/Makefile.am b/morituri/extern/Makefile.am
index c39716d..327739b 100644
--- a/morituri/extern/Makefile.am
+++ b/morituri/extern/Makefile.am
@@ -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
diff --git a/morituri/extern/python-command b/morituri/extern/python-command
index f96c666..bea37f8 160000
--- a/morituri/extern/python-command
+++ b/morituri/extern/python-command
@@ -1 +1 @@
-Subproject commit f96c66672b0a674fb932562e1375f4e406f88f16
+Subproject commit bea37f88ecb02db5342e52d3ab0f61ec33d85b1f
diff --git a/morituri/image/table.py b/morituri/image/table.py
index a34717a..c3cd570 100644
--- a/morituri/image/table.py
+++ b/morituri/image/table.py
@@ -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)
diff --git a/morituri/image/toc.py b/morituri/image/toc.py
index 55f902d..c83e940 100644
--- a/morituri/image/toc.py
+++ b/morituri/image/toc.py
@@ -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.*)" # 'file name' in quotes
\s+(?P.+) # start offset
- \s(?P.+)$ # stop offset
+ \s(?P.+)$ # 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 = '' % 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 '' % (self.path, )
diff --git a/morituri/program/cdparanoia.py b/morituri/program/cdparanoia.py
index 406d6d9..46176d5 100644
--- a/morituri/program/cdparanoia.py
+++ b/morituri/program/cdparanoia.py
@@ -64,19 +64,21 @@ class ChecksumException(Exception):
pass
+# example:
+# ##: 0 [read] @ 24696
_PROGRESS_RE = re.compile(r"""
- ^\#\#: (?P.+)\s # function code
- \[(?P.*)\]\s@\s # function name
- (?P\d+) # offset
+ ^\#\#: (?P.+)\s # function code
+ \[(?P.*)\]\s@\s # [function name] @
+ (?P\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.')
diff --git a/morituri/program/cdrdao.py b/morituri/program/cdrdao.py
index 15f90bf..c6fba64 100644
--- a/morituri/program/cdrdao.py
+++ b/morituri/program/cdrdao.py
@@ -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.+) -")
+
+
+def getCDRDAOVersion():
+ getter = common.VersionGetter('cdrdao',
+ ["cdrdao"],
+ _VERSION_RE,
+ "%(version)s")
+
+ return getter.get()
diff --git a/morituri/rip/cd.py b/morituri/rip/cd.py
index 436b6ba..0183764 100644
--- a/morituri/rip/cd.py
+++ b/morituri/rip/cd.py
@@ -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()
diff --git a/morituri/rip/common.py b/morituri/rip/common.py
index ccf1ef2..e931646 100644
--- a/morituri/rip/common.py
+++ b/morituri/rip/common.py
@@ -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)
diff --git a/morituri/rip/debug.py b/morituri/rip/debug.py
index 5bb6a68..68a57cc 100644
--- a/morituri/rip/debug.py
+++ b/morituri/rip/debug.py
@@ -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]
diff --git a/morituri/rip/image.py b/morituri/rip/image.py
index a9afef8..2fd668d 100644
--- a/morituri/rip/image.py
+++ b/morituri/rip/image.py
@@ -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()
diff --git a/morituri/rip/offset.py b/morituri/rip/offset.py
index 43e3131..965c9f0 100644
--- a/morituri/rip/offset.py
+++ b/morituri/rip/offset.py
@@ -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:
diff --git a/morituri/test/Makefile.am b/morituri/test/Makefile.am
index e9f0751..22bac66 100644
--- a/morituri/test/Makefile.am
+++ b/morituri/test/Makefile.am
@@ -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 \
diff --git a/morituri/test/bloc.cue b/morituri/test/bloc.cue
index 2176f26..a6c3295 100644
--- a/morituri/test/bloc.cue
+++ b/morituri/test/bloc.cue
@@ -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
diff --git a/morituri/test/cdparanoia.progress.strokes b/morituri/test/cdparanoia.progress.strokes
new file mode 100644
index 0000000..3369ab5
--- /dev/null
+++ b/morituri/test/cdparanoia.progress.strokes
@@ -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.
+
+
diff --git a/morituri/test/common.py b/morituri/test/common.py
index d1aa8d7..955db36 100644
--- a/morituri/test/common.py
+++ b/morituri/test/common.py
@@ -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')]
diff --git a/morituri/test/morituri.release.61c6fd9b-18f8-4a45-963a-ba3c5d990cae.json b/morituri/test/morituri.release.61c6fd9b-18f8-4a45-963a-ba3c5d990cae.json
new file mode 100644
index 0000000..de8f5d8
--- /dev/null
+++ b/morituri/test/morituri.release.61c6fd9b-18f8-4a45-963a-ba3c5d990cae.json
@@ -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"}}
\ No newline at end of file
diff --git a/morituri/test/morituri.release.a76714e0-32b1-4ed4-b28e-f86d99642193.json b/morituri/test/morituri.release.a76714e0-32b1-4ed4-b28e-f86d99642193.json
new file mode 100644
index 0000000..b7390a9
--- /dev/null
+++ b/morituri/test/morituri.release.a76714e0-32b1-4ed4-b28e-f86d99642193.json
@@ -0,0 +1 @@
+{"release": {"status": "Official", "artist-credit": [{"artist": {"sort-name": "Various Artists", "id": "89ad4ac3-39f7-470e-963a-56509c546377", "name": "Various Artists"}}], "title": "2 Meter Sessies, Volume 10", "label-info-list": [], "medium-list": [{"disc-list": [{"id": "f7XO36a7n1LCCskkCiulReWbwZA-", "sectors": "317128"}], "position": "1", "track-list": [{"recording": {"artist-credit": [{"artist": {"sort-name": "Coldplay", "id": "cc197bad-dc9c-440d-a5b5-d52ba2e14234", "name": "Coldplay"}}], "length": "265200", "artist-credit-phrase": "Coldplay", "id": "06813123-5047-4c94-88bf-6a300540e954", "title": "Trouble"}, "position": "1"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Live", "id": "cba77ba2-862d-4cee-a8f6-d3f9daf7211c", "name": "Live"}}], "length": "264466", "artist-credit-phrase": "Live", "id": "7b57b108-35bb-4fcb-9046-06228fb7e5f7", "title": "Run to the Water"}, "position": "2"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Beck", "id": "309c62ba-7a22-4277-9f67-4a162526d18a", "name": "Beck"}}], "length": "384440", "artist-credit-phrase": "Beck", "id": "cfbfb04e-ccfd-4316-a5eb-5e4daa670905", "title": "Debra"}, "position": "3"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Jayhawks, The", "id": "24ed5b09-02b1-47fe-bd83-6fa5270039b0", "name": "The Jayhawks"}}], "length": "261746", "artist-credit-phrase": "The Jayhawks", "id": "d7b84a3f-628d-49f3-ae2f-b34d5630ee45", "title": "Mr. Wilson"}, "position": "4"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Dijk, De", "id": "d7a55e92-a14c-4543-8152-de2163af06bb", "name": "De Dijk"}}], "length": "239080", "artist-credit-phrase": "De Dijk", "id": "2bb1a0ca-399a-4488-8a53-bb0ca9ece5ea", "title": "Wie het niet weet"}, "position": "5"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Torrini, Emil\u00edana", "id": "b2a9731b-9e13-4ff9-af21-5e694a5663e8", "name": "Emil\u00edana Torrini"}}], "length": "209800", "artist-credit-phrase": "Emil\u00edana Torrini", "id": "3e6433da-9af5-41b0-9210-6dbef13c630c", "title": "Summer Breeze"}, "position": "6"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Hiatt, John", "id": "e78202c9-7717-435c-9aac-dd5ebc4e64d5", "name": "John Hiatt"}}], "length": "159600", "artist-credit-phrase": "John Hiatt", "id": "ce684ade-741f-47f7-ac1c-0d7d206da8a3", "title": "What Do We Do Now"}, "position": "7"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Hay, Barry & Barking Dogs", "id": "dc11b420-0e21-4e05-aca7-273f58c8bcce", "name": "Barry Hay & Barking Dogs"}}], "length": "184800", "artist-credit-phrase": "Barry Hay & Barking Dogs", "id": "56b1b506-64cd-4c2f-be6d-044e3888dabd", "title": "Happiness Is a Warm Gun"}, "position": "8"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Penn, Dan", "id": "cc54ec8d-ba66-4051-970d-6b3c24cd9e8b", "name": "Dan Penn"}}, " & ", {"artist": {"sort-name": "Oldham, Spooner", "id": "ba170eca-541b-4ee5-b332-54ff954b75ea", "name": "Spooner Oldham"}}], "length": "231640", "artist-credit-phrase": "Dan Penn & Spooner Oldham", "id": "4d1c6a29-dd96-4af6-a594-622d70e214ac", "title": "I'm Your Puppet"}, "position": "9"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Stone, Angie", "id": "82f8dd22-0319-4f35-953c-358b3f883027", "name": "Angie Stone"}}], "length": "211800", "artist-credit-phrase": "Angie Stone", "id": "cb0447fc-3ad3-4dea-a94d-517179a6d68c", "title": "Everyday"}, "position": "10"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Helsen, Tom", "id": "0a5fe43b-ace7-407b-bfc2-be4851e7d3f2", "name": "Tom Helsen"}}], "length": "249693", "artist-credit-phrase": "Tom Helsen", "id": "2f6501f8-262a-4f02-a782-ed365621e100", "title": "When Marvin Calls"}, "position": "11"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "K's Choice", "id": "9bd1e632-b17b-4842-b520-ddfce3b538b9", "name": "K\u2019s Choice"}}], "length": "210666", "artist-credit-phrase": "K\u2019s Choice", "id": "e3ef3fa1-3155-464d-a5e0-4096e9cc63ad", "title": "Almost Happy"}, "position": "12"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Casey, Paddy", "id": "d36a3897-f76d-4227-be80-d0d7282ff12a", "name": "Paddy Casey"}}], "length": "191000", "artist-credit-phrase": "Paddy Casey", "id": "c419e7a6-cbe7-44c9-a45e-08e0721695dd", "title": "Can't Take That Away"}, "position": "13"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Jackson, Joe", "id": "07f6d469-38f3-46da-9cfa-2f532422b84e", "name": "Joe Jackson"}}], "length": "267933", "artist-credit-phrase": "Joe Jackson", "id": "ebb7083f-4db2-4daa-a67d-2993887b67ad", "title": "Stranger Than You"}, "position": "14"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "My Morning Jacket", "id": "ea5883b7-68ce-48b3-b115-61746ea53b8c", "name": "My Morning Jacket"}}], "length": "325466", "artist-credit-phrase": "My Morning Jacket", "id": "62594b12-5907-42b6-b7d9-03ad5b0ddd35", "title": "Old September Blues"}, "position": "15"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Jones, Tom", "id": "57c6f649-6cde-48a7-8114-2a200247601a", "name": "Tom Jones"}}, " & ", {"artist": {"sort-name": "Stereophonics", "id": "0bfba3d3-6a04-4779-bb0a-df07df5b0558", "name": "Stereophonics"}}], "length": "193973", "artist-credit-phrase": "Tom Jones & Stereophonics", "id": "ba50a1c7-9e23-4c3e-b7aa-12e23eea6d19", "title": "Mama Told Me Not to Come"}, "position": "16"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Christophers, Ben", "id": "1a5b4ad0-593a-4069-a77d-dae722a5f0ac", "name": "Ben Christophers"}}], "length": "223333", "artist-credit-phrase": "Ben Christophers", "id": "c0cfc4cb-8c80-4516-b500-2df010418697", "title": "Sunday"}, "position": "17"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Barman, Tom", "id": "a9be8bc0-47a4-4a0b-af5f-feac18d3bc43", "name": "Tom Barman"}}, " & ", {"artist": {"sort-name": "Nueten, Van, Guy", "id": "8779d2fd-3fc8-4c1e-a37d-2edf66b07c4e", "name": "Guy Van Nueten"}}], "length": "151733", "artist-credit-phrase": "Tom Barman & Guy Van Nueten", "id": "e423a1d7-3ae1-4540-b267-d873c50043e7", "title": "Magnolia"}, "position": "18"}], "format": "CD"}], "text-representation": {"language": "eng", "script": "Latn"}, "date": "2001-10-15", "artist-credit-phrase": "Various Artists", "quality": "normal", "id": "a76714e0-32b1-4ed4-b28e-f86d99642193"}}
\ No newline at end of file
diff --git a/morituri/test/morituri.release.e32ae79a-336e-4d33-945c-8c5e8206dbd3.json b/morituri/test/morituri.release.e32ae79a-336e-4d33-945c-8c5e8206dbd3.json
new file mode 100644
index 0000000..e5bb887
--- /dev/null
+++ b/morituri/test/morituri.release.e32ae79a-336e-4d33-945c-8c5e8206dbd3.json
@@ -0,0 +1 @@
+{"release": {"status": "Official", "asin": "B000CNEQ64", "label-info-list": [{"label": {"sort-name": "V2 Records International", "id": "947c12a1-cf28-4380-a695-a944ad15e387", "name": "V2 Records International"}, "catalog-number": "VVR1035822"}], "title": "Ballad of the Broken Seas", "country": "GB", "barcode": "5033197358222", "artist-credit": [{"artist": {"sort-name": "Campbell, Isobel", "id": "d51f3a15-12a2-41a0-acfa-33b5eae71164", "name": "Isobel Campbell"}}, " & ", {"artist": {"sort-name": "Lanegan, Mark", "id": "a9126556-f555-4920-9617-6e013f8228a7", "name": "Mark Lanegan"}}], "medium-list": [{"disc-list": [{"id": "xAq8L4ELMW14.6wI6tt7QAcxiDI-", "sectors": "192868"}], "position": "1", "track-list": [{"recording": {"artist-credit": [{"artist": {"sort-name": "Campbell, Isobel", "id": "d51f3a15-12a2-41a0-acfa-33b5eae71164", "name": "Isobel Campbell"}}, " & ", {"artist": {"sort-name": "Lanegan, Mark", "id": "a9126556-f555-4920-9617-6e013f8228a7", "name": "Mark Lanegan"}}], "length": "171613", "artist-credit-phrase": "Isobel Campbell & Mark Lanegan", "id": "4fe44724-1d7e-4275-9693-b889864de750", "title": "Deus Ibi Est"}, "position": "1"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Campbell, Isobel", "id": "d51f3a15-12a2-41a0-acfa-33b5eae71164", "name": "Isobel Campbell"}}, " & ", {"artist": {"sort-name": "Lanegan, Mark", "id": "a9126556-f555-4920-9617-6e013f8228a7", "name": "Mark Lanegan"}}], "length": "190120", "artist-credit-phrase": "Isobel Campbell & Mark Lanegan", "id": "32047729-7ad9-42ae-8d9e-c256ef9251ec", "title": "Black Mountain"}, "position": "2"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Campbell, Isobel", "id": "d51f3a15-12a2-41a0-acfa-33b5eae71164", "name": "Isobel Campbell"}}, " & ", {"artist": {"sort-name": "Lanegan, Mark", "id": "a9126556-f555-4920-9617-6e013f8228a7", "name": "Mark Lanegan"}}], "length": "233880", "artist-credit-phrase": "Isobel Campbell & Mark Lanegan", "id": "0c71631a-5862-4834-ae8f-257b64bca745", "title": "The False Husband"}, "position": "3"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Campbell, Isobel", "id": "d51f3a15-12a2-41a0-acfa-33b5eae71164", "name": "Isobel Campbell"}}, " & ", {"artist": {"sort-name": "Lanegan, Mark", "id": "a9126556-f555-4920-9617-6e013f8228a7", "name": "Mark Lanegan"}}], "length": "162386", "artist-credit-phrase": "Isobel Campbell & Mark Lanegan", "id": "afc9e785-60fd-4942-a23c-3653633f4783", "title": "Ballad of the Broken Seas"}, "position": "4"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Campbell, Isobel", "id": "d51f3a15-12a2-41a0-acfa-33b5eae71164", "name": "Isobel Campbell"}}, " & ", {"artist": {"sort-name": "Lanegan, Mark", "id": "a9126556-f555-4920-9617-6e013f8228a7", "name": "Mark Lanegan"}}], "length": "160680", "artist-credit-phrase": "Isobel Campbell & Mark Lanegan", "id": "048932de-992d-4b08-ab4f-b5d735ea323e", "title": "Revolver"}, "position": "5"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Campbell, Isobel", "id": "d51f3a15-12a2-41a0-acfa-33b5eae71164", "name": "Isobel Campbell"}}, " & ", {"artist": {"sort-name": "Lanegan, Mark", "id": "a9126556-f555-4920-9617-6e013f8228a7", "name": "Mark Lanegan"}}], "length": "209066", "artist-credit-phrase": "Isobel Campbell & Mark Lanegan", "id": "42c0e096-6c48-43cf-b6d4-700903727418", "title": "Ramblin' Man"}, "position": "6"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Campbell, Isobel", "id": "d51f3a15-12a2-41a0-acfa-33b5eae71164", "name": "Isobel Campbell"}}, " & ", {"artist": {"sort-name": "Lanegan, Mark", "id": "a9126556-f555-4920-9617-6e013f8228a7", "name": "Mark Lanegan"}}], "length": "207133", "artist-credit-phrase": "Isobel Campbell & Mark Lanegan", "id": "ef599a4c-8163-4829-9332-8dfe8c79219a", "title": "(Do You Wanna) Come Walk With Me?"}, "position": "7"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Campbell, Isobel", "id": "d51f3a15-12a2-41a0-acfa-33b5eae71164", "name": "Isobel Campbell"}}, " & ", {"artist": {"sort-name": "Lanegan, Mark", "id": "a9126556-f555-4920-9617-6e013f8228a7", "name": "Mark Lanegan"}}], "length": "277186", "artist-credit-phrase": "Isobel Campbell & Mark Lanegan", "id": "765fc7cc-2055-4066-a5b2-f1afbd1fd1f8", "title": "Saturday's Gone"}, "position": "8"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Campbell, Isobel", "id": "d51f3a15-12a2-41a0-acfa-33b5eae71164", "name": "Isobel Campbell"}}, " & ", {"artist": {"sort-name": "Lanegan, Mark", "id": "a9126556-f555-4920-9617-6e013f8228a7", "name": "Mark Lanegan"}}], "length": "173640", "artist-credit-phrase": "Isobel Campbell & Mark Lanegan", "id": "61ac7fad-d396-4467-93a9-a25472561008", "title": "It's Hard to Kill a Bad Thing"}, "position": "9"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Campbell, Isobel", "id": "d51f3a15-12a2-41a0-acfa-33b5eae71164", "name": "Isobel Campbell"}}, " & ", {"artist": {"sort-name": "Lanegan, Mark", "id": "a9126556-f555-4920-9617-6e013f8228a7", "name": "Mark Lanegan"}}], "length": "224173", "artist-credit-phrase": "Isobel Campbell & Mark Lanegan", "id": "2fed65ae-3297-40d6-8f54-0d55f8ed7287", "title": "Honey Child What Can I Do?"}, "position": "10"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Campbell, Isobel", "id": "d51f3a15-12a2-41a0-acfa-33b5eae71164", "name": "Isobel Campbell"}}, " & ", {"artist": {"sort-name": "Lanegan, Mark", "id": "a9126556-f555-4920-9617-6e013f8228a7", "name": "Mark Lanegan"}}], "length": "224560", "artist-credit-phrase": "Isobel Campbell & Mark Lanegan", "id": "33ce6721-b148-45ad-9a1e-1a4b1ea6912e", "title": "Dusty Wreath"}, "position": "11"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Campbell, Isobel", "id": "d51f3a15-12a2-41a0-acfa-33b5eae71164", "name": "Isobel Campbell"}}, " & ", {"artist": {"sort-name": "Lanegan, Mark", "id": "a9126556-f555-4920-9617-6e013f8228a7", "name": "Mark Lanegan"}}], "length": "335133", "artist-credit-phrase": "Isobel Campbell & Mark Lanegan", "id": "6cdb184d-12a0-4ba8-b50b-3325e0664f9e", "title": "The Circus Is Leaving Town"}, "position": "12"}], "format": "CD"}], "text-representation": {"language": "eng", "script": "Latn"}, "date": "2006-01-30", "artist-credit-phrase": "Isobel Campbell & Mark Lanegan", "quality": "normal", "id": "e32ae79a-336e-4d33-945c-8c5e8206dbd3"}}
\ No newline at end of file
diff --git a/morituri/test/strokes-someday.eac.cue b/morituri/test/strokes-someday.eac.cue
new file mode 100644
index 0000000..0180794
--- /dev/null
+++ b/morituri/test/strokes-someday.eac.cue
@@ -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
diff --git a/morituri/test/strokes-someday.toc b/morituri/test/strokes-someday.toc
new file mode 100644
index 0000000..bafd8e0
--- /dev/null
+++ b/morituri/test/strokes-someday.toc
@@ -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
+
diff --git a/morituri/test/surferrosa.eac.corrected.cue b/morituri/test/surferrosa.eac.corrected.cue
new file mode 100644
index 0000000..bf764b3
--- /dev/null
+++ b/morituri/test/surferrosa.eac.corrected.cue
@@ -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
diff --git a/morituri/test/surferrosa.eac.currentgap.cue b/morituri/test/surferrosa.eac.currentgap.cue
new file mode 100644
index 0000000..9e7f189
--- /dev/null
+++ b/morituri/test/surferrosa.eac.currentgap.cue
@@ -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
diff --git a/morituri/test/surferrosa.eac.leftout.cue b/morituri/test/surferrosa.eac.leftout.cue
new file mode 100644
index 0000000..b32adb4
--- /dev/null
+++ b/morituri/test/surferrosa.eac.leftout.cue
@@ -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
diff --git a/morituri/test/surferrosa.eac.noncompliant.cue b/morituri/test/surferrosa.eac.noncompliant.cue
new file mode 100644
index 0000000..9e7f189
--- /dev/null
+++ b/morituri/test/surferrosa.eac.noncompliant.cue
@@ -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
diff --git a/morituri/test/surferrosa.eac.single.cue b/morituri/test/surferrosa.eac.single.cue
new file mode 100644
index 0000000..702eb48
--- /dev/null
+++ b/morituri/test/surferrosa.eac.single.cue
@@ -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
diff --git a/morituri/test/surferrosa.toc b/morituri/test/surferrosa.toc
new file mode 100644
index 0000000..484f83b
--- /dev/null
+++ b/morituri/test/surferrosa.toc
@@ -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
+
diff --git a/morituri/test/test_common_common.py b/morituri/test/test_common_common.py
index 073a93a..de9e8a4 100644
--- a/morituri/test/test_common_common.py
+++ b/morituri/test/test_common_common.py
@@ -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)
diff --git a/morituri/test/test_common_directory.py b/morituri/test/test_common_directory.py
new file mode 100644
index 0000000..c7b2766
--- /dev/null
+++ b/morituri/test/test_common_directory.py
@@ -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'))
diff --git a/morituri/test/test_common_mbngs.py b/morituri/test/test_common_mbngs.py
new file mode 100644
index 0000000..6777bcc
--- /dev/null
+++ b/morituri/test/test_common_mbngs.py
@@ -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'
+ )
+
+
diff --git a/morituri/test/test_common_musicbrainzngs.py b/morituri/test/test_common_musicbrainzngs.py
deleted file mode 100644
index ffe9c1b..0000000
--- a/morituri/test/test_common_musicbrainzngs.py
+++ /dev/null
@@ -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)
diff --git a/morituri/test/test_common_path.py b/morituri/test/test_common_path.py
new file mode 100644
index 0000000..84a66d3
--- /dev/null
+++ b/morituri/test/test_common_path.py
@@ -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')
diff --git a/morituri/test/test_common_program.py b/morituri/test/test_common_program.py
index 102540b..d49c8bb 100644
--- a/morituri/test/test_common_program.py
+++ b/morituri/test/test_common_program.py
@@ -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
diff --git a/morituri/test/test_image_cue.py b/morituri/test/test_image_cue.py
index b6e2909..5abfcd7 100644
--- a/morituri/test/test_image_cue.py
+++ b/morituri/test/test_image_cue.py
@@ -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)
diff --git a/morituri/test/test_image_toc.py b/morituri/test/test_image_toc.py
index 1955d03..998bc0a 100644
--- a/morituri/test/test_image_toc.py
+++ b/morituri/test/test_image_toc.py
@@ -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()
+
diff --git a/morituri/test/test_program_cdparanoia.py b/morituri/test/test_program_cdparanoia.py
index 58e0bbc..6c910b1 100644
--- a/morituri/test/test_program_cdparanoia.py
+++ b/morituri/test/test_program_cdparanoia.py
@@ -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):
diff --git a/morituri/test/test_program_cdrdao.py b/morituri/test/test_program_cdrdao.py
index 0a777dd..37b4986 100644
--- a/morituri/test/test_program_cdrdao.py
+++ b/morituri/test/test_program_cdrdao.py
@@ -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]))