Merge pull request #1 from thomasvs/master
Catch JDLH fork up with thomasvs:master
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,3 +14,4 @@ install-sh
|
||||
missing
|
||||
morituri.spec
|
||||
py-compile
|
||||
REVISION
|
||||
|
||||
5
HACKING
5
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
|
||||
|
||||
@@ -5,7 +5,7 @@ ACLOCAL_AMFLAGS = -I m4
|
||||
|
||||
SUBDIRS = morituri bin etc doc m4 misc
|
||||
|
||||
EXTRA_DIST = morituri.spec morituri.doap RELEASE README HACKING
|
||||
EXTRA_DIST = morituri.spec morituri.doap RELEASE README.md HACKING REVISION
|
||||
|
||||
SOURCES = $(top_srcdir)/morituri/*.py $(top_srcdir)/morituri/*/*.py
|
||||
|
||||
@@ -38,6 +38,9 @@ PYCHECKER_BLACKLIST = \
|
||||
release: dist
|
||||
make $(PACKAGE)-$(VERSION).tar.bz2.md5
|
||||
|
||||
REVISION: $(top_srcdir)/.git
|
||||
$(PYTHON) -c 'from morituri.configure import configure; print configure.revision' > REVISION
|
||||
|
||||
# generate md5 sum files
|
||||
%.md5: %
|
||||
md5sum $< > $@
|
||||
|
||||
57
NEWS
57
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:
|
||||
|
||||
|
||||
157
README
157
README
@@ -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
|
||||
193
README.md
Normal file
193
README.md
Normal file
@@ -0,0 +1,193 @@
|
||||
morituri is a CD ripper aiming for accuracy over speed.
|
||||
Its features are modeled to compare with Exact Audio Copy on Windows.
|
||||
|
||||
|
||||
RATIONALE
|
||||
---------
|
||||
For a more detailed rationale, see my wiki page ['The Art of the Rip'](
|
||||
https://thomas.apestaart.org/thomas/trac/wiki/DAD/Rip).
|
||||
|
||||
FEATURES
|
||||
--------
|
||||
* support for MusicBrainz for metadata lookup
|
||||
* support for AccurateRip verification
|
||||
* detects sample read offset and ability to defeat cache of drives
|
||||
* performs test and copy rip
|
||||
* detects and rips Hidden Track One Audio
|
||||
* templates for file and directory naming
|
||||
* support for lossless encoding and lossy encoding or re-encoding of images
|
||||
* tagging using GStreamer, including embedding MusicBrainz id's
|
||||
* retagging of images
|
||||
* plugins for logging
|
||||
* for now, only a command line client (rip) is shipped
|
||||
|
||||
REQUIREMENTS
|
||||
------------
|
||||
- cdparanoia, for the actual ripping
|
||||
- cdrdao, for session, TOC, pregap, and ISRC extraction
|
||||
- GStreamer and its python bindings, for encoding
|
||||
- gst-plugins-base >= 0.10.22 for appsink
|
||||
- python musicbrainz2, for metadata lookup
|
||||
- python-setuptools, for plugin support
|
||||
- python-cddb, for showing but not using disc info if not in musicbrainz
|
||||
- pycdio, for drive identification (optional)
|
||||
|
||||
GETTING MORITURI
|
||||
----------------
|
||||
If you are building from a source tarball or checkout, you can choose to
|
||||
use morituri installed or uninstalled.
|
||||
|
||||
- getting:
|
||||
- Change to a directory where you want to put the morituri source code
|
||||
(For example, `$HOME/dev/ext` or `$HOME/prefix/src`)
|
||||
- source: download tarball, unpack, and change to its directory
|
||||
- checkout:
|
||||
|
||||
git clone git://github.com/thomasvs/morituri.git
|
||||
cd morituri
|
||||
git submodule init
|
||||
git submodule update
|
||||
./autogen.sh
|
||||
|
||||
- building:
|
||||
|
||||
./configure
|
||||
make
|
||||
|
||||
- you can now choose to install it or run it uninstalled.
|
||||
|
||||
- installing:
|
||||
|
||||
make install
|
||||
|
||||
- running uninstalled:
|
||||
|
||||
ln -sf `pwd`/misc/morituri-uninstalled $HOME/bin/morituri-git
|
||||
morituri-git # this drops you in a shell where everything is set up to use morituri
|
||||
|
||||
RUNNING MORITURI
|
||||
----------------
|
||||
morituri currently only has a command-line interface called 'rip'
|
||||
|
||||
rip is self-documenting.
|
||||
`rip -h` gives you the basic instructions.
|
||||
|
||||
rip implements a tree of commands; for example, the top-level 'changelog'
|
||||
command has a number of sub-commands.
|
||||
|
||||
Positioning of arguments is important;
|
||||
|
||||
rip cd -d (device) rip
|
||||
|
||||
is correct, while
|
||||
|
||||
rip cd rip -d (device)
|
||||
|
||||
is not, because the `-d` argument applies to the rip command.
|
||||
|
||||
Check the man page (rip(1)) for more information.
|
||||
|
||||
|
||||
RUNNING UNINSTALLED
|
||||
-------------------
|
||||
|
||||
To make it easier for developers, you can run morituri straight from the
|
||||
source checkout:
|
||||
|
||||
./autogen.sh
|
||||
make
|
||||
misc/morituri-uninstalled
|
||||
|
||||
GETTING STARTED
|
||||
---------------
|
||||
The simplest way to get started making accurate rips is:
|
||||
|
||||
- pick a relatively popular CD that has a good change of being in the
|
||||
AccurateRip database
|
||||
- find the drive's offset by running
|
||||
|
||||
rip offset find
|
||||
|
||||
- wait for it to complete; this might take a while
|
||||
- optionally, confirm this offset with two more discs
|
||||
- analyze the drive's caching behaviour
|
||||
|
||||
rip drive analyze
|
||||
|
||||
- rip the disc by running one of
|
||||
|
||||
rip cd rip # uses the offset from configuration file
|
||||
rip cd rip --offset (the number you got before) # manually specified offset
|
||||
|
||||
FILING BUGS
|
||||
-----------
|
||||
morituri's bug tracker is at [https://thomas.apestaart.org/morituri/trac/](
|
||||
https://thomas.apestaart.org/morituri/trac/).
|
||||
When filing bugs, please run the failing command with the environment variable
|
||||
`RIP_DEBUG` set; for example:
|
||||
|
||||
RIP_DEBUG=5 rip offset find > morituri.log 2>&1
|
||||
gzip morituri.log
|
||||
|
||||
And attach the gzipped log file to your bug report.
|
||||
|
||||
KNOWN ISSUES
|
||||
------------
|
||||
- no GUI yet
|
||||
|
||||
GOALS
|
||||
-----
|
||||
- quality over speed
|
||||
- support one-command automatic ripping
|
||||
- support offline ripping (doing metadata lookup and log rewriting later)
|
||||
- separate the info/result about the rip from the metadata/file generation/...
|
||||
|
||||
CONFIGURATION FILE
|
||||
------------------
|
||||
|
||||
The configuration file is stored according to [XDG Base Directory Specification](
|
||||
http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html)
|
||||
when possible.
|
||||
|
||||
It lives in `$XDG_CONFIG_HOME/morituri/morituri.conf`
|
||||
|
||||
The configuration file follows python's ConfigParser syntax.
|
||||
There is a "main" section and zero or more sections starting with "drive:"
|
||||
|
||||
- main section:
|
||||
- `path_filter_fat`: whether to filter path components for FAT file systems
|
||||
- `path_filter_special`: whether to filter path components for special
|
||||
characters
|
||||
|
||||
- drive section:
|
||||
All these values are probed by morituri and should not be edited by hand.
|
||||
- `defeats_cache`: whether this drive can defeat the audio cache
|
||||
- `read_offset`: the read offset of the drive
|
||||
|
||||
CONTRIBUTING
|
||||
------------
|
||||
- Please send pull requests through github.
|
||||
- You can always [flattr morituri to donate](https://flattr.com/submit/auto?%20%20user_id=thomasvs&url=https://thomas.apestaart.org/morituri/trac/&%20%20title=morituri&%20%20description=morituri&%20%20language=en_GB&tags=flattr,morituri,software&category=software)
|
||||
|
||||
|
||||
rip command tree
|
||||
----------------
|
||||
rip
|
||||
|
||||
* accurip
|
||||
* show (show accuraterip data)
|
||||
* offset
|
||||
* find (find drive's read offset using AccurateRip)
|
||||
* verify (verify drive's read offset using AccurateRip)
|
||||
* cd
|
||||
* rip (rip the cd)
|
||||
* debug
|
||||
* encode (encode a file)
|
||||
* htoa
|
||||
* find
|
||||
* rip (rip the htoa if it's there)
|
||||
* image
|
||||
* verify (verify the cd image)
|
||||
* encode (encode to a different codec)
|
||||
* retag (retag the image with current MusicBrainz data)
|
||||
|
||||
48
RELEASE
48
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
|
||||
|
||||
11
TODO
11
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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
17
doc/release
17
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
|
||||
|
||||
1
etc/bash_completion.d/.gitignore
vendored
1
etc/bash_completion.d/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
rip
|
||||
bash-compgen
|
||||
|
||||
@@ -40,6 +40,44 @@ Morituri is a CD ripper aiming for maximum quality.
|
||||
</foaf:Person>
|
||||
</maintainer>
|
||||
|
||||
<release>
|
||||
<Version>
|
||||
<revision>0.2.2</revision>
|
||||
<branch>master</branch>
|
||||
<name>my bad</name>
|
||||
<created>2013-07-30</created>
|
||||
<file-release rdf:resource="http://thomas.apestaart.org/download/morituri/morituri-0.2.2.tar.bz2" />
|
||||
<file-release rdf:resource="http://thomas.apestaart.org/download/morituri/morituri-0.2.2-1.noarch.rpm" />
|
||||
<dc:description>
|
||||
- fixed rip offset find
|
||||
- set album and track artist id's properly
|
||||
- rip cd info no longer ejects
|
||||
</dc:description>
|
||||
</Version>
|
||||
</release>
|
||||
|
||||
<release>
|
||||
<Version>
|
||||
<revision>0.2.1</revision>
|
||||
<branch>master</branch>
|
||||
<name>married</name>
|
||||
<created>2013-07-14</created>
|
||||
<file-release rdf:resource="http://thomas.apestaart.org/download/morituri/morituri-0.2.1.tar.bz2" />
|
||||
<file-release rdf:resource="http://thomas.apestaart.org/download/morituri/morituri-0.2.1-1.noarch.rpm" />
|
||||
<dc:description>
|
||||
- added "%X" template variable for uppercase filename extension
|
||||
- added rip cd info
|
||||
- added storing catalog number and barcode
|
||||
- disambiguate releases with same name but different catno/barcode
|
||||
- use all but last track to find offset
|
||||
- add support to filter path names for better file system support
|
||||
- add config options for path filtering
|
||||
- fixes for older pyxdg and some versions of pycdio
|
||||
</dc:description>
|
||||
</Version>
|
||||
</release>
|
||||
|
||||
|
||||
<release>
|
||||
<Version>
|
||||
<revision>0.2.0</revision>
|
||||
|
||||
@@ -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*
|
||||
|
||||
@@ -16,7 +16,8 @@ morituri_PYTHON = \
|
||||
gstreamer.py \
|
||||
log.py \
|
||||
logcommand.py \
|
||||
musicbrainzngs.py \
|
||||
mbngs.py \
|
||||
path.py \
|
||||
program.py \
|
||||
renamer.py \
|
||||
task.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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)'
|
||||
|
||||
@@ -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:'):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# -*- Mode: Python; test-case-name: morituri.test.test_common_musicbrainzngs -*-
|
||||
# -*- Mode: Python; test-case-name: morituri.test.test_common_mbngs -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
# Morituri - for those about to RIP
|
||||
@@ -56,10 +56,13 @@ class TrackMetadata(object):
|
||||
|
||||
class DiscMetadata(object):
|
||||
"""
|
||||
@param artist: artist(s) name
|
||||
@param sortName: album artist sort name
|
||||
@param release: earliest release date, in YYYY-MM-DD
|
||||
@type release: unicode
|
||||
@param title: title of the disc (with disambiguation)
|
||||
@param releaseTitle: title of the release (without disambiguation)
|
||||
@type tracks: C{list} of L{TrackMetadata}
|
||||
"""
|
||||
artist = None
|
||||
sortName = None
|
||||
@@ -90,7 +93,55 @@ def _record(record, which, name, what):
|
||||
handle = open(filename, 'w')
|
||||
handle.write(json.dumps(what))
|
||||
handle.close()
|
||||
log.info('musicbrainzngs', 'Wrote %s %s to %s', which, name, filename)
|
||||
log.info('mbngs', 'Wrote %s %s to %s', which, name, filename)
|
||||
|
||||
# credit is of the form [dict, str, dict, ... ]
|
||||
# e.g. [
|
||||
# {'artist': {
|
||||
# 'sort-name': 'Sukilove',
|
||||
# 'id': '5f4af6cf-a1b8-4e51-a811-befed399a1c6',
|
||||
# 'name': 'Sukilove'
|
||||
# }}, ' & ', {
|
||||
# 'artist': {
|
||||
# 'sort-name': 'Blackie and the Oohoos',
|
||||
# 'id': '028a9dc7-f5ef-43c2-866b-08d69ffff363',
|
||||
# 'name': 'Blackie & the Oohoos'}}]
|
||||
# or
|
||||
# [{'artist':
|
||||
# {'sort-name': 'Pixies',
|
||||
# 'id': 'b6b2bb8d-54a9-491f-9607-7b546023b433', 'name': 'Pixies'}}]
|
||||
|
||||
|
||||
class _Credit(list):
|
||||
"""
|
||||
I am a representation of an artist-credit in musicbrainz for a disc
|
||||
or track.
|
||||
"""
|
||||
|
||||
def joiner(self, attributeGetter, joinString=None):
|
||||
res = []
|
||||
|
||||
for item in self:
|
||||
if isinstance(item, dict):
|
||||
res.append(attributeGetter(item))
|
||||
else:
|
||||
if not joinString:
|
||||
res.append(item)
|
||||
else:
|
||||
res.append(joinString)
|
||||
|
||||
return "".join(res)
|
||||
|
||||
|
||||
def getSortName(self):
|
||||
return self.joiner(lambda i: i.get('artist').get('sort-name', None))
|
||||
|
||||
def getName(self):
|
||||
return self.joiner(lambda i: i.get('artist').get('name', None))
|
||||
|
||||
def getIds(self):
|
||||
return self.joiner(lambda i: i.get('artist').get('id', None),
|
||||
joinString=";")
|
||||
|
||||
|
||||
def _getMetadata(releaseShort, release, discid):
|
||||
@@ -109,45 +160,39 @@ def _getMetadata(releaseShort, release, discid):
|
||||
|
||||
assert release['id'], 'Release does not have an id'
|
||||
|
||||
metadata = DiscMetadata()
|
||||
discMD = DiscMetadata()
|
||||
|
||||
metadata.releaseType = releaseShort.get('release-group', {}).get('type')
|
||||
credit = release['artist-credit']
|
||||
discMD.releaseType = releaseShort.get('release-group', {}).get('type')
|
||||
discCredit = _Credit(release['artist-credit'])
|
||||
|
||||
artist = credit[0]['artist']
|
||||
# FIXME: is there a better way to check for VA ?
|
||||
discMD.various = False
|
||||
if discCredit[0]['artist']['id'] == VA_ID:
|
||||
discMD.various = True
|
||||
|
||||
if len(credit) > 1:
|
||||
log.debug('musicbrainzngs', 'artist-credit more than 1: %r', credit)
|
||||
|
||||
for i, c in enumerate(credit):
|
||||
if isinstance(c, dict):
|
||||
credit[i] = c.get(
|
||||
'name', c['artist'].get('name', None))
|
||||
if len(discCredit) > 1:
|
||||
log.debug('mbngs', 'artist-credit more than 1: %r', discCredit)
|
||||
|
||||
albumArtistName = "".join(credit)
|
||||
|
||||
# FIXME: is there a better way to check for VA
|
||||
metadata.various = False
|
||||
if artist['id'] == VA_ID:
|
||||
metadata.various = True
|
||||
albumArtistName = discCredit.getName()
|
||||
|
||||
# getUniqueName gets disambiguating names like Muse (UK rock band)
|
||||
metadata.artist = albumArtistName
|
||||
metadata.sortName = artist['sort-name']
|
||||
discMD.artist = albumArtistName
|
||||
discMD.sortName = discCredit.getSortName()
|
||||
# FIXME: is format str ?
|
||||
if not 'date' in release:
|
||||
log.warning('musicbrainzngs', 'Release %r does not have date', release)
|
||||
log.warning('mbngs', 'Release %r does not have date', release)
|
||||
else:
|
||||
metadata.release = release['date']
|
||||
discMD.release = release['date']
|
||||
|
||||
metadata.mbid = release['id']
|
||||
metadata.mbidArtist = artist['id']
|
||||
metadata.url = 'http://musicbrainz.org/release/' + release['id']
|
||||
discMD.mbid = release['id']
|
||||
discMD.mbidArtist = discCredit.getIds()
|
||||
discMD.url = 'http://musicbrainz.org/release/' + release['id']
|
||||
|
||||
metadata.barcode = release.get('barcode', None)
|
||||
discMD.barcode = release.get('barcode', None)
|
||||
lil = release.get('label-info-list', [{}])
|
||||
if lil:
|
||||
metadata.catalogNumber = lil[0].get('catalog-number')
|
||||
discMD.catalogNumber = lil[0].get('catalog-number')
|
||||
tainted = False
|
||||
duration = 0
|
||||
|
||||
@@ -156,7 +201,7 @@ def _getMetadata(releaseShort, release, discid):
|
||||
for disc in medium['disc-list']:
|
||||
if disc['id'] == discid:
|
||||
title = release['title']
|
||||
metadata.releaseTitle = title
|
||||
discMD.releaseTitle = title
|
||||
if 'disambiguation' in release:
|
||||
title += " (%s)" % release['disambiguation']
|
||||
count = len(release['medium-list'])
|
||||
@@ -165,31 +210,19 @@ def _getMetadata(releaseShort, release, discid):
|
||||
int(medium['position']), count)
|
||||
if 'title' in medium:
|
||||
title += ": %s" % medium['title']
|
||||
metadata.title = title
|
||||
discMD.title = title
|
||||
for t in medium['track-list']:
|
||||
track = TrackMetadata()
|
||||
credit = t['recording']['artist-credit']
|
||||
if len(credit) > 1:
|
||||
log.debug('musicbrainzngs',
|
||||
'artist-credit more than 1: %r', credit)
|
||||
# credit is of the form [dict, str, dict, ... ]
|
||||
for i, c in enumerate(credit):
|
||||
if isinstance(c, dict):
|
||||
credit[i] = c.get(
|
||||
'name', c['artist'].get('name', None))
|
||||
trackCredit = _Credit(t['recording']['artist-credit'])
|
||||
if len(trackCredit) > 1:
|
||||
log.debug('mbngs',
|
||||
'artist-credit more than 1: %r', trackCredit)
|
||||
|
||||
|
||||
trackArtistName = "".join(credit)
|
||||
|
||||
if not artist:
|
||||
track.artist = metadata.artist
|
||||
track.sortName = metadata.sortName
|
||||
track.mbidArtist = metadata.mbidArtist
|
||||
else:
|
||||
# various artists discs can have tracks with no artist
|
||||
track.artist = trackArtistName
|
||||
track.sortName = artist['sort-name']
|
||||
track.mbidArtist = artist['id']
|
||||
# FIXME: leftover comment, need an example
|
||||
# various artists discs can have tracks with no artist
|
||||
track.artist = trackCredit.getName()
|
||||
track.sortName = trackCredit.getSortName()
|
||||
track.mbidArtist = trackCredit.getIds()
|
||||
|
||||
track.title = t['recording']['title']
|
||||
track.mbid = t['recording']['id']
|
||||
@@ -204,14 +237,14 @@ def _getMetadata(releaseShort, release, discid):
|
||||
else:
|
||||
duration += track.duration
|
||||
|
||||
metadata.tracks.append(track)
|
||||
discMD.tracks.append(track)
|
||||
|
||||
if not tainted:
|
||||
metadata.duration = duration
|
||||
discMD.duration = duration
|
||||
else:
|
||||
metadata.duration = 0
|
||||
discMD.duration = 0
|
||||
|
||||
return metadata
|
||||
return discMD
|
||||
|
||||
|
||||
# see http://bugs.musicbrainz.org/browser/python-musicbrainz2/trunk/examples/
|
||||
68
morituri/common/path.py
Normal file
68
morituri/common/path.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# -*- Mode: Python; test-case-name: morituri.test.test_common_path -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
# Morituri - for those about to RIP
|
||||
|
||||
# Copyright (C) 2009 Thomas Vander Stichele
|
||||
|
||||
# This file is part of morituri.
|
||||
#
|
||||
# morituri is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# morituri is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with morituri. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
|
||||
|
||||
class PathFilter(object):
|
||||
"""
|
||||
I filter path components for safe storage on file systems.
|
||||
"""
|
||||
|
||||
def __init__(self, slashes=True, quotes=True, fat=True, special=False):
|
||||
"""
|
||||
@param slashes: whether to convert slashes to dashes
|
||||
@param quotes: whether to normalize quotes
|
||||
@param fat: whether to strip characters illegal on FAT filesystems
|
||||
@param special: whether to strip special characters
|
||||
"""
|
||||
self._slashes = slashes
|
||||
self._quotes = quotes
|
||||
self._fat = fat
|
||||
self._special = special
|
||||
|
||||
def filter(self, path):
|
||||
if self._slashes:
|
||||
path = re.sub(r'[/\\]', '-', path, re.UNICODE)
|
||||
|
||||
def separators(path):
|
||||
# replace separators with a space-hyphen or hyphen
|
||||
path = re.sub(r'[:]', ' -', path, re.UNICODE)
|
||||
path = re.sub(r'[\|]', '-', path, re.UNICODE)
|
||||
return path
|
||||
|
||||
# change all fancy single/double quotes to normal quotes
|
||||
if self._quotes:
|
||||
path = re.sub(ur'[\xc2\xb4\u2018\u2019\u201b]', "'", path,
|
||||
re.UNICODE)
|
||||
path = re.sub(ur'[\u201c\u201d\u201f]', '"', path, re.UNICODE)
|
||||
|
||||
if self._special:
|
||||
path = separators(path)
|
||||
path = re.sub(r'[\*\?&!\'\"\$\(\)`{}\[\]<>]', '_', path, re.UNICODE)
|
||||
|
||||
if self._fat:
|
||||
path = separators(path)
|
||||
# : and | already gone, but leave them here for reference
|
||||
path = re.sub(r'[:\*\?"<>|"]', '_', path, re.UNICODE)
|
||||
|
||||
return path
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -7,4 +7,5 @@ def get():
|
||||
'isinstalled': True,
|
||||
'pluginsdir': '@PLUGINSDIR@',
|
||||
'version': '@VERSION@',
|
||||
'revision': '@REVISION@',
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
13
morituri/extern/Makefile.am
vendored
13
morituri/extern/Makefile.am
vendored
@@ -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
|
||||
|
||||
2
morituri/extern/python-command
vendored
2
morituri/extern/python-command
vendored
Submodule morituri/extern/python-command updated: f96c66672b...bea37f88ec
@@ -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)
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
"""
|
||||
Reading .toc files
|
||||
|
||||
The .toc file format is described in the man page of cdrdao
|
||||
"""
|
||||
|
||||
import re
|
||||
@@ -62,7 +64,7 @@ _FILE_RE = re.compile(r"""
|
||||
^FILE # FILE
|
||||
\s+"(?P<name>.*)" # 'file name' in quotes
|
||||
\s+(?P<start>.+) # start offset
|
||||
\s(?P<length>.+)$ # stop offset
|
||||
\s(?P<length>.+)$ # length in frames of section
|
||||
""", re.VERBOSE)
|
||||
|
||||
_DATAFILE_RE = re.compile(r"""
|
||||
@@ -86,6 +88,48 @@ _INDEX_RE = re.compile(r"""
|
||||
""", re.VERBOSE)
|
||||
|
||||
|
||||
class Sources(log.Loggable):
|
||||
"""
|
||||
I represent the list of sources used in the .toc file.
|
||||
Each SILENCE and each FILE is a source.
|
||||
If the filename for FILE doesn't change, the counter is not increased.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._sources = []
|
||||
|
||||
def append(self, counter, offset, source):
|
||||
"""
|
||||
@param counter: the source counter; updates for each different
|
||||
data source (silence or different file path)
|
||||
@type counter: int
|
||||
@param offset: the absolute disc offset where this source starts
|
||||
"""
|
||||
self.debug('Appending source, counter %d, abs offset %d, source %r' % (
|
||||
counter, offset, source))
|
||||
self._sources.append((counter, offset, source))
|
||||
|
||||
def get(self, offset):
|
||||
"""
|
||||
Retrieve the source used at the given offset.
|
||||
"""
|
||||
for i, (c, o, s) in enumerate(self._sources):
|
||||
if offset < o:
|
||||
return self._sources[i - 1]
|
||||
|
||||
return self._sources[-1]
|
||||
|
||||
def getCounterStart(self, counter):
|
||||
"""
|
||||
Retrieve the absolute offset of the first source for this counter
|
||||
"""
|
||||
for i, (c, o, s) in enumerate(self._sources):
|
||||
if c == counter:
|
||||
return self._sources[i][1]
|
||||
|
||||
return self._sources[-1][1]
|
||||
|
||||
|
||||
class TocFile(object, log.Loggable):
|
||||
|
||||
def __init__(self, path):
|
||||
@@ -98,6 +142,27 @@ class TocFile(object, log.Loggable):
|
||||
self.table = table.Table()
|
||||
self.logName = '<TocFile %08x>' % id(self)
|
||||
|
||||
self._sources = Sources()
|
||||
|
||||
def _index(self, currentTrack, i, absoluteOffset, trackOffset):
|
||||
absolute = absoluteOffset + trackOffset
|
||||
# this may be in a new source, so calculate relative
|
||||
c, o, s = self._sources.get(absolute)
|
||||
self.debug('at abs offset %d, we are in source %r' % (
|
||||
absolute, s))
|
||||
counterStart = self._sources.getCounterStart(c)
|
||||
relative = absolute - counterStart
|
||||
|
||||
currentTrack.index(i, path=s.path,
|
||||
absolute=absolute,
|
||||
relative=relative,
|
||||
counter=c)
|
||||
self.debug(
|
||||
'[track %02d index %02d] trackOffset %r, added %r',
|
||||
currentTrack.number, i, trackOffset,
|
||||
currentTrack.getIndex(i))
|
||||
|
||||
|
||||
def parse(self):
|
||||
# these two objects start as None then get set as real objects,
|
||||
# so no need to complain about them here
|
||||
@@ -106,15 +171,16 @@ class TocFile(object, log.Loggable):
|
||||
currentTrack = None
|
||||
|
||||
state = 'HEADER'
|
||||
counter = 0
|
||||
counter = 0 # counts sources for audio data; SILENCE/ZERO/FILE
|
||||
trackNumber = 0
|
||||
indexNumber = 0
|
||||
absoluteOffset = 0 # running absolute offset of where each track starts
|
||||
relativeOffset = 0 # running relative offset, relative to counter src
|
||||
currentLength = 0 # accrued during TRACK record parsing, current track
|
||||
currentLength = 0 # accrued during TRACK record parsing;
|
||||
# length of current track as parsed so far;
|
||||
# reset on each TRACK statement
|
||||
totalLength = 0 # accrued during TRACK record parsing, total disc
|
||||
pregapLength = 0 # length of the pre-gap, current track
|
||||
|
||||
pregapLength = 0 # length of the pre-gap, current track in for loop
|
||||
|
||||
# the first track's INDEX 1 can only be gotten from the .toc
|
||||
# file once the first pregap is calculated; so we add INDEX 1
|
||||
@@ -160,28 +226,29 @@ class TocFile(object, log.Loggable):
|
||||
# set index 1 of previous track if there was one, using
|
||||
# pregapLength if applicable
|
||||
if currentTrack:
|
||||
# FIXME: why not set absolute offsets too ?
|
||||
currentTrack.index(1, path=currentFile.path,
|
||||
absolute=absoluteOffset + pregapLength,
|
||||
relative=relativeOffset + pregapLength,
|
||||
counter=counter)
|
||||
self.debug('track %d, added index %r',
|
||||
currentTrack.number, currentTrack.getIndex(1))
|
||||
self._index(currentTrack, 1, absoluteOffset, pregapLength)
|
||||
|
||||
# create a new track to be filled by later lines
|
||||
trackNumber += 1
|
||||
absoluteOffset += currentLength
|
||||
relativeOffset += currentLength
|
||||
totalLength += currentLength
|
||||
currentLength = 0
|
||||
indexNumber = 1
|
||||
trackMode = m.group('mode')
|
||||
pregapLength = 0
|
||||
|
||||
# FIXME: track mode
|
||||
self.debug('found track %d, mode %s', trackNumber, trackMode)
|
||||
audio = trackMode == 'AUDIO'
|
||||
currentTrack = table.Track(trackNumber, audio=audio)
|
||||
self.table.tracks.append(currentTrack)
|
||||
|
||||
# update running totals
|
||||
absoluteOffset += currentLength
|
||||
relativeOffset += currentLength
|
||||
totalLength += currentLength
|
||||
|
||||
# FIXME: track mode
|
||||
self.debug('found track %d, mode %s, at absoluteOffset %d',
|
||||
trackNumber, trackMode, absoluteOffset)
|
||||
|
||||
# reset counters relative to a track
|
||||
currentLength = 0
|
||||
indexNumber = 1
|
||||
pregapLength = 0
|
||||
|
||||
continue
|
||||
|
||||
# look for ISRC lines
|
||||
@@ -196,9 +263,11 @@ class TocFile(object, log.Loggable):
|
||||
if m:
|
||||
length = m.group('length')
|
||||
self.debug('SILENCE of %r', length)
|
||||
self._sources.append(counter, absoluteOffset, None)
|
||||
if currentFile is not None:
|
||||
self.debug('SILENCE after FILE, increasing counter')
|
||||
counter += 1
|
||||
relativeOffset = 0
|
||||
currentFile = None
|
||||
currentLength += common.msfToFrames(length)
|
||||
|
||||
@@ -208,6 +277,7 @@ class TocFile(object, log.Loggable):
|
||||
if currentFile is not None:
|
||||
self.debug('ZERO after FILE, increasing counter')
|
||||
counter += 1
|
||||
relativeOffset = 0
|
||||
currentFile = None
|
||||
length = m.group('length')
|
||||
currentLength += common.msfToFrames(length)
|
||||
@@ -227,7 +297,10 @@ class TocFile(object, log.Loggable):
|
||||
self.debug('track %d, switched to new FILE, '
|
||||
'increased counter to %d',
|
||||
trackNumber, counter)
|
||||
currentFile = File(filePath, start, length)
|
||||
currentFile = File(filePath, common.msfToFrames(start),
|
||||
common.msfToFrames(length))
|
||||
self._sources.append(counter, absoluteOffset + currentLength,
|
||||
currentFile)
|
||||
#absoluteOffset += common.msfToFrames(start)
|
||||
currentLength += common.msfToFrames(length)
|
||||
|
||||
@@ -246,7 +319,9 @@ class TocFile(object, log.Loggable):
|
||||
'increased counter to %d',
|
||||
trackNumber, counter)
|
||||
# FIXME: assume that a MODE2_FORM_MIX track always starts at 0
|
||||
currentFile = File(filePath, 0, length)
|
||||
currentFile = File(filePath, 0, common.msfToFrames(length))
|
||||
self._sources.append(counter, absoluteOffset + currentLength,
|
||||
currentFile)
|
||||
#absoluteOffset += common.msfToFrames(start)
|
||||
currentLength += common.msfToFrames(length)
|
||||
|
||||
@@ -260,10 +335,16 @@ class TocFile(object, log.Loggable):
|
||||
continue
|
||||
|
||||
length = common.msfToFrames(m.group('length'))
|
||||
currentTrack.index(0, path=currentFile.path,
|
||||
c, o, s = self._sources.get(absoluteOffset)
|
||||
self.debug('at abs offset %d, we are in source %r' % (
|
||||
absoluteOffset, s))
|
||||
counterStart = self._sources.getCounterStart(c)
|
||||
relativeOffset = absoluteOffset - counterStart
|
||||
|
||||
currentTrack.index(0, path=s and s.path or None,
|
||||
absolute=absoluteOffset,
|
||||
relative=relativeOffset, counter=counter)
|
||||
self.debug('track %d, added index %r',
|
||||
relative=relativeOffset, counter=c)
|
||||
self.debug('[track %02d index 00] added %r',
|
||||
currentTrack.number, currentTrack.getIndex(0))
|
||||
# store the pregapLength to add it when we index 1 for this
|
||||
# track on the next iteration
|
||||
@@ -279,22 +360,15 @@ class TocFile(object, log.Loggable):
|
||||
|
||||
indexNumber += 1
|
||||
offset = common.msfToFrames(m.group('offset'))
|
||||
currentTrack.index(indexNumber, path=currentFile.path,
|
||||
relative=offset, counter=counter)
|
||||
self.debug('track %d, added index %r',
|
||||
currentTrack.number, currentTrack.getIndex(indexNumber))
|
||||
self._index(currentTrack, indexNumber, absoluteOffset, offset)
|
||||
|
||||
# handle index 1 of final track, if any
|
||||
if currentTrack:
|
||||
currentTrack.index(1, path=currentFile.path,
|
||||
absolute=absoluteOffset + pregapLength,
|
||||
relative=relativeOffset + pregapLength, counter=counter)
|
||||
self.debug('track %d, added index %r',
|
||||
currentTrack.number, currentTrack.getIndex(1))
|
||||
self._index(currentTrack, 1, absoluteOffset, pregapLength)
|
||||
|
||||
# totalLength was added up to the penultimate track
|
||||
self.table.leadout = totalLength + currentLength
|
||||
self.debug('leadout: %r', self.table.leadout)
|
||||
self.debug('parse: leadout: %r', self.table.leadout)
|
||||
|
||||
def message(self, number, message):
|
||||
"""
|
||||
@@ -305,6 +379,10 @@ class TocFile(object, log.Loggable):
|
||||
self._messages.append((number + 1, message))
|
||||
|
||||
def getTrackLength(self, track):
|
||||
"""
|
||||
Returns the length of the given track, from its INDEX 01 to the next
|
||||
track's INDEX 01
|
||||
"""
|
||||
# returns track length in frames, or -1 if can't be determined and
|
||||
# complete file should be assumed
|
||||
# FIXME: this assumes a track can only be in one file; is this true ?
|
||||
@@ -340,13 +418,16 @@ class File:
|
||||
|
||||
def __init__(self, path, start, length):
|
||||
"""
|
||||
@type path: unicode
|
||||
@type path: C{unicode}
|
||||
@type start: C{int}
|
||||
@param start: starting point for the track in this file, in frames
|
||||
@param length: length for the track in this file, in frames
|
||||
"""
|
||||
assert type(path) is unicode, "%r is not unicode" % path
|
||||
|
||||
self.path = path
|
||||
#self.start = start
|
||||
#self.length = length
|
||||
self.start = start
|
||||
self.length = length
|
||||
|
||||
def __repr__(self):
|
||||
return '<File %r>' % (self.path, )
|
||||
|
||||
@@ -64,19 +64,21 @@ class ChecksumException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# example:
|
||||
# ##: 0 [read] @ 24696
|
||||
_PROGRESS_RE = re.compile(r"""
|
||||
^\#\#: (?P<code>.+)\s # function code
|
||||
\[(?P<function>.*)\]\s@\s # function name
|
||||
(?P<offset>\d+) # offset
|
||||
^\#\#: (?P<code>.+)\s # function code
|
||||
\[(?P<function>.*)\]\s@\s # [function name] @
|
||||
(?P<offset>\d+) # offset in words (2-byte one channel value)
|
||||
""", re.VERBOSE)
|
||||
|
||||
_ERROR_RE = re.compile("^scsi_read error:")
|
||||
|
||||
# from reading cdparanoia source code, it looks like offset is reported in
|
||||
# number of single-channel samples, ie. 2 bytes per unit, and absolute
|
||||
# number of single-channel samples, ie. 2 bytes (word) per unit, and absolute
|
||||
|
||||
|
||||
class ProgressParser(object):
|
||||
class ProgressParser(log.Loggable):
|
||||
read = 0 # last [read] frame
|
||||
wrote = 0 # last [wrote] frame
|
||||
errors = 0 # count of number of scsi errors
|
||||
@@ -128,13 +130,15 @@ class ProgressParser(object):
|
||||
# set nframes if not yet set
|
||||
if self._nframes is None and self.read != 0:
|
||||
self._nframes = frameOffset - self.read
|
||||
self.debug('set nframes to %r', self._nframes)
|
||||
|
||||
# set firstFrames if not yet set
|
||||
if self._firstFrames is None:
|
||||
self._firstFrames = frameOffset - self.start
|
||||
self.debug('set firstFrames to %r', self._firstFrames)
|
||||
|
||||
markStart = None
|
||||
markEnd = None
|
||||
markEnd = None # the next unread frame (half-inclusive)
|
||||
|
||||
# verify it either read nframes more or went back for verify
|
||||
if frameOffset > self.read:
|
||||
@@ -165,10 +169,11 @@ class ProgressParser(object):
|
||||
|
||||
# cdparanoia reads quite a bit beyond the current track before it
|
||||
# goes back to verify; don't count those
|
||||
if markEnd > self.stop:
|
||||
markEnd = self.stop
|
||||
if markStart > self.stop:
|
||||
markStart = self.stop
|
||||
# markStart, markEnd of 0, 21 with stop 0 should give 1 read
|
||||
if markEnd > self.stop + 1:
|
||||
markEnd = self.stop + 1
|
||||
if markStart > self.stop + 1:
|
||||
markStart = self.stop + 1
|
||||
|
||||
self.reads += markEnd - markStart
|
||||
|
||||
@@ -185,8 +190,9 @@ class ProgressParser(object):
|
||||
Each frame gets read twice.
|
||||
More than two reads for a frame reduce track quality.
|
||||
"""
|
||||
frames = self.stop - self.start + 1
|
||||
frames = self.stop - self.start + 1 # + 1 since stop is inclusive
|
||||
reads = self.reads
|
||||
self.debug('getTrackQuality: frames %d, reads %d' % (frames, reads))
|
||||
|
||||
# don't go over a 100%; we know cdparanoia reads each frame at least
|
||||
# twice
|
||||
@@ -544,25 +550,12 @@ _VERSION_RE = re.compile(
|
||||
|
||||
|
||||
def getCdParanoiaVersion():
|
||||
version = "(Unknown)"
|
||||
getter = common.VersionGetter('cdparanoia',
|
||||
["cdparanoia", "-V"],
|
||||
_VERSION_RE,
|
||||
"%(version)s %(release)s")
|
||||
|
||||
try:
|
||||
p = asyncsub.Popen(["cdparanoia", "-V"],
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, close_fds=True)
|
||||
version = asyncsub.recv_some(p, e=0, stderr=1)
|
||||
vre = _VERSION_RE.search(version)
|
||||
if vre and len(vre.groups()) == 2:
|
||||
version = "%s %s" % (
|
||||
vre.groupdict().get('version'),
|
||||
vre.groupdict().get('release'))
|
||||
except OSError, e:
|
||||
import errno
|
||||
if e.errno == errno.ENOENT:
|
||||
raise common.MissingDependencyException('cdparanoia')
|
||||
raise
|
||||
|
||||
return version
|
||||
return getter.get()
|
||||
|
||||
|
||||
_OK_RE = re.compile(r'Drive tests OK with Paranoia.')
|
||||
|
||||
@@ -290,7 +290,7 @@ class DiscInfoTask(CDRDAOTask):
|
||||
@param device: the device to rip from
|
||||
@type device: str
|
||||
"""
|
||||
self.debug('creating DiscInfoTask')
|
||||
self.debug('creating DiscInfoTask for device %r', device)
|
||||
CDRDAOTask.__init__(self)
|
||||
|
||||
self.options = ['disk-info', ]
|
||||
@@ -342,6 +342,8 @@ class ReadSessionTask(CDRDAOTask):
|
||||
@param device: the device to rip from
|
||||
@type device: str
|
||||
"""
|
||||
self.debug('Creating ReadSessionTask for session %d on device %r',
|
||||
session, device)
|
||||
CDRDAOTask.__init__(self)
|
||||
self.parser = OutputParser(self)
|
||||
(fd, self._tocfilepath) = tempfile.mkstemp(
|
||||
@@ -505,3 +507,16 @@ class ProgramFailedException(Exception):
|
||||
def __init__(self, code):
|
||||
self.code = code
|
||||
self.args = (code, )
|
||||
|
||||
|
||||
_VERSION_RE = re.compile(
|
||||
"^Cdrdao version (?P<version>.+) -")
|
||||
|
||||
|
||||
def getCDRDAOVersion():
|
||||
getter = common.VersionGetter('cdrdao',
|
||||
["cdrdao"],
|
||||
_VERSION_RE,
|
||||
"%(version)s")
|
||||
|
||||
return getter.get()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
111
morituri/test/cdparanoia.progress.strokes
Normal file
111
morituri/test/cdparanoia.progress.strokes
Normal file
@@ -0,0 +1,111 @@
|
||||
Sending all callbacks to stderr for wrapper script
|
||||
cdparanoia III release 10.2 (September 11, 2008)
|
||||
|
||||
Ripping from sector 0 (track 0 [0:00.00])
|
||||
to sector 0 (track 0 [0:00.00])
|
||||
|
||||
outputting to cdda.wav
|
||||
|
||||
##: 0 [read] @ 24696
|
||||
##: 0 [read] @ 56448
|
||||
##: 0 [read] @ 88200
|
||||
##: 0 [read] @ 119952
|
||||
##: 0 [read] @ 151704
|
||||
##: 0 [read] @ 183456
|
||||
##: 0 [read] @ 215208
|
||||
##: 0 [read] @ 246960
|
||||
##: 0 [read] @ 278712
|
||||
##: 0 [read] @ 310464
|
||||
##: 0 [read] @ 342216
|
||||
##: 0 [read] @ 373968
|
||||
##: 0 [read] @ 405720
|
||||
##: 0 [read] @ 437472
|
||||
##: 0 [read] @ 469224
|
||||
##: 0 [read] @ 500976
|
||||
##: 0 [read] @ 532728
|
||||
##: 0 [read] @ 564480
|
||||
##: 0 [read] @ 596232
|
||||
##: 0 [read] @ 627984
|
||||
##: 0 [read] @ 659736
|
||||
##: 0 [read] @ 691488
|
||||
##: 0 [read] @ 723240
|
||||
##: 0 [read] @ 754992
|
||||
##: 0 [read] @ 786744
|
||||
##: 0 [read] @ 818496
|
||||
##: 0 [read] @ 850248
|
||||
##: 0 [read] @ 882000
|
||||
##: 0 [read] @ 913752
|
||||
##: 0 [read] @ 945504
|
||||
##: 0 [read] @ 977256
|
||||
##: 0 [read] @ 1009008
|
||||
##: 0 [read] @ 1040760
|
||||
##: 0 [read] @ 1072512
|
||||
##: 0 [read] @ 1104264
|
||||
##: 0 [read] @ 1136016
|
||||
##: 0 [read] @ 1167768
|
||||
##: 0 [read] @ 1199520
|
||||
##: 0 [read] @ 1231272
|
||||
##: 0 [read] @ 1263024
|
||||
##: 0 [read] @ 1294776
|
||||
##: 0 [read] @ 1326528
|
||||
##: 0 [read] @ 1358280
|
||||
##: 0 [read] @ 1390032
|
||||
##: 0 [read] @ 1410024
|
||||
##: 0 [read] @ 23520
|
||||
##: 0 [read] @ 55272
|
||||
##: 0 [read] @ 87024
|
||||
##: 0 [read] @ 118776
|
||||
##: 0 [read] @ 150528
|
||||
##: 0 [read] @ 182280
|
||||
##: 0 [read] @ 214032
|
||||
##: 0 [read] @ 245784
|
||||
##: 0 [read] @ 277536
|
||||
##: 0 [read] @ 309288
|
||||
##: 0 [read] @ 341040
|
||||
##: 0 [read] @ 372792
|
||||
##: 0 [read] @ 404544
|
||||
##: 0 [read] @ 436296
|
||||
##: 0 [read] @ 468048
|
||||
##: 0 [read] @ 499800
|
||||
##: 0 [read] @ 531552
|
||||
##: 0 [read] @ 563304
|
||||
##: 0 [read] @ 595056
|
||||
##: 0 [read] @ 626808
|
||||
##: 0 [read] @ 658560
|
||||
##: 0 [read] @ 690312
|
||||
##: 0 [read] @ 722064
|
||||
##: 0 [read] @ 753816
|
||||
##: 0 [read] @ 785568
|
||||
##: 0 [read] @ 817320
|
||||
##: 0 [read] @ 849072
|
||||
##: 0 [read] @ 880824
|
||||
##: 0 [read] @ 912576
|
||||
##: 0 [read] @ 944328
|
||||
##: 0 [read] @ 976080
|
||||
##: 0 [read] @ 1007832
|
||||
##: 0 [read] @ 1039584
|
||||
##: 0 [read] @ 1071336
|
||||
##: 0 [read] @ 1103088
|
||||
##: 0 [read] @ 1134840
|
||||
##: 0 [read] @ 1166592
|
||||
##: 0 [read] @ 1198344
|
||||
##: 0 [read] @ 1230096
|
||||
##: 0 [read] @ 1261848
|
||||
##: 0 [read] @ 1293600
|
||||
##: 0 [read] @ 1325352
|
||||
##: 0 [read] @ 1357104
|
||||
##: 0 [read] @ 1388856
|
||||
##: 0 [read] @ 1410024
|
||||
##: 1 [verify] @ 0
|
||||
##: 3 [correction] @ 1005459
|
||||
##: 3 [correction] @ 1005480
|
||||
##: 1 [verify] @ 1005480
|
||||
##: 1 [verify] @ 1005480
|
||||
##: -2 [wrote] @ 1175
|
||||
##: -2 [wrote] @ 1176
|
||||
##: -1 [finished] @ 1175
|
||||
|
||||
|
||||
Done.
|
||||
|
||||
|
||||
@@ -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')]
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"release": {"status": "Official", "asin": "B008R78K1Y", "label-info-list": [{"label": {"sort-name": "Brownswood Recordings", "id": "6483a614-d00f-42b0-af39-a602b3ce5daa", "name": "Brownswood Recordings"}, "catalog-number": "BWOOD090CD"}], "title": "Mala in Cuba", "country": "GB", "barcode": "5060180321505", "artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "medium-list": [{"disc-list": [{"id": "u0aKVpO.59JBy6eQRX2vYcoqQZ0-", "sectors": "257868"}], "position": "1", "track-list": [{"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "155000", "artist-credit-phrase": "Mala", "id": "3fa9c442-6ae7-4242-ae3b-0150a3002da4", "title": "Introduction"}, "position": "1"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "195626", "artist-credit-phrase": "Mala", "id": "983ad5e0-c52e-459d-8828-85718ceff2cc", "title": "Mulata"}, "position": "2"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "242826", "artist-credit-phrase": "Mala", "id": "6855abf0-32a3-4fe2-a3fb-858f3157d42b", "title": "Tribal"}, "position": "3"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "263760", "artist-credit-phrase": "Mala", "id": "2f938885-94ad-4b11-b251-f18c3a2a5fa9", "title": "Changuito"}, "position": "4"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "274520", "artist-credit-phrase": "Mala", "id": "a5ecfa15-06d0-44cf-a28e-c748e8270488", "title": "Revolution"}, "position": "5"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}, " feat. ", {"artist": {"sort-name": "Dreiser", "id": "ec07a209-55ff-4084-bc41-9d4d1764e075", "name": "Dreiser"}}, " & ", {"artist": {"sort-name": "Sexto Sentido", "id": "f626b92e-07b1-4a19-ad13-c09d690db66c", "name": "Sexto Sentido"}}], "length": "227800", "artist-credit-phrase": "Mala feat. Dreiser & Sexto Sentido", "id": "cfb3ddaf-584c-4c86-b58c-752c63977bb8", "title": "Como como"}, "position": "6"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "276693", "artist-credit-phrase": "Mala", "id": "90da8ada-21e2-4e7b-ab46-ff04004a3d84", "title": "Cuba Electronic"}, "position": "7"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "267973", "artist-credit-phrase": "Mala", "id": "2bf67b46-30f5-4746-ab91-4c9675221a21", "title": "The Tunnel"}, "position": "8"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "246000", "artist-credit-phrase": "Mala", "id": "0cd61fa9-a97a-41e3-b3c3-db36f633b611", "title": "Ghost"}, "position": "9"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "250000", "artist-credit-phrase": "Mala", "id": "136989e9-f24f-4872-9026-1487869cc8de", "title": "Curfew"}, "position": "10"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "174000", "artist-credit-phrase": "Mala", "id": "26b6fd89-7021-4239-b6a7-76eca8c0515a", "title": "The Tourist"}, "position": "11"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "270733", "artist-credit-phrase": "Mala", "id": "62f7a892-f63b-4a2b-866f-db2a36533f8c", "title": "Change"}, "position": "12"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "251853", "artist-credit-phrase": "Mala", "id": "4395c91a-d5e9-4fe4-92d2-deee3e0ebb5a", "title": "Calle F"}, "position": "13"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}, " feat. ", {"artist": {"sort-name": "Suarez, Danay", "id": "82f04998-7da8-4259-aa7f-d623e6ea2b91", "name": "Danay Suarez"}}], "length": "338000", "artist-credit-phrase": "Mala feat. Danay Suarez", "id": "e47a4fd9-8359-4a33-add8-e8c690e59055", "title": "Noche sue\u00f1os"}, "position": "14"}], "format": "CD"}], "text-representation": {"language": "eng", "script": "Latn"}, "date": "2012-09-17", "artist-credit-phrase": "Mala", "quality": "normal", "id": "61c6fd9b-18f8-4a45-963a-ba3c5d990cae"}}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
13
morituri/test/strokes-someday.eac.cue
Normal file
13
morituri/test/strokes-someday.eac.cue
Normal file
@@ -0,0 +1,13 @@
|
||||
REM GENRE "Alternative Rock"
|
||||
REM DATE 2001
|
||||
REM DISCID 0200BA01
|
||||
REM COMMENT "ExactAudioCopy v0.99pb4"
|
||||
PERFORMER "The Strokes"
|
||||
TITLE "Someday"
|
||||
FILE "The Strokes - Someday\01 - The Strokes - Someday.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
TITLE "Someday"
|
||||
PERFORMER "The Strokes"
|
||||
FLAGS DCP
|
||||
PREGAP 00:00:01
|
||||
INDEX 01 00:00:00
|
||||
12
morituri/test/strokes-someday.toc
Normal file
12
morituri/test/strokes-someday.toc
Normal file
@@ -0,0 +1,12 @@
|
||||
CD_DA
|
||||
|
||||
|
||||
// Track 1
|
||||
TRACK AUDIO
|
||||
COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
SILENCE 00:00:01
|
||||
FILE "data.wav" 0 03:06:59
|
||||
START 00:00:01
|
||||
|
||||
136
morituri/test/surferrosa.eac.corrected.cue
Normal file
136
morituri/test/surferrosa.eac.corrected.cue
Normal file
@@ -0,0 +1,136 @@
|
||||
REM GENRE Alternative
|
||||
REM DATE 1987
|
||||
REM DISCID 350CAA15
|
||||
REM COMMENT "ExactAudioCopy v0.99pb4"
|
||||
CATALOG 0000000000000
|
||||
PERFORMER "Pixies"
|
||||
TITLE "Surfer Rosa & Come on Pilgrim"
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\01 - Pixies - Bone Machine.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
TITLE "Bone Machine"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 00 00:00:00
|
||||
INDEX 01 00:00:32
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\02 - Pixies - Break My Body.wav" WAVE
|
||||
TRACK 02 AUDIO
|
||||
TITLE "Break My Body"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\03 - Pixies - Something Against You.wav" WAVE
|
||||
TRACK 03 AUDIO
|
||||
TITLE "Something Against You"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 00 00:00:00
|
||||
INDEX 01 00:00:45
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\04 - Pixies - Broken Face.wav" WAVE
|
||||
TRACK 04 AUDIO
|
||||
TITLE "Broken Face"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\05 - Pixies - Gigantic.wav" WAVE
|
||||
TRACK 05 AUDIO
|
||||
TITLE "Gigantic"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\06 - Pixies - River Euphrates.wav" WAVE
|
||||
TRACK 06 AUDIO
|
||||
TITLE "River Euphrates"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\07 - Pixies - Where Is My Mind .wav" WAVE
|
||||
TRACK 07 AUDIO
|
||||
TITLE "Where Is My Mind?"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\08 - Pixies - Cactus.wav" WAVE
|
||||
TRACK 08 AUDIO
|
||||
TITLE "Cactus"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\09 - Pixies - Tony's Theme.wav" WAVE
|
||||
TRACK 09 AUDIO
|
||||
TITLE "Tony's Theme"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\10 - Pixies - Oh My Golly!.wav" WAVE
|
||||
TRACK 10 AUDIO
|
||||
TITLE "Oh My Golly!"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\11 - Pixies - Vamos.wav" WAVE
|
||||
TRACK 11 AUDIO
|
||||
TITLE "Vamos"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
INDEX 02 00:44:70
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\12 - Pixies - I'm Amazed.wav" WAVE
|
||||
TRACK 12 AUDIO
|
||||
TITLE "I'm Amazed"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\13 - Pixies - Brick is Red.wav" WAVE
|
||||
TRACK 13 AUDIO
|
||||
TITLE "Brick is Red"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\14 - Pixies - Caribou.wav" WAVE
|
||||
TRACK 14 AUDIO
|
||||
TITLE "Caribou"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\15 - Pixies - Vamos.wav" WAVE
|
||||
TRACK 15 AUDIO
|
||||
TITLE "Vamos"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\16 - Pixies - Isla de Encanta.wav" WAVE
|
||||
TRACK 16 AUDIO
|
||||
TITLE "Isla de Encanta"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\17 - Pixies - Ed is Dead.wav" WAVE
|
||||
TRACK 17 AUDIO
|
||||
TITLE "Ed is Dead"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\18 - Pixies - The Holyday Song.wav" WAVE
|
||||
TRACK 18 AUDIO
|
||||
TITLE "The Holyday Song"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\19 - Pixies - Nimrod's Son.wav" WAVE
|
||||
TRACK 19 AUDIO
|
||||
TITLE "Nimrod's Son"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\20 - Pixies - I've Been Tired.wav" WAVE
|
||||
TRACK 20 AUDIO
|
||||
TITLE "I've Been Tired"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\21 - Pixies - Levitate Me.wav" WAVE
|
||||
TRACK 21 AUDIO
|
||||
TITLE "Levitate Me"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
136
morituri/test/surferrosa.eac.currentgap.cue
Normal file
136
morituri/test/surferrosa.eac.currentgap.cue
Normal file
@@ -0,0 +1,136 @@
|
||||
REM GENRE Alternative
|
||||
REM DATE 1987
|
||||
REM DISCID 350CAA15
|
||||
REM COMMENT "ExactAudioCopy v0.99pb4"
|
||||
CATALOG 0000000000000
|
||||
PERFORMER "Pixies"
|
||||
TITLE "Surfer Rosa & Come on Pilgrim"
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\01 - Pixies - Bone Machine.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
TITLE "Bone Machine"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
PREGAP 00:00:32
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\02 - Pixies - Break My Body.wav" WAVE
|
||||
TRACK 02 AUDIO
|
||||
TITLE "Break My Body"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
TRACK 03 AUDIO
|
||||
TITLE "Something Against You"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 00 02:05:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\03 - Pixies - Something Against You.wav" WAVE
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\04 - Pixies - Broken Face.wav" WAVE
|
||||
TRACK 04 AUDIO
|
||||
TITLE "Broken Face"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\05 - Pixies - Gigantic.wav" WAVE
|
||||
TRACK 05 AUDIO
|
||||
TITLE "Gigantic"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\06 - Pixies - River Euphrates.wav" WAVE
|
||||
TRACK 06 AUDIO
|
||||
TITLE "River Euphrates"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\07 - Pixies - Where Is My Mind .wav" WAVE
|
||||
TRACK 07 AUDIO
|
||||
TITLE "Where Is My Mind?"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\08 - Pixies - Cactus.wav" WAVE
|
||||
TRACK 08 AUDIO
|
||||
TITLE "Cactus"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\09 - Pixies - Tony's Theme.wav" WAVE
|
||||
TRACK 09 AUDIO
|
||||
TITLE "Tony's Theme"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\10 - Pixies - Oh My Golly!.wav" WAVE
|
||||
TRACK 10 AUDIO
|
||||
TITLE "Oh My Golly!"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\11 - Pixies - Vamos.wav" WAVE
|
||||
TRACK 11 AUDIO
|
||||
TITLE "Vamos"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
INDEX 02 00:44:70
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\12 - Pixies - I'm Amazed.wav" WAVE
|
||||
TRACK 12 AUDIO
|
||||
TITLE "I'm Amazed"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\13 - Pixies - Brick is Red.wav" WAVE
|
||||
TRACK 13 AUDIO
|
||||
TITLE "Brick is Red"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\14 - Pixies - Caribou.wav" WAVE
|
||||
TRACK 14 AUDIO
|
||||
TITLE "Caribou"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\15 - Pixies - Vamos.wav" WAVE
|
||||
TRACK 15 AUDIO
|
||||
TITLE "Vamos"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\16 - Pixies - Isla de Encanta.wav" WAVE
|
||||
TRACK 16 AUDIO
|
||||
TITLE "Isla de Encanta"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\17 - Pixies - Ed is Dead.wav" WAVE
|
||||
TRACK 17 AUDIO
|
||||
TITLE "Ed is Dead"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\18 - Pixies - The Holyday Song.wav" WAVE
|
||||
TRACK 18 AUDIO
|
||||
TITLE "The Holyday Song"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\19 - Pixies - Nimrod's Son.wav" WAVE
|
||||
TRACK 19 AUDIO
|
||||
TITLE "Nimrod's Son"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\20 - Pixies - I've Been Tired.wav" WAVE
|
||||
TRACK 20 AUDIO
|
||||
TITLE "I've Been Tired"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\21 - Pixies - Levitate Me.wav" WAVE
|
||||
TRACK 21 AUDIO
|
||||
TITLE "Levitate Me"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
136
morituri/test/surferrosa.eac.leftout.cue
Normal file
136
morituri/test/surferrosa.eac.leftout.cue
Normal file
@@ -0,0 +1,136 @@
|
||||
REM GENRE Alternative
|
||||
REM DATE 1987
|
||||
REM DISCID 350CAA15
|
||||
REM COMMENT "ExactAudioCopy v0.99pb4"
|
||||
CATALOG 0000000000000
|
||||
PERFORMER "Pixies"
|
||||
TITLE "Surfer Rosa & Come on Pilgrim"
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\01 - Pixies - Bone Machine.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
TITLE "Bone Machine"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
PREGAP 00:00:32
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\02 - Pixies - Break My Body.wav" WAVE
|
||||
TRACK 02 AUDIO
|
||||
TITLE "Break My Body"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\03 - Pixies - Something Against You.wav" WAVE
|
||||
TRACK 03 AUDIO
|
||||
TITLE "Something Against You"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
PREGAP 00:00:45
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\04 - Pixies - Broken Face.wav" WAVE
|
||||
TRACK 04 AUDIO
|
||||
TITLE "Broken Face"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\05 - Pixies - Gigantic.wav" WAVE
|
||||
TRACK 05 AUDIO
|
||||
TITLE "Gigantic"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\06 - Pixies - River Euphrates.wav" WAVE
|
||||
TRACK 06 AUDIO
|
||||
TITLE "River Euphrates"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\07 - Pixies - Where Is My Mind .wav" WAVE
|
||||
TRACK 07 AUDIO
|
||||
TITLE "Where Is My Mind?"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\08 - Pixies - Cactus.wav" WAVE
|
||||
TRACK 08 AUDIO
|
||||
TITLE "Cactus"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\09 - Pixies - Tony's Theme.wav" WAVE
|
||||
TRACK 09 AUDIO
|
||||
TITLE "Tony's Theme"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\10 - Pixies - Oh My Golly!.wav" WAVE
|
||||
TRACK 10 AUDIO
|
||||
TITLE "Oh My Golly!"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\11 - Pixies - Vamos.wav" WAVE
|
||||
TRACK 11 AUDIO
|
||||
TITLE "Vamos"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
INDEX 02 00:44:70
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\12 - Pixies - I'm Amazed.wav" WAVE
|
||||
TRACK 12 AUDIO
|
||||
TITLE "I'm Amazed"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\13 - Pixies - Brick is Red.wav" WAVE
|
||||
TRACK 13 AUDIO
|
||||
TITLE "Brick is Red"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\14 - Pixies - Caribou.wav" WAVE
|
||||
TRACK 14 AUDIO
|
||||
TITLE "Caribou"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\15 - Pixies - Vamos.wav" WAVE
|
||||
TRACK 15 AUDIO
|
||||
TITLE "Vamos"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\16 - Pixies - Isla de Encanta.wav" WAVE
|
||||
TRACK 16 AUDIO
|
||||
TITLE "Isla de Encanta"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\17 - Pixies - Ed is Dead.wav" WAVE
|
||||
TRACK 17 AUDIO
|
||||
TITLE "Ed is Dead"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\18 - Pixies - The Holyday Song.wav" WAVE
|
||||
TRACK 18 AUDIO
|
||||
TITLE "The Holyday Song"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\19 - Pixies - Nimrod's Son.wav" WAVE
|
||||
TRACK 19 AUDIO
|
||||
TITLE "Nimrod's Son"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\20 - Pixies - I've Been Tired.wav" WAVE
|
||||
TRACK 20 AUDIO
|
||||
TITLE "I've Been Tired"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\21 - Pixies - Levitate Me.wav" WAVE
|
||||
TRACK 21 AUDIO
|
||||
TITLE "Levitate Me"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
136
morituri/test/surferrosa.eac.noncompliant.cue
Normal file
136
morituri/test/surferrosa.eac.noncompliant.cue
Normal file
@@ -0,0 +1,136 @@
|
||||
REM GENRE Alternative
|
||||
REM DATE 1987
|
||||
REM DISCID 350CAA15
|
||||
REM COMMENT "ExactAudioCopy v0.99pb4"
|
||||
CATALOG 0000000000000
|
||||
PERFORMER "Pixies"
|
||||
TITLE "Surfer Rosa & Come on Pilgrim"
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\01 - Pixies - Bone Machine.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
TITLE "Bone Machine"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
PREGAP 00:00:32
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\02 - Pixies - Break My Body.wav" WAVE
|
||||
TRACK 02 AUDIO
|
||||
TITLE "Break My Body"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
TRACK 03 AUDIO
|
||||
TITLE "Something Against You"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 00 02:05:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\03 - Pixies - Something Against You.wav" WAVE
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\04 - Pixies - Broken Face.wav" WAVE
|
||||
TRACK 04 AUDIO
|
||||
TITLE "Broken Face"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\05 - Pixies - Gigantic.wav" WAVE
|
||||
TRACK 05 AUDIO
|
||||
TITLE "Gigantic"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\06 - Pixies - River Euphrates.wav" WAVE
|
||||
TRACK 06 AUDIO
|
||||
TITLE "River Euphrates"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\07 - Pixies - Where Is My Mind .wav" WAVE
|
||||
TRACK 07 AUDIO
|
||||
TITLE "Where Is My Mind?"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\08 - Pixies - Cactus.wav" WAVE
|
||||
TRACK 08 AUDIO
|
||||
TITLE "Cactus"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\09 - Pixies - Tony's Theme.wav" WAVE
|
||||
TRACK 09 AUDIO
|
||||
TITLE "Tony's Theme"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\10 - Pixies - Oh My Golly!.wav" WAVE
|
||||
TRACK 10 AUDIO
|
||||
TITLE "Oh My Golly!"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\11 - Pixies - Vamos.wav" WAVE
|
||||
TRACK 11 AUDIO
|
||||
TITLE "Vamos"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
INDEX 02 00:44:70
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\12 - Pixies - I'm Amazed.wav" WAVE
|
||||
TRACK 12 AUDIO
|
||||
TITLE "I'm Amazed"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\13 - Pixies - Brick is Red.wav" WAVE
|
||||
TRACK 13 AUDIO
|
||||
TITLE "Brick is Red"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\14 - Pixies - Caribou.wav" WAVE
|
||||
TRACK 14 AUDIO
|
||||
TITLE "Caribou"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\15 - Pixies - Vamos.wav" WAVE
|
||||
TRACK 15 AUDIO
|
||||
TITLE "Vamos"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\16 - Pixies - Isla de Encanta.wav" WAVE
|
||||
TRACK 16 AUDIO
|
||||
TITLE "Isla de Encanta"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\17 - Pixies - Ed is Dead.wav" WAVE
|
||||
TRACK 17 AUDIO
|
||||
TITLE "Ed is Dead"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\18 - Pixies - The Holyday Song.wav" WAVE
|
||||
TRACK 18 AUDIO
|
||||
TITLE "The Holyday Song"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\19 - Pixies - Nimrod's Son.wav" WAVE
|
||||
TRACK 19 AUDIO
|
||||
TITLE "Nimrod's Son"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\20 - Pixies - I've Been Tired.wav" WAVE
|
||||
TRACK 20 AUDIO
|
||||
TITLE "I've Been Tired"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\21 - Pixies - Levitate Me.wav" WAVE
|
||||
TRACK 21 AUDIO
|
||||
TITLE "Levitate Me"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
116
morituri/test/surferrosa.eac.single.cue
Normal file
116
morituri/test/surferrosa.eac.single.cue
Normal file
@@ -0,0 +1,116 @@
|
||||
REM GENRE Alternative
|
||||
REM DATE 1987
|
||||
REM DISCID 350CAA15
|
||||
REM COMMENT "ExactAudioCopy v0.99pb4"
|
||||
CATALOG 0000000000000
|
||||
PERFORMER "Pixies"
|
||||
TITLE "Surfer Rosa & Come on Pilgrim"
|
||||
FILE "Range.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
TITLE "Bone Machine"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 00 00:00:00
|
||||
INDEX 01 00:00:32
|
||||
TRACK 02 AUDIO
|
||||
TITLE "Break My Body"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 03:03:42
|
||||
TRACK 03 AUDIO
|
||||
TITLE "Something Against You"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 00 05:08:42
|
||||
INDEX 01 05:09:12
|
||||
TRACK 04 AUDIO
|
||||
TITLE "Broken Face"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 06:56:67
|
||||
TRACK 05 AUDIO
|
||||
TITLE "Gigantic"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 08:27:00
|
||||
TRACK 06 AUDIO
|
||||
TITLE "River Euphrates"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 12:21:70
|
||||
TRACK 07 AUDIO
|
||||
TITLE "Where Is My Mind?"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 14:53:60
|
||||
TRACK 08 AUDIO
|
||||
TITLE "Cactus"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 18:47:15
|
||||
TRACK 09 AUDIO
|
||||
TITLE "Tony's Theme"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 21:03:70
|
||||
TRACK 10 AUDIO
|
||||
TITLE "Oh My Golly!"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 22:56:15
|
||||
TRACK 11 AUDIO
|
||||
TITLE "Vamos"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 24:43:32
|
||||
INDEX 02 25:28:27
|
||||
TRACK 12 AUDIO
|
||||
TITLE "I'm Amazed"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 29:49:20
|
||||
TRACK 13 AUDIO
|
||||
TITLE "Brick is Red"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 31:31:27
|
||||
TRACK 14 AUDIO
|
||||
TITLE "Caribou"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 33:32:20
|
||||
TRACK 15 AUDIO
|
||||
TITLE "Vamos"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 36:46:45
|
||||
TRACK 16 AUDIO
|
||||
TITLE "Isla de Encanta"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 39:40:22
|
||||
TRACK 17 AUDIO
|
||||
TITLE "Ed is Dead"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 41:21:47
|
||||
TRACK 18 AUDIO
|
||||
TITLE "The Holyday Song"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 43:51:47
|
||||
TRACK 19 AUDIO
|
||||
TITLE "Nimrod's Son"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 46:06:10
|
||||
TRACK 20 AUDIO
|
||||
TITLE "I've Been Tired"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 48:23:25
|
||||
TRACK 21 AUDIO
|
||||
TITLE "Levitate Me"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 51:24:07
|
||||
196
morituri/test/surferrosa.toc
Normal file
196
morituri/test/surferrosa.toc
Normal file
@@ -0,0 +1,196 @@
|
||||
CD_DA
|
||||
|
||||
CATALOG "0000000000000"
|
||||
|
||||
// Track 1
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
SILENCE 00:00:32
|
||||
FILE "data.wav" 0 03:03:10
|
||||
START 00:00:32
|
||||
|
||||
|
||||
// Track 2
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 03:03:10 02:05:00
|
||||
|
||||
|
||||
// Track 3
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 05:08:10 01:48:25
|
||||
START 00:00:45
|
||||
|
||||
|
||||
// Track 4
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 06:56:35 01:30:08
|
||||
|
||||
|
||||
// Track 5
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 08:26:43 03:54:70
|
||||
|
||||
|
||||
// Track 6
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 12:21:38 02:31:65
|
||||
|
||||
|
||||
// Track 7
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 14:53:28 03:53:30
|
||||
|
||||
|
||||
// Track 8
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 18:46:58 02:16:55
|
||||
|
||||
|
||||
// Track 9
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 21:03:38 01:52:20
|
||||
|
||||
|
||||
// Track 10
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 22:55:58 01:47:17
|
||||
|
||||
|
||||
// Track 11
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 24:43:00 05:05:63
|
||||
INDEX 00:44:70
|
||||
|
||||
|
||||
// Track 12
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 29:48:63 01:42:07
|
||||
|
||||
|
||||
// Track 13
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 31:30:70 02:00:68
|
||||
|
||||
|
||||
// Track 14
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 33:31:63 03:14:25
|
||||
|
||||
|
||||
// Track 15
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 36:46:13 02:53:52
|
||||
|
||||
|
||||
// Track 16
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 39:39:65 01:41:25
|
||||
|
||||
|
||||
// Track 17
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 41:21:15 02:30:00
|
||||
|
||||
|
||||
// Track 18
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 43:51:15 02:14:38
|
||||
|
||||
|
||||
// Track 19
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 46:05:53 02:17:15
|
||||
|
||||
|
||||
// Track 20
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 48:22:68 03:00:57
|
||||
|
||||
|
||||
// Track 21
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 51:23:50 02:38:38
|
||||
|
||||
@@ -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)
|
||||
|
||||
21
morituri/test/test_common_directory.py
Normal file
21
morituri/test/test_common_directory.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- Mode: Python; test-case-name: morituri.test.test_common_directory -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
from morituri.common import directory
|
||||
|
||||
from morituri.test import common
|
||||
|
||||
|
||||
class DirectoryTestCase(common.TestCase):
|
||||
|
||||
def testAll(self):
|
||||
d = directory.Directory()
|
||||
|
||||
path = d.getConfig()
|
||||
self.failUnless(path.startswith('/home'))
|
||||
|
||||
path = d.getCache()
|
||||
self.failUnless(path.startswith('/home'))
|
||||
|
||||
paths = d.getReadCaches()
|
||||
self.failUnless(paths[0].startswith('/home'))
|
||||
118
morituri/test/test_common_mbngs.py
Normal file
118
morituri/test/test_common_mbngs.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# -*- Mode: Python; test-case-name: morituri.test.test_common_mbngs -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
import json
|
||||
|
||||
import unittest
|
||||
|
||||
from morituri.common import mbngs
|
||||
|
||||
|
||||
class MetadataTestCase(unittest.TestCase):
|
||||
|
||||
# Generated with rip -R cd info
|
||||
def testJeffEverybodySingle(self):
|
||||
path = os.path.join(os.path.dirname(__file__),
|
||||
'morituri.release.3451f29c-9bb8-4cc5-bfcc-bd50104b94f8.json')
|
||||
handle = open(path, "rb")
|
||||
response = json.loads(handle.read())
|
||||
handle.close()
|
||||
discid = "wbjbST2jUHRZaB1inCyxxsL7Eqc-"
|
||||
|
||||
metadata = mbngs._getMetadata({}, response['release'], discid)
|
||||
|
||||
self.failIf(metadata.release)
|
||||
|
||||
def test2MeterSessies10(self):
|
||||
# various artists, multiple artists per track
|
||||
path = os.path.join(os.path.dirname(__file__),
|
||||
'morituri.release.a76714e0-32b1-4ed4-b28e-f86d99642193.json')
|
||||
handle = open(path, "rb")
|
||||
response = json.loads(handle.read())
|
||||
handle.close()
|
||||
discid = "f7XO36a7n1LCCskkCiulReWbwZA-"
|
||||
|
||||
metadata = mbngs._getMetadata({}, response['release'], discid)
|
||||
|
||||
self.assertEquals(metadata.artist, u'Various Artists')
|
||||
self.assertEquals(metadata.release, u'2001-10-15')
|
||||
self.assertEquals(metadata.mbidArtist,
|
||||
u'89ad4ac3-39f7-470e-963a-56509c546377')
|
||||
|
||||
self.assertEquals(len(metadata.tracks), 18)
|
||||
|
||||
track16 = metadata.tracks[15]
|
||||
|
||||
self.assertEquals(track16.artist, 'Tom Jones & Stereophonics')
|
||||
self.assertEquals(track16.mbidArtist,
|
||||
u'57c6f649-6cde-48a7-8114-2a200247601a'
|
||||
';0bfba3d3-6a04-4779-bb0a-df07df5b0558'
|
||||
)
|
||||
self.assertEquals(track16.sortName,
|
||||
u'Jones, Tom & Stereophonics')
|
||||
|
||||
def testBalladOfTheBrokenSeas(self):
|
||||
# various artists disc
|
||||
path = os.path.join(os.path.dirname(__file__),
|
||||
'morituri.release.e32ae79a-336e-4d33-945c-8c5e8206dbd3.json')
|
||||
handle = open(path, "rb")
|
||||
response = json.loads(handle.read())
|
||||
handle.close()
|
||||
discid = "xAq8L4ELMW14.6wI6tt7QAcxiDI-"
|
||||
|
||||
metadata = mbngs._getMetadata({}, response['release'], discid)
|
||||
|
||||
self.assertEquals(metadata.artist, u'Isobel Campbell & Mark Lanegan')
|
||||
self.assertEquals(metadata.sortName,
|
||||
u'Campbell, Isobel & Lanegan, Mark')
|
||||
self.assertEquals(metadata.release, u'2006-01-30')
|
||||
self.assertEquals(metadata.mbidArtist,
|
||||
u'd51f3a15-12a2-41a0-acfa-33b5eae71164;'
|
||||
'a9126556-f555-4920-9617-6e013f8228a7')
|
||||
|
||||
self.assertEquals(len(metadata.tracks), 12)
|
||||
|
||||
track12 = metadata.tracks[11]
|
||||
|
||||
self.assertEquals(track12.artist, u'Isobel Campbell & Mark Lanegan')
|
||||
self.assertEquals(track12.sortName,
|
||||
u'Campbell, Isobel'
|
||||
' & Lanegan, Mark'
|
||||
)
|
||||
self.assertEquals(track12.mbidArtist,
|
||||
u'd51f3a15-12a2-41a0-acfa-33b5eae71164;'
|
||||
'a9126556-f555-4920-9617-6e013f8228a7')
|
||||
|
||||
def testMalaInCuba(self):
|
||||
# single artist disc, but with multiple artists tracks
|
||||
# see https://github.com/thomasvs/morituri/issues/19
|
||||
path = os.path.join(os.path.dirname(__file__),
|
||||
'morituri.release.61c6fd9b-18f8-4a45-963a-ba3c5d990cae.json')
|
||||
handle = open(path, "rb")
|
||||
response = json.loads(handle.read())
|
||||
handle.close()
|
||||
discid = "u0aKVpO.59JBy6eQRX2vYcoqQZ0-"
|
||||
|
||||
metadata = mbngs._getMetadata({}, response['release'], discid)
|
||||
|
||||
self.assertEquals(metadata.artist, u'Mala')
|
||||
self.assertEquals(metadata.sortName, u'Mala')
|
||||
self.assertEquals(metadata.release, u'2012-09-17')
|
||||
self.assertEquals(metadata.mbidArtist,
|
||||
u'09f221eb-c97e-4da5-ac22-d7ab7c555bbb')
|
||||
|
||||
self.assertEquals(len(metadata.tracks), 14)
|
||||
|
||||
track6 = metadata.tracks[5]
|
||||
|
||||
self.assertEquals(track6.artist, u'Mala feat. Dreiser & Sexto Sentido')
|
||||
self.assertEquals(track6.sortName,
|
||||
u'Mala feat. Dreiser & Sexto Sentido')
|
||||
self.assertEquals(track6.mbidArtist,
|
||||
u'09f221eb-c97e-4da5-ac22-d7ab7c555bbb'
|
||||
';ec07a209-55ff-4084-bc41-9d4d1764e075'
|
||||
';f626b92e-07b1-4a19-ad13-c09d690db66c'
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
30
morituri/test/test_common_path.py
Normal file
30
morituri/test/test_common_path.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# -*- Mode: Python; test-case-name: morituri.test.test_common_path -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
from morituri.common import path
|
||||
|
||||
from morituri.test import common
|
||||
|
||||
|
||||
class FilterTestCase(common.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self._filter = path.PathFilter(special=True)
|
||||
|
||||
def testSlash(self):
|
||||
part = u'A Charm/A Blade'
|
||||
self.assertEquals(self._filter.filter(part), u'A Charm-A Blade')
|
||||
|
||||
def testFat(self):
|
||||
part = u'A Word: F**k you?'
|
||||
self.assertEquals(self._filter.filter(part), u'A Word - F__k you_')
|
||||
|
||||
def testSpecial(self):
|
||||
part = u'<<< $&*!\' "()`{}[]spaceship>>>'
|
||||
self.assertEquals(self._filter.filter(part),
|
||||
u'___ _____ ________spaceship___')
|
||||
|
||||
def testGreatest(self):
|
||||
part = u'Greatest Ever! Soul: The Definitive Collection'
|
||||
self.assertEquals(self._filter.filter(part),
|
||||
u'Greatest Ever_ Soul - The Definitive Collection')
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]))
|
||||
|
||||
Reference in New Issue
Block a user