argparse & logging (#92)
* introduce logcommand.Lager, Whipper(); use argparse for whipper image commands, stub logging * update Lager docstring to mention config.Config() * make incorrect subcommand and --version work on toplevel command * migrate accurip show, expand Lager, do not attempt to return from Lager.__init__. * migrate offset find, add Lager.error * correct offset find drive symlink handling * migrate drive * change Lager.__init__(prog) to arg from kwarg * but actually * remove Whipper.usage * add and use Lager.device_option() context manager * help I married an axe murderer * use unified options namespace for entire command tree * migrate whipper cd without comprehensive config loading * switch to logging module - use logging instead of flog for non-extern modules - use WHIPPER_DEBUG and WHIPPER_LOGFILE env variables * convert self.log calls to logger.debug * convert self.error calls to logger.error * remove log.Loggable, use logger not logging * Logging conversion continues - Convert log.* calls to logger.* - Remove morituri.common.log imports * remove morituri.common.log from tests * remove extern/flog, bare minimum Debug conversion * update README for logging changes * update soxi to use logging * refactor Lager for more declarative subcommands * Refactor Lager.device_option: - inline into __init__ - throw IOError instead of Exception for missing drives - remove CommandError checking in rip/main * rename rip to whipper in rip.main * convert rip.debug commands * Rename logcommand.Lager to command.BaseCommand - remove command.CommandError occurrences - remove python-command external module * remove submodules from README, update rclog formatter * update minor ambiguity in readme for command invocation * update version number to match setup.py * remove gitmodules * update version number in tests as well (boo) * convert logger.error to logger.critical * Change morituri.rip to morituri.command - mv common.command to command.basecommand - move TEMPLATES used only by rip.cd out of rip.common - update entry point for command to command.main * update basecommand documentation * go pyflaking: import fixing * replace self.stdout with sys.stdout * remove BaseCommand.config, alphabetise imports * convert self.stdXXX leftovers * convert last getRootCommand to config.Config * convert last getExceptionMessage's to str * change musicbrainz useragent to whipper
This commit is contained in:
committed by
JoeLametta
parent
8f4607de4c
commit
d1ed80d62a
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -1,6 +0,0 @@
|
||||
[submodule "morituri/extern/python-command"]
|
||||
path = morituri/extern/python-command
|
||||
url = git://github.com/thomasvs/python-command.git
|
||||
[submodule "morituri/extern/flog"]
|
||||
path = morituri/extern/flog
|
||||
url = git://github.com/Flumotion/flog
|
||||
11
README.md
11
README.md
@@ -81,9 +81,6 @@ Change to a directory where you want to put whipper source code (for example, `$
|
||||
```bash
|
||||
git clone -b master --single-branch https://github.com/JoeLametta/whipper.git
|
||||
cd whipper
|
||||
# fetch bundled python dependencies
|
||||
git submodule init
|
||||
git submodule update
|
||||
```
|
||||
|
||||
### Building the bundled dependencies
|
||||
@@ -114,7 +111,7 @@ is correct, while
|
||||
|
||||
`whipper cd rip -d (device)`
|
||||
|
||||
is not, because the `-d` argument applies to the whipper command.
|
||||
is not, because the `-d` argument applies to the `cd` command.
|
||||
|
||||
~~Check the man page (`whipper(1)`) for more information.~~ (currently not available as whipper's documentation is planned to be reworked ([Issue #73](https://github.com/JoeLametta/whipper/issues/73)).
|
||||
|
||||
@@ -261,15 +258,17 @@ Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
Please use the [issue tracker](https://github.com/JoeLametta/whipper/issues) to report any bugs or to file feature requests.
|
||||
|
||||
When filing bug reports, please run the failing command with the environment variable `RIP_DEBUG` set. For example:
|
||||
When filing bug reports, please run the failing command with the environment variable `WHIPPER_DEBUG` set. For example:
|
||||
|
||||
```bash
|
||||
RIP_DEBUG=5 whipper offset find > whipper.log 2>&1
|
||||
WHIPPER_DEBUG=DEBUG WHIPPER_LOGFILE=whipper.log whipper offset find
|
||||
gzip whipper.log
|
||||
```
|
||||
|
||||
And attach the gzipped log file to your bug report.
|
||||
|
||||
Without `WHIPPER_LOGFILE` set, logging messages will go to stderr. `WHIPPER_DEBUG` accepts a string of the [default python logging levels](https://docs.python.org/2/library/logging.html#logging-levels).
|
||||
|
||||
### Developing
|
||||
|
||||
Pull requests are welcome.
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
level = logging.WARNING
|
||||
if 'WHIPPER_DEBUG' in os.environ:
|
||||
level = os.environ['WHIPPER_DEBUG'].upper()
|
||||
if 'WHIPPER_LOGFILE' in os.environ:
|
||||
logging.basicConfig(filename=os.environ['WHIPPER_LOGFILE'],
|
||||
filemode='w', level=level)
|
||||
else:
|
||||
logging.basicConfig(stream=sys.stderr, level=level)
|
||||
|
||||
@@ -20,39 +20,44 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with morituri. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from morituri.common import logcommand, accurip
|
||||
import sys
|
||||
|
||||
from morituri.command.basecommand import BaseCommand
|
||||
from morituri.common import accurip
|
||||
|
||||
class Show(logcommand.LogCommand):
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Show(BaseCommand):
|
||||
summary = "show accuraterip data"
|
||||
description = """
|
||||
retrieves and display accuraterip data from the given URL
|
||||
"""
|
||||
|
||||
def do(self, args):
|
||||
|
||||
try:
|
||||
url = args[0]
|
||||
except IndexError:
|
||||
self.stdout.write('Please specify an accuraterip URL.\n')
|
||||
return 3
|
||||
def add_arguments(self):
|
||||
self.parser.add_argument('url', action='store',
|
||||
help="accuraterip URL to load data from")
|
||||
|
||||
def do(self):
|
||||
url = self.options.url
|
||||
cache = accurip.AccuCache()
|
||||
responses = cache.retrieve(url)
|
||||
|
||||
count = responses[0].trackCount
|
||||
|
||||
self.stdout.write("Found %d responses for %d tracks\n\n" % (
|
||||
sys.stdout.write("Found %d responses for %d tracks\n\n" % (
|
||||
len(responses), count))
|
||||
|
||||
for (i, r) in enumerate(responses):
|
||||
if r.trackCount != count:
|
||||
self.stdout.write(
|
||||
sys.stdout.write(
|
||||
"Warning: response %d has %d tracks instead of %d\n" % (
|
||||
i, r.trackCount, count))
|
||||
|
||||
|
||||
# checksum and confidence by track
|
||||
for track in range(count):
|
||||
self.stdout.write("Track %d:\n" % (track + 1))
|
||||
sys.stdout.write("Track %d:\n" % (track + 1))
|
||||
checksums = {}
|
||||
|
||||
for (i, r) in enumerate(responses):
|
||||
@@ -81,12 +86,17 @@ class Show(logcommand.LogCommand):
|
||||
sortedChecksums.reverse()
|
||||
|
||||
for highest, checksum in sortedChecksums:
|
||||
self.stdout.write(" %d result(s) for checksum %s: %s\n" % (
|
||||
sys.stdout.write(" %d result(s) for checksum %s: %s\n" % (
|
||||
len(checksums[checksum]), checksum,
|
||||
str(checksums[checksum])))
|
||||
|
||||
|
||||
class AccuRip(logcommand.LogCommand):
|
||||
description = "Handle AccurateRip information."
|
||||
|
||||
subCommandClasses = [Show, ]
|
||||
class AccuRip(BaseCommand):
|
||||
summary = "handle AccurateRip information"
|
||||
description = """
|
||||
Handle AccurateRip information. Retrieves AccurateRip disc entries and
|
||||
displays diagnostic information.
|
||||
"""
|
||||
subcommands = {
|
||||
'show': Show
|
||||
}
|
||||
129
morituri/command/basecommand.py
Normal file
129
morituri/command/basecommand.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# -*- Mode: Python -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
from morituri.common import drive
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Q: What about argparse.add_subparsers(), you ask?
|
||||
# A: Unfortunately add_subparsers() does not support specifying the
|
||||
# formatter_class of subparsers, nor does it support epilogs, so
|
||||
# it does not quite fit our use case.
|
||||
|
||||
# Q: Why not subclass ArgumentParser and extend/replace the relevant
|
||||
# methods?
|
||||
# A: If this can be done in a simpler fashion than this current
|
||||
# implementation, by all means submit a patch.
|
||||
|
||||
# Q: Why not argparse.parse_known_args()?
|
||||
# A: The prefix matching prevents passing '-h' (and possibly other
|
||||
# options) to the child command.
|
||||
|
||||
class BaseCommand():
|
||||
"""
|
||||
A base command class for whipper commands.
|
||||
|
||||
Creates an argparse.ArgumentParser.
|
||||
Override add_arguments() and handle_arguments() to register
|
||||
and process arguments before & after argparse.parse_args().
|
||||
|
||||
Provides self.epilog() formatting command for argparse.
|
||||
|
||||
device_option = True adds -d / --device option to current command
|
||||
no_add_help = True removes -h / --help option from current command
|
||||
|
||||
Overriding formatter_class sets the argparse formatter class.
|
||||
|
||||
If the 'subcommands' dictionary is set, __init__ searches the
|
||||
arguments for subcommands.keys() and instantiates the class
|
||||
implementing the subcommand as self.cmd, passing all non-understood
|
||||
arguments, the current options namespace, and the full command path
|
||||
name.
|
||||
"""
|
||||
device_option = False
|
||||
no_add_help = False # for rip.main.Whipper
|
||||
formatter_class = argparse.RawDescriptionHelpFormatter
|
||||
|
||||
def __init__(self, argv, prog_name, opts):
|
||||
self.opts = opts # for Rip.add_arguments()
|
||||
self.prog_name = prog_name
|
||||
|
||||
self.init_parser()
|
||||
self.add_arguments()
|
||||
|
||||
if hasattr(self, 'subcommands'):
|
||||
self.parser.add_argument('remainder',
|
||||
nargs=argparse.REMAINDER,
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
if self.device_option:
|
||||
# pick the first drive as default
|
||||
drives = drive.getAllDevicePaths()
|
||||
if not drives:
|
||||
msg = 'No CD-DA drives found!'
|
||||
logger.critical(msg)
|
||||
# morituri exited with return code 3 here
|
||||
raise IOError(msg)
|
||||
self.parser.add_argument('-d', '--device',
|
||||
action="store",
|
||||
dest="device",
|
||||
default=drives[0],
|
||||
help="CD-DA device")
|
||||
|
||||
self.options = self.parser.parse_args(argv, namespace=opts)
|
||||
|
||||
if self.device_option:
|
||||
# this can be a symlink to another device
|
||||
self.options.device = os.path.realpath(self.options.device)
|
||||
if not os.path.exists(self.options.device):
|
||||
msg = 'CD-DA device %s not found!' % self.options.device
|
||||
logger.critical(msg)
|
||||
raise IOError(msg)
|
||||
|
||||
self.handle_arguments()
|
||||
|
||||
if hasattr(self, 'subcommands'):
|
||||
if not self.options.remainder:
|
||||
self.parser.print_help()
|
||||
sys.exit(0)
|
||||
if not self.options.remainder[0] in self.subcommands:
|
||||
sys.stderr.write("incorrect subcommand: %s" %
|
||||
self.options.remainder[0])
|
||||
sys.exit(1)
|
||||
self.cmd = self.subcommands[self.options.remainder[0]](
|
||||
self.options.remainder[1:],
|
||||
prog_name + " " + self.options.remainder[0],
|
||||
self.options
|
||||
)
|
||||
|
||||
def init_parser(self):
|
||||
kw = {
|
||||
'prog': self.prog_name,
|
||||
'description': self.description,
|
||||
'formatter_class': self.formatter_class,
|
||||
}
|
||||
if hasattr(self, 'subcommands'):
|
||||
kw['epilog'] = self.epilog()
|
||||
if self.no_add_help:
|
||||
kw['add_help'] = False
|
||||
self.parser = argparse.ArgumentParser(**kw)
|
||||
|
||||
def add_arguments(self):
|
||||
pass
|
||||
|
||||
def handle_arguments(self):
|
||||
pass
|
||||
|
||||
def do(self):
|
||||
return self.cmd.do()
|
||||
|
||||
def epilog(self):
|
||||
s = "commands:\n"
|
||||
for com in sorted(self.subcommands.keys()):
|
||||
s += " %s %s\n" % (com.ljust(8), self.subcommands[com].summary)
|
||||
return s
|
||||
@@ -20,29 +20,57 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with morituri. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import math
|
||||
import glob
|
||||
import urllib2
|
||||
import socket
|
||||
import sys
|
||||
|
||||
import gobject
|
||||
gobject.threads_init()
|
||||
|
||||
from morituri.common import logcommand, common, accurip, gstreamer
|
||||
from morituri.common import drive, program, task
|
||||
from morituri.result import result
|
||||
from morituri.command.basecommand import BaseCommand
|
||||
from morituri.common import (
|
||||
accurip, common, config, drive, gstreamer, program, task
|
||||
)
|
||||
from morituri.program import cdrdao, cdparanoia
|
||||
from morituri.rip import common as rcommon
|
||||
from morituri.result import result
|
||||
|
||||
from morituri.extern.command import command
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
SILENT = 1e-10
|
||||
MAX_TRIES = 5
|
||||
|
||||
DEFAULT_TRACK_TEMPLATE = u'%r/%A - %d/%t. %a - %n'
|
||||
DEFAULT_DISC_TEMPLATE = u'%r/%A - %d/%A - %d'
|
||||
|
||||
class _CD(logcommand.LogCommand):
|
||||
TEMPLATE_DESCRIPTION = '''
|
||||
Tracks are named according to the track template, filling in the variables
|
||||
and adding the file extension. Variables exclusive to the track template are:
|
||||
- %t: track number
|
||||
- %a: track artist
|
||||
- %n: track title
|
||||
- %s: track sort name
|
||||
|
||||
Disc files (.cue, .log, .m3u) are named according to the disc template,
|
||||
filling in the variables and adding the file extension. Variables for both
|
||||
disc and track template are:
|
||||
- %A: album artist
|
||||
- %S: album sort name
|
||||
- %d: disc title
|
||||
- %y: release year
|
||||
- %r: release type, lowercase
|
||||
- %R: Release type, normal case
|
||||
- %x: audio extension, lowercase
|
||||
- %X: audio extension, uppercase
|
||||
|
||||
'''
|
||||
|
||||
|
||||
class _CD(BaseCommand):
|
||||
|
||||
"""
|
||||
@type program: L{program.Program}
|
||||
@@ -51,31 +79,34 @@ class _CD(logcommand.LogCommand):
|
||||
|
||||
eject = True
|
||||
|
||||
def addOptions(self):
|
||||
@staticmethod
|
||||
def add_arguments(parser):
|
||||
# FIXME: have a cache of these pickles somewhere
|
||||
self.parser.add_option('-T', '--toc-pickle',
|
||||
parser.add_argument('-T', '--toc-pickle',
|
||||
action="store", dest="toc_pickle",
|
||||
help="pickle to use for reading and writing the TOC")
|
||||
self.parser.add_option('-R', '--release-id',
|
||||
parser.add_argument('-R', '--release-id',
|
||||
action="store", dest="release_id",
|
||||
help="MusicBrainz release id to match to (if there are multiple)")
|
||||
self.parser.add_option('-p', '--prompt',
|
||||
parser.add_argument('-p', '--prompt',
|
||||
action="store_true", dest="prompt",
|
||||
help="Prompt if there are multiple matching releases")
|
||||
self.parser.add_option('-c', '--country',
|
||||
parser.add_argument('-c', '--country',
|
||||
action="store", dest="country",
|
||||
help="Filter releases by country")
|
||||
|
||||
|
||||
def do(self, args):
|
||||
self.program = program.Program(self.getRootCommand().config,
|
||||
record=self.getRootCommand().record,
|
||||
stdout=self.stdout)
|
||||
def do(self):
|
||||
self.config = config.Config()
|
||||
self.program = program.Program(self.config,
|
||||
record=self.options.record,
|
||||
stdout=sys.stdout)
|
||||
self.runner = task.SyncRunner()
|
||||
|
||||
# if the device is mounted (data session), unmount it
|
||||
self.device = self.parentCommand.options.device
|
||||
self.stdout.write('Checking device %s\n' % self.device)
|
||||
#self.device = self.parentCommand.options.device
|
||||
self.device = self.options.device
|
||||
sys.stdout.write('Checking device %s\n' % self.device)
|
||||
|
||||
self.program.loadDevice(self.device)
|
||||
self.program.unmountDevice(self.device)
|
||||
@@ -87,11 +118,11 @@ class _CD(logcommand.LogCommand):
|
||||
|
||||
# already show us some info based on this
|
||||
self.program.getRipResult(self.ittoc.getCDDBDiscId())
|
||||
self.stdout.write("CDDB disc id: %s\n" % self.ittoc.getCDDBDiscId())
|
||||
sys.stdout.write("CDDB disc id: %s\n" % self.ittoc.getCDDBDiscId())
|
||||
self.mbdiscid = self.ittoc.getMusicBrainzDiscId()
|
||||
self.stdout.write("MusicBrainz disc id %s\n" % self.mbdiscid)
|
||||
sys.stdout.write("MusicBrainz disc id %s\n" % self.mbdiscid)
|
||||
|
||||
self.stdout.write("MusicBrainz lookup URL %s\n" %
|
||||
sys.stdout.write("MusicBrainz lookup URL %s\n" %
|
||||
self.ittoc.getMusicBrainzSubmitURL())
|
||||
|
||||
self.program.metadata = self.program.getMusicBrainz(self.ittoc,
|
||||
@@ -105,7 +136,7 @@ class _CD(logcommand.LogCommand):
|
||||
cddbid = self.ittoc.getCDDBValues()
|
||||
cddbmd = self.program.getCDDB(cddbid)
|
||||
if cddbmd:
|
||||
self.stdout.write('FreeDB identifies disc as %s\n' % cddbmd)
|
||||
sys.stdout.write('FreeDB identifies disc as %s\n' % cddbmd)
|
||||
|
||||
# also used by rip cd info
|
||||
if not getattr(self.options, 'unknown', False):
|
||||
@@ -113,12 +144,13 @@ class _CD(logcommand.LogCommand):
|
||||
self.program.ejectDevice(self.device)
|
||||
return -1
|
||||
|
||||
# FIXME ?????
|
||||
# Hackish fix for broken commit
|
||||
offset = 0
|
||||
info = drive.getDeviceInfo(self.parentCommand.options.device)
|
||||
info = drive.getDeviceInfo(self.device)
|
||||
if info:
|
||||
try:
|
||||
offset = self.getRootCommand().config.getReadOffset(*info)
|
||||
offset = self.config.getReadOffset(*info)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@@ -148,13 +180,13 @@ class _CD(logcommand.LogCommand):
|
||||
self.program.result.cdrdaoVersion = cdrdao.getCDRDAOVersion()
|
||||
self.program.result.cdparanoiaVersion = \
|
||||
cdparanoia.getCdParanoiaVersion()
|
||||
info = drive.getDeviceInfo(self.parentCommand.options.device)
|
||||
info = drive.getDeviceInfo(self.device)
|
||||
if info:
|
||||
try:
|
||||
self.program.result.cdparanoiaDefeatsCache = \
|
||||
self.getRootCommand().config.getDefeatsCache(*info)
|
||||
self.config.getDefeatsCache(*info)
|
||||
except KeyError, e:
|
||||
self.debug('Got key error: %r' % (e, ))
|
||||
logger.debug('Got key error: %r' % (e, ))
|
||||
self.program.result.artist = self.program.metadata \
|
||||
and self.program.metadata.artist \
|
||||
or 'Unknown Artist'
|
||||
@@ -182,13 +214,17 @@ class _CD(logcommand.LogCommand):
|
||||
|
||||
class Info(_CD):
|
||||
summary = "retrieve information about the currently inserted CD"
|
||||
|
||||
description = ("Display musicbrainz, CDDB/FreeDB, and AccurateRip"
|
||||
"information for the currently inserted CD.")
|
||||
eject = False
|
||||
|
||||
# Requires opts.device
|
||||
|
||||
def add_arguments(self):
|
||||
_CD.add_arguments(self.parser)
|
||||
|
||||
class Rip(_CD):
|
||||
summary = "rip CD"
|
||||
|
||||
# see morituri.common.program.Program.getPath for expansion
|
||||
description = """
|
||||
Rips a CD.
|
||||
@@ -200,76 +236,66 @@ relative to the directory of the disc files.
|
||||
|
||||
All files will be created relative to the given output directory.
|
||||
Log files will log the path to tracks relative to this directory.
|
||||
""" % rcommon.TEMPLATE_DESCRIPTION
|
||||
""" % TEMPLATE_DESCRIPTION
|
||||
formatter_class = argparse.ArgumentDefaultsHelpFormatter
|
||||
|
||||
def addOptions(self):
|
||||
_CD.addOptions(self)
|
||||
# Requires opts.record
|
||||
# Requires opts.device
|
||||
|
||||
def add_arguments(self):
|
||||
loggers = result.getLoggers().keys()
|
||||
default_offset = None
|
||||
info = drive.getDeviceInfo(self.opts.device)
|
||||
if info:
|
||||
try:
|
||||
default_offset = config.Config().getReadOffset(*info)
|
||||
sys.stdout.write("Using configured read offset %d\n" %
|
||||
default_offset)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
self.parser.add_option('-L', '--logger',
|
||||
action="store", dest="logger",
|
||||
default='morituri',
|
||||
help="logger to use "
|
||||
"(default '%default', choose from '" +
|
||||
"', '".join(loggers) + "')")
|
||||
_CD.add_arguments(self.parser)
|
||||
|
||||
self.parser.add_argument('-L', '--logger',
|
||||
action="store", dest="logger", default='morituri',
|
||||
help="logger to use (choose from '" + "', '".join(loggers) + "')")
|
||||
# FIXME: get from config
|
||||
self.parser.add_option('-o', '--offset',
|
||||
action="store", dest="offset",
|
||||
help="sample read offset (defaults to configured value, or 0)")
|
||||
self.parser.add_option('-x', '--force-overread',
|
||||
action="store_true", dest="overread",
|
||||
self.parser.add_argument('-o', '--offset',
|
||||
action="store", dest="offset", default=default_offset,
|
||||
help="sample read offset")
|
||||
self.parser.add_argument('-x', '--force-overread',
|
||||
action="store_true", dest="overread", default=False,
|
||||
help="Force overreading into the lead-out portion of the disc. "
|
||||
"Works only if the patched cdparanoia package is installed "
|
||||
"and the drive supports this feature. "
|
||||
"The default value is: %default",
|
||||
default=False)
|
||||
self.parser.add_option('-O', '--output-directory',
|
||||
"and the drive supports this feature. ")
|
||||
self.parser.add_argument('-O', '--output-directory',
|
||||
action="store", dest="output_directory",
|
||||
help="output directory; will be included in file paths in result "
|
||||
"files "
|
||||
"(defaults to absolute path to current directory; set to "
|
||||
"empty if you want paths to be relative instead; "
|
||||
"configured value: %default) ")
|
||||
self.parser.add_option('-W', '--working-directory',
|
||||
default=os.path.relpath(os.getcwd()),
|
||||
help="output directory; will be included in file paths in log")
|
||||
self.parser.add_argument('-W', '--working-directory',
|
||||
action="store", dest="working_directory",
|
||||
help="working directory; morituri will change to this directory "
|
||||
"and files will be created relative to it when not absolute "
|
||||
"(configured value: %default) ")
|
||||
|
||||
rcommon.addTemplate(self)
|
||||
|
||||
default = 'flac'
|
||||
|
||||
# here to avoid import gst eating our options
|
||||
from morituri.common import encode
|
||||
|
||||
self.parser.add_option('', '--profile',
|
||||
action="store", dest="profile",
|
||||
help="profile for encoding (default '%%default', choices '%s')" % (
|
||||
"', '".join(encode.PROFILES.keys())),
|
||||
default=default)
|
||||
self.parser.add_option('-U', '--unknown',
|
||||
"and files will be created relative to it when not absolute")
|
||||
self.parser.add_argument('--track-template',
|
||||
action="store", dest="track_template",
|
||||
default=DEFAULT_TRACK_TEMPLATE,
|
||||
help="template for track file naming (default default)")
|
||||
self.parser.add_argument('--disc-template',
|
||||
action="store", dest="disc_template",
|
||||
default=DEFAULT_DISC_TEMPLATE,
|
||||
help="template for disc file naming (default default)")
|
||||
self.parser.add_argument('-U', '--unknown',
|
||||
action="store_true", dest="unknown",
|
||||
help="whether to continue ripping if the CD is unknown (%default)",
|
||||
help="whether to continue ripping if the CD is unknown",
|
||||
default=False)
|
||||
|
||||
def handleOptions(self, options):
|
||||
options.track_template = options.track_template.decode('utf-8')
|
||||
options.disc_template = options.disc_template.decode('utf-8')
|
||||
def handle_arguments(self):
|
||||
self.options.output_directory = os.path.expanduser(self.options.output_directory)
|
||||
|
||||
if options.offset is None:
|
||||
info = drive.getDeviceInfo(self.parentCommand.options.device)
|
||||
if info:
|
||||
try:
|
||||
options.offset = self.getRootCommand(
|
||||
).config.getReadOffset(*info)
|
||||
self.stdout.write("Using configured read offset %d\n" %
|
||||
options.offset)
|
||||
except KeyError:
|
||||
pass
|
||||
self.options.track_template = self.options.track_template.decode('utf-8')
|
||||
self.options.disc_template = self.options.disc_template.decode('utf-8')
|
||||
|
||||
if options.offset is None:
|
||||
if self.options.offset is None:
|
||||
raise ValueError("Drive offset is unconfigured.\n"
|
||||
"Please install pycdio and run 'rip offset "
|
||||
"find' to detect your drive's offset or set it "
|
||||
@@ -277,29 +303,23 @@ Log files will log the path to tracks relative to this directory.
|
||||
"also be specified at runtime using the "
|
||||
"'--offset=value' argument")
|
||||
|
||||
if self.options.output_directory is None:
|
||||
self.options.output_directory = os.getcwd()
|
||||
else:
|
||||
self.options.output_directory = os.path.expanduser(self.options.output_directory)
|
||||
|
||||
if self.options.working_directory is not None:
|
||||
self.options.working_directory = os.path.expanduser(self.options.working_directory)
|
||||
|
||||
if self.options.logger:
|
||||
try:
|
||||
klazz = result.getLoggers()[self.options.logger]
|
||||
self.logger = result.getLoggers()[self.options.logger]()
|
||||
except KeyError:
|
||||
self.stderr.write("No logger named %s found!\n" % (
|
||||
self.options.logger))
|
||||
raise command.CommandError("No logger named %s" %
|
||||
self.options.logger)
|
||||
msg = "No logger named %s found!" % self.options.logger
|
||||
logger.critical(msg)
|
||||
raise ValueError(msg)
|
||||
|
||||
self.logger = klazz()
|
||||
|
||||
def doCommand(self):
|
||||
# here to avoid import gst eating our options
|
||||
from morituri.common import encode
|
||||
profile = encode.PROFILES[self.options.profile]()
|
||||
profile = encode.PROFILES['flac']()
|
||||
self.program.result.profileName = profile.name
|
||||
self.program.result.profilePipeline = profile.pipeline
|
||||
elementFactory = profile.pipeline.split(' ')[0]
|
||||
@@ -322,11 +342,11 @@ Log files will log the path to tracks relative to this directory.
|
||||
profile=profile, disambiguate=disambiguate)
|
||||
dirname = os.path.dirname(discName)
|
||||
if os.path.exists(dirname):
|
||||
self.stdout.write("Output directory %s already exists\n" %
|
||||
sys.stdout.write("Output directory %s already exists\n" %
|
||||
dirname.encode('utf-8'))
|
||||
logs = glob.glob(os.path.join(dirname, '*.log'))
|
||||
if logs:
|
||||
self.stdout.write(
|
||||
sys.stdout.write(
|
||||
"Output directory %s is a finished rip\n" %
|
||||
dirname.encode('utf-8'))
|
||||
if not disambiguate:
|
||||
@@ -337,7 +357,7 @@ Log files will log the path to tracks relative to this directory.
|
||||
break
|
||||
|
||||
else:
|
||||
self.stdout.write("Creating output directory %s\n" %
|
||||
sys.stdout.write("Creating output directory %s\n" %
|
||||
dirname.encode('utf-8'))
|
||||
os.makedirs(dirname)
|
||||
break
|
||||
@@ -349,14 +369,14 @@ Log files will log the path to tracks relative to this directory.
|
||||
# FIXME: turn this into a method
|
||||
|
||||
def ripIfNotRipped(number):
|
||||
self.debug('ripIfNotRipped for track %d' % number)
|
||||
logger.debug('ripIfNotRipped for track %d' % number)
|
||||
# we can have a previous result
|
||||
trackResult = self.program.result.getTrackResult(number)
|
||||
if not trackResult:
|
||||
trackResult = result.TrackResult()
|
||||
self.program.result.tracks.append(trackResult)
|
||||
else:
|
||||
self.debug('ripIfNotRipped have trackresult, path %r' %
|
||||
logger.debug('ripIfNotRipped have trackresult, path %r' %
|
||||
trackResult.filename)
|
||||
|
||||
path = self.program.getPath(self.program.outdir,
|
||||
@@ -364,7 +384,7 @@ Log files will log the path to tracks relative to this directory.
|
||||
self.mbdiscid, number,
|
||||
profile=profile, disambiguate=disambiguate) \
|
||||
+ '.' + profile.extension
|
||||
self.debug('ripIfNotRipped: path %r' % path)
|
||||
logger.debug('ripIfNotRipped: path %r' % path)
|
||||
trackResult.number = number
|
||||
|
||||
assert type(path) is unicode, "%r is not unicode" % path
|
||||
@@ -377,18 +397,18 @@ Log files will log the path to tracks relative to this directory.
|
||||
if path != trackResult.filename:
|
||||
# the path is different (different name/template ?)
|
||||
# but we can copy it
|
||||
self.debug('previous result %r, expected %r' % (
|
||||
logger.debug('previous result %r, expected %r' % (
|
||||
trackResult.filename, path))
|
||||
|
||||
self.stdout.write('Verifying track %d of %d: %s\n' % (
|
||||
sys.stdout.write('Verifying track %d of %d: %s\n' % (
|
||||
number, len(self.itable.tracks),
|
||||
os.path.basename(path).encode('utf-8')))
|
||||
if not self.program.verifyTrack(self.runner, trackResult):
|
||||
self.stdout.write('Verification failed, reripping...\n')
|
||||
sys.stdout.write('Verification failed, reripping...\n')
|
||||
os.unlink(path)
|
||||
|
||||
if not os.path.exists(path):
|
||||
self.debug('path %r does not exist, ripping...' % path)
|
||||
logger.debug('path %r does not exist, ripping...' % path)
|
||||
tries = 0
|
||||
# we reset durations for test and copy here
|
||||
trackResult.testduration = 0.0
|
||||
@@ -398,15 +418,15 @@ Log files will log the path to tracks relative to this directory.
|
||||
tries += 1
|
||||
if tries > 1:
|
||||
extra = " (try %d)" % tries
|
||||
self.stdout.write('Ripping track %d of %d%s: %s\n' % (
|
||||
sys.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',
|
||||
logger.debug('ripIfNotRipped: track %d, try %d',
|
||||
number, tries)
|
||||
self.program.ripTrack(self.runner, trackResult,
|
||||
offset=int(self.options.offset),
|
||||
device=self.parentCommand.options.device,
|
||||
device=self.device,
|
||||
profile=profile,
|
||||
taglist=self.program.getTagList(number),
|
||||
overread=self.options.overread,
|
||||
@@ -414,41 +434,41 @@ Log files will log the path to tracks relative to this directory.
|
||||
number, len(self.itable.tracks), extra))
|
||||
break
|
||||
except Exception, e:
|
||||
self.debug('Got exception %r on try %d',
|
||||
logger.debug('Got exception %r on try %d',
|
||||
e, tries)
|
||||
|
||||
|
||||
if tries == MAX_TRIES:
|
||||
self.error('Giving up on track %d after %d times' % (
|
||||
logger.critical('Giving up on track %d after %d times' % (
|
||||
number, tries))
|
||||
raise RuntimeError(
|
||||
"track can't be ripped. "
|
||||
"Rip attempts number is equal to 'MAX_TRIES'")
|
||||
if trackResult.testcrc == trackResult.copycrc:
|
||||
self.stdout.write('Checksums match for track %d\n' %
|
||||
sys.stdout.write('Checksums match for track %d\n' %
|
||||
number)
|
||||
else:
|
||||
self.stdout.write(
|
||||
sys.stdout.write(
|
||||
'ERROR: checksums did not match for track %d\n' %
|
||||
number)
|
||||
raise
|
||||
|
||||
self.stdout.write('Peak level: {:.2%} \n'.format(trackResult.peak))
|
||||
sys.stdout.write('Peak level: {:.2%} \n'.format(trackResult.peak))
|
||||
|
||||
self.stdout.write('Rip quality: {:.2%}\n'.format(trackResult.quality))
|
||||
sys.stdout.write('Rip quality: {:.2%}\n'.format(trackResult.quality))
|
||||
|
||||
# overlay this rip onto the Table
|
||||
if number == 0:
|
||||
# HTOA goes on index 0 of track 1
|
||||
# ignore silence in PREGAP
|
||||
if trackResult.peak <= SILENT:
|
||||
self.debug('HTOA peak %r is below SILENT threshold, disregarding', trackResult.peak)
|
||||
logger.debug('HTOA peak %r is below SILENT threshold, disregarding', trackResult.peak)
|
||||
self.itable.setFile(1, 0, None,
|
||||
self.ittoc.getTrackStart(1), number)
|
||||
self.debug('Unlinking %r', trackResult.filename)
|
||||
logger.debug('Unlinking %r', trackResult.filename)
|
||||
os.unlink(trackResult.filename)
|
||||
trackResult.filename = None
|
||||
self.stdout.write('HTOA discarded, contains digital silence\n')
|
||||
sys.stdout.write('HTOA discarded, contains digital silence\n')
|
||||
else:
|
||||
self.itable.setFile(1, 0, trackResult.filename,
|
||||
self.ittoc.getTrackStart(1), number)
|
||||
@@ -464,7 +484,7 @@ Log files will log the path to tracks relative to this directory.
|
||||
htoa = self.program.getHTOA()
|
||||
if htoa:
|
||||
start, stop = htoa
|
||||
self.stdout.write(
|
||||
sys.stdout.write(
|
||||
'Found Hidden Track One Audio from frame %d to %d\n' % (
|
||||
start, stop))
|
||||
|
||||
@@ -475,7 +495,7 @@ Log files will log the path to tracks relative to this directory.
|
||||
for i, track in enumerate(self.itable.tracks):
|
||||
# FIXME: rip data tracks differently
|
||||
if not track.audio:
|
||||
self.stdout.write(
|
||||
sys.stdout.write(
|
||||
'WARNING: skipping data track %d, not implemented\n' % (
|
||||
i + 1, ))
|
||||
# FIXME: make it work for now
|
||||
@@ -492,11 +512,11 @@ Log files will log the path to tracks relative to this directory.
|
||||
if not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
|
||||
self.debug('writing cue file for %r', discName)
|
||||
logger.debug('writing cue file for %r', discName)
|
||||
self.program.writeCue(discName)
|
||||
|
||||
# write .m3u file
|
||||
self.debug('writing m3u file for %r', discName)
|
||||
logger.debug('writing m3u file for %r', discName)
|
||||
m3uPath = u'%s.m3u' % discName
|
||||
handle = open(m3uPath, 'w')
|
||||
handle.write(u'#EXTM3U\n')
|
||||
@@ -528,7 +548,7 @@ Log files will log the path to tracks relative to this directory.
|
||||
|
||||
# verify using accuraterip
|
||||
url = self.ittoc.getAccurateRipURL()
|
||||
self.stdout.write("AccurateRip URL %s\n" % url)
|
||||
sys.stdout.write("AccurateRip URL %s\n" % url)
|
||||
|
||||
accucache = accurip.AccuCache()
|
||||
try:
|
||||
@@ -536,7 +556,7 @@ Log files will log the path to tracks relative to this directory.
|
||||
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" % (
|
||||
sys.stdout.write("Warning: network error: %r\n" % (
|
||||
e.args[0], ))
|
||||
responses = None
|
||||
else:
|
||||
@@ -545,21 +565,21 @@ Log files will log the path to tracks relative to this directory.
|
||||
raise
|
||||
|
||||
if not responses:
|
||||
self.stdout.write('Album not found in AccurateRip database\n')
|
||||
sys.stdout.write('Album not found in AccurateRip database\n')
|
||||
|
||||
if responses:
|
||||
self.stdout.write('%d AccurateRip reponses found\n' %
|
||||
sys.stdout.write('%d AccurateRip reponses found\n' %
|
||||
len(responses))
|
||||
|
||||
if responses[0].cddbDiscId != self.itable.getCDDBDiscId():
|
||||
self.stdout.write(
|
||||
sys.stdout.write(
|
||||
"AccurateRip response discid different: %s\n" %
|
||||
responses[0].cddbDiscId)
|
||||
|
||||
|
||||
self.program.verifyImage(self.runner, responses)
|
||||
|
||||
self.stdout.write("\n".join(
|
||||
sys.stdout.write("\n".join(
|
||||
self.program.getAccurateRipResults()) + "\n")
|
||||
|
||||
self.program.saveRipResult()
|
||||
@@ -570,26 +590,12 @@ Log files will log the path to tracks relative to this directory.
|
||||
self.program.ejectDevice(self.device)
|
||||
|
||||
|
||||
class CD(logcommand.LogCommand):
|
||||
class CD(BaseCommand):
|
||||
summary = "handle CDs"
|
||||
description = "Display and rip CD-DA and metadata."
|
||||
device_option = True
|
||||
|
||||
summary = "handle CD's"
|
||||
|
||||
subCommandClasses = [Info, Rip, ]
|
||||
|
||||
def addOptions(self):
|
||||
self.parser.add_option('-d', '--device',
|
||||
action="store", dest="device",
|
||||
help="CD-DA device")
|
||||
|
||||
def handleOptions(self, options):
|
||||
if not options.device:
|
||||
drives = drive.getAllDevicePaths()
|
||||
if not drives:
|
||||
self.error('No CD-DA drives found!')
|
||||
return 3
|
||||
|
||||
# pick the first
|
||||
self.options.device = drives[0]
|
||||
|
||||
# this can be a symlink to another device
|
||||
self.options.device = os.path.realpath(self.options.device)
|
||||
subcommands = {
|
||||
'info': Info,
|
||||
'rip': Rip
|
||||
}
|
||||
@@ -20,15 +20,19 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with morituri. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from morituri.common import logcommand
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from morituri.command.basecommand import BaseCommand
|
||||
from morituri.common import cache, task
|
||||
from morituri.result import result
|
||||
|
||||
from morituri.common import task, cache
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class RCCue(logcommand.LogCommand):
|
||||
|
||||
name = "cue"
|
||||
class RCCue(BaseCommand):
|
||||
summary = "write a cue file for the cached result"
|
||||
description = summary
|
||||
|
||||
def do(self, args):
|
||||
self._cache = cache.ResultCache()
|
||||
@@ -36,24 +40,23 @@ class RCCue(logcommand.LogCommand):
|
||||
try:
|
||||
discid = args[0]
|
||||
except IndexError:
|
||||
self.stderr.write(
|
||||
sys.stderr.write(
|
||||
'Please specify a cddb disc id\n')
|
||||
return 3
|
||||
|
||||
persisted = self._cache.getRipResult(discid, create=False)
|
||||
|
||||
if not persisted:
|
||||
self.stderr.write(
|
||||
sys.stderr.write(
|
||||
'Could not find a result for cddb disc id %s\n' % discid)
|
||||
return 3
|
||||
|
||||
self.stdout.write(persisted.object.table.cue().encode('utf-8'))
|
||||
sys.stdout.write(persisted.object.table.cue().encode('utf-8'))
|
||||
|
||||
|
||||
class RCList(logcommand.LogCommand):
|
||||
|
||||
name = "list"
|
||||
class RCList(BaseCommand):
|
||||
summary = "list cached results"
|
||||
description = summary
|
||||
|
||||
def do(self, args):
|
||||
self._cache = cache.ResultCache()
|
||||
@@ -71,24 +74,24 @@ class RCList(logcommand.LogCommand):
|
||||
if title is None:
|
||||
title = '(None)'
|
||||
|
||||
self.stdout.write('%s: %s - %s\n' % (
|
||||
sys.stdout.write('%s: %s - %s\n' % (
|
||||
cddbid, artist.encode('utf-8'), title.encode('utf-8')))
|
||||
|
||||
|
||||
class RCLog(logcommand.LogCommand):
|
||||
|
||||
name = "log"
|
||||
class RCLog(BaseCommand):
|
||||
summary = "write a log file for the cached result"
|
||||
description = summary
|
||||
formatter_class = argparse.ArgumentDefaultsHelpFormatter
|
||||
|
||||
def addOptions(self):
|
||||
def add_arguments(self):
|
||||
loggers = result.getLoggers().keys()
|
||||
|
||||
self.parser.add_option('-L', '--logger',
|
||||
self.parser.add_argument(
|
||||
'-L', '--logger',
|
||||
action="store", dest="logger",
|
||||
default='morituri',
|
||||
help="logger to use "
|
||||
"(default '%default', choose from '" +
|
||||
"', '".join(loggers) + "')")
|
||||
help="logger to use (choose from '" + "', '".join(loggers) + "')"
|
||||
)
|
||||
|
||||
def do(self, args):
|
||||
self._cache = cache.ResultCache()
|
||||
@@ -96,146 +99,155 @@ class RCLog(logcommand.LogCommand):
|
||||
persisted = self._cache.getRipResult(args[0], create=False)
|
||||
|
||||
if not persisted:
|
||||
self.stderr.write(
|
||||
sys.stderr.write(
|
||||
'Could not find a result for cddb disc id %s\n' % args[0])
|
||||
return 3
|
||||
|
||||
try:
|
||||
klazz = result.getLoggers()[self.options.logger]
|
||||
except KeyError:
|
||||
self.stderr.write("No logger named %s found!\n" % (
|
||||
sys.stderr.write("No logger named %s found!\n" % (
|
||||
self.options.logger))
|
||||
return 3
|
||||
|
||||
logger = klazz()
|
||||
self.stdout.write(logger.log(persisted.object).encode('utf-8'))
|
||||
sys.stdout.write(logger.log(persisted.object).encode('utf-8'))
|
||||
|
||||
|
||||
class ResultCache(logcommand.LogCommand):
|
||||
|
||||
class ResultCache(BaseCommand):
|
||||
summary = "debug result cache"
|
||||
aliases = ['rc', ]
|
||||
description = summary
|
||||
|
||||
subCommandClasses = [RCCue, RCList, RCLog, ]
|
||||
subcommands = {
|
||||
'cue': RCCue,
|
||||
'list': RCList,
|
||||
'log': RCLog,
|
||||
}
|
||||
|
||||
|
||||
class Checksum(logcommand.LogCommand):
|
||||
|
||||
class Checksum(BaseCommand):
|
||||
summary = "run a checksum task"
|
||||
description = summary
|
||||
|
||||
def do(self, args):
|
||||
if not args:
|
||||
self.stdout.write('Please specify one or more input files.\n')
|
||||
return 3
|
||||
def add_arguments(self):
|
||||
self.parser.add_argument('files', nargs='+', action='store',
|
||||
help="audio files to checksum")
|
||||
|
||||
def do(self):
|
||||
runner = task.SyncRunner()
|
||||
# here to avoid import gst eating our options
|
||||
from morituri.common import checksum
|
||||
|
||||
for arg in args:
|
||||
fromPath = unicode(arg)
|
||||
|
||||
for f in self.options.files:
|
||||
fromPath = unicode(f)
|
||||
checksumtask = checksum.CRC32Task(fromPath)
|
||||
|
||||
runner.run(checksumtask)
|
||||
|
||||
self.stdout.write('Checksum: %08x\n' % checksumtask.checksum)
|
||||
sys.stdout.write('Checksum: %08x\n' % checksumtask.checksum)
|
||||
|
||||
|
||||
class Encode(logcommand.LogCommand):
|
||||
|
||||
class Encode(BaseCommand):
|
||||
summary = "run an encode task"
|
||||
description = summary
|
||||
|
||||
def addOptions(self):
|
||||
def add_arguments(self):
|
||||
# here to avoid import gst eating our options
|
||||
from morituri.common import encode
|
||||
|
||||
default = 'flac'
|
||||
self.parser.add_option('', '--profile',
|
||||
action="store", dest="profile",
|
||||
# slated for deletion as flac will be the only encoder
|
||||
self.parser.add_argument('--profile',
|
||||
action="store",
|
||||
dest="profile",
|
||||
help="profile for encoding (default '%s', choices '%s')" % (
|
||||
default, "', '".join(encode.ALL_PROFILES.keys())),
|
||||
default=default)
|
||||
self.parser.add_argument('input', action='store',
|
||||
help="audio file to encode")
|
||||
self.parser.add_argument('output', nargs='?', action='store',
|
||||
help="output path")
|
||||
|
||||
def do(self, args):
|
||||
def do(self):
|
||||
from morituri.common import encode
|
||||
profile = encode.ALL_PROFILES[self.options.profile]()
|
||||
|
||||
try:
|
||||
fromPath = unicode(args[0])
|
||||
fromPath = unicode(self.options.input)
|
||||
except IndexError:
|
||||
self.stdout.write('Please specify an input file.\n')
|
||||
# unexercised after BaseCommand
|
||||
sys.stdout.write('Please specify an input file.\n')
|
||||
return 3
|
||||
|
||||
try:
|
||||
toPath = unicode(args[1])
|
||||
toPath = unicode(self.options.output)
|
||||
except IndexError:
|
||||
toPath = fromPath + '.' + profile.extension
|
||||
|
||||
runner = task.SyncRunner()
|
||||
|
||||
self.debug('Encoding %s to %s',
|
||||
logger.debug('Encoding %s to %s',
|
||||
fromPath.encode('utf-8'),
|
||||
toPath.encode('utf-8'))
|
||||
encodetask = encode.EncodeTask(fromPath, toPath, profile)
|
||||
|
||||
runner.run(encodetask)
|
||||
|
||||
self.stdout.write('Peak level: %r\n' % encodetask.peak)
|
||||
self.stdout.write('Encoded to %s\n' % toPath.encode('utf-8'))
|
||||
sys.stdout.write('Peak level: %r\n' % encodetask.peak)
|
||||
sys.stdout.write('Encoded to %s\n' % toPath.encode('utf-8'))
|
||||
|
||||
|
||||
class MaxSample(logcommand.LogCommand):
|
||||
|
||||
class MaxSample(BaseCommand):
|
||||
summary = "run a max sample task"
|
||||
description = summary
|
||||
|
||||
def add_arguments(self):
|
||||
self.parser.add_argument('files', nargs='+', action='store',
|
||||
help="audio files to sample")
|
||||
|
||||
def do(self, args):
|
||||
if not args:
|
||||
self.stdout.write('Please specify one or more input files.\n')
|
||||
return 3
|
||||
|
||||
def do(self):
|
||||
runner = task.SyncRunner()
|
||||
# here to avoid import gst eating our options
|
||||
from morituri.common import checksum
|
||||
|
||||
for arg in args:
|
||||
for arg in self.options.files:
|
||||
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' %
|
||||
sys.stdout.write('%s\n' % arg)
|
||||
sys.stdout.write('Biggest absolute sample: %04x\n' %
|
||||
checksumtask.checksum)
|
||||
|
||||
|
||||
class Tag(logcommand.LogCommand):
|
||||
|
||||
class Tag(BaseCommand):
|
||||
summary = "run a tag reading task"
|
||||
description = summary
|
||||
|
||||
def do(self, args):
|
||||
def add_arguments(self):
|
||||
self.parser.add_argument('file', action='store',
|
||||
help="audio file to tag")
|
||||
|
||||
def do(self):
|
||||
try:
|
||||
path = unicode(args[0])
|
||||
path = unicode(self.options.file)
|
||||
except IndexError:
|
||||
self.stdout.write('Please specify an input file.\n')
|
||||
sys.stdout.write('Please specify an input file.\n')
|
||||
return 3
|
||||
|
||||
runner = task.SyncRunner()
|
||||
|
||||
from morituri.common import encode
|
||||
self.debug('Reading tags from %s' % path.encode('utf-8'))
|
||||
logger.debug('Reading tags from %s' % path.encode('utf-8'))
|
||||
tagtask = encode.TagReadTask(path)
|
||||
|
||||
runner.run(tagtask)
|
||||
|
||||
for key in tagtask.taglist.keys():
|
||||
self.stdout.write('%s: %r\n' % (key, tagtask.taglist[key]))
|
||||
sys.stdout.write('%s: %r\n' % (key, tagtask.taglist[key]))
|
||||
|
||||
|
||||
class MusicBrainzNGS(logcommand.LogCommand):
|
||||
|
||||
usage = "[MusicBrainz disc id]"
|
||||
class MusicBrainzNGS(BaseCommand):
|
||||
summary = "examine MusicBrainz NGS info"
|
||||
description = """Look up a MusicBrainz disc id and output information.
|
||||
|
||||
@@ -243,62 +255,79 @@ You can get the MusicBrainz disc id with rip cd info.
|
||||
|
||||
Example disc id: KnpGsLhvH.lPrNc1PBL21lb9Bg4-"""
|
||||
|
||||
def do(self, args):
|
||||
def add_arguments(self):
|
||||
self.parser.add_argument('mbdiscid', action='store',
|
||||
help="MB disc id to look up")
|
||||
|
||||
def do(self):
|
||||
try:
|
||||
discId = unicode(args[0])
|
||||
discId = unicode(self.options.mbdiscid)
|
||||
except IndexError:
|
||||
self.stdout.write('Please specify a MusicBrainz disc id.\n')
|
||||
sys.stdout.write('Please specify a MusicBrainz disc id.\n')
|
||||
return 3
|
||||
|
||||
from morituri.common import mbngs
|
||||
metadatas = mbngs.musicbrainz(discId,
|
||||
record=self.getRootCommand().record)
|
||||
metadatas = mbngs.musicbrainz(discId, record=self.options.record)
|
||||
|
||||
self.stdout.write('%d releases\n' % len(metadatas))
|
||||
sys.stdout.write('%d releases\n' % len(metadatas))
|
||||
for i, md in enumerate(metadatas):
|
||||
self.stdout.write('- Release %d:\n' % (i + 1, ))
|
||||
self.stdout.write(' Artist: %s\n' % md.artist.encode('utf-8'))
|
||||
self.stdout.write(' Title: %s\n' % md.title.encode('utf-8'))
|
||||
self.stdout.write(' Type: %s\n' % md.releaseType.encode('utf-8'))
|
||||
self.stdout.write(' URL: %s\n' % md.url)
|
||||
self.stdout.write(' Tracks: %d\n' % len(md.tracks))
|
||||
sys.stdout.write('- Release %d:\n' % (i + 1, ))
|
||||
sys.stdout.write(' Artist: %s\n' % md.artist.encode('utf-8'))
|
||||
sys.stdout.write(' Title: %s\n' % md.title.encode('utf-8'))
|
||||
sys.stdout.write(' Type: %s\n' % md.releaseType.encode('utf-8'))
|
||||
sys.stdout.write(' URL: %s\n' % md.url)
|
||||
sys.stdout.write(' Tracks: %d\n' % len(md.tracks))
|
||||
if md.catalogNumber:
|
||||
self.stdout.write(' Cat no: %s\n' % md.catalogNumber)
|
||||
sys.stdout.write(' Cat no: %s\n' % md.catalogNumber)
|
||||
if md.barcode:
|
||||
self.stdout.write(' Barcode: %s\n' % md.barcode)
|
||||
sys.stdout.write(' Barcode: %s\n' % md.barcode)
|
||||
|
||||
for j, track in enumerate(md.tracks):
|
||||
self.stdout.write(' Track %2d: %s - %s\n' % (
|
||||
sys.stdout.write(' Track %2d: %s - %s\n' % (
|
||||
j + 1, track.artist.encode('utf-8'),
|
||||
track.title.encode('utf-8')))
|
||||
|
||||
|
||||
class CDParanoia(logcommand.LogCommand):
|
||||
class CDParanoia(BaseCommand):
|
||||
summary = "show cdparanoia version"
|
||||
description = summary
|
||||
|
||||
def do(self, args):
|
||||
def do(self):
|
||||
from morituri.program import cdparanoia
|
||||
version = cdparanoia.getCdParanoiaVersion()
|
||||
self.stdout.write("cdparanoia version: %s\n" % version)
|
||||
sys.stdout.write("cdparanoia version: %s\n" % version)
|
||||
|
||||
|
||||
class CDRDAO(logcommand.LogCommand):
|
||||
class CDRDAO(BaseCommand):
|
||||
summary = "show cdrdao version"
|
||||
description = summary
|
||||
|
||||
def do(self, args):
|
||||
def do(self):
|
||||
from morituri.program import cdrdao
|
||||
version = cdrdao.getCDRDAOVersion()
|
||||
self.stdout.write("cdrdao version: %s\n" % version)
|
||||
sys.stdout.write("cdrdao version: %s\n" % version)
|
||||
|
||||
|
||||
class Version(logcommand.LogCommand):
|
||||
|
||||
class Version(BaseCommand):
|
||||
summary = "debug version getting"
|
||||
description = summary
|
||||
|
||||
subCommandClasses = [CDParanoia, CDRDAO]
|
||||
subcommands = {
|
||||
'cdparanoia': CDParanoia,
|
||||
'cdrdao': CDRDAO,
|
||||
}
|
||||
|
||||
|
||||
class Debug(logcommand.LogCommand):
|
||||
|
||||
class Debug(BaseCommand):
|
||||
summary = "debug internals"
|
||||
description = "debug internals"
|
||||
|
||||
subCommandClasses = [Checksum, Encode, MaxSample, Tag, MusicBrainzNGS,
|
||||
ResultCache, Version]
|
||||
subcommands = {
|
||||
'checksum': Checksum,
|
||||
'encode': Encode,
|
||||
'maxsample': MaxSample,
|
||||
'tag': Tag,
|
||||
'musicbrainzngs': MusicBrainzNGS,
|
||||
'resultcache': ResultCache,
|
||||
'version': Version,
|
||||
}
|
||||
@@ -20,116 +20,105 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with morituri. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from morituri.command.basecommand import BaseCommand
|
||||
from morituri.common import config, drive
|
||||
from morituri.extern.task import task
|
||||
|
||||
from morituri.common import logcommand, drive
|
||||
from morituri.program import cdparanoia
|
||||
|
||||
class Analyze(logcommand.LogCommand):
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Analyze(BaseCommand):
|
||||
summary = "analyze caching behaviour of drive"
|
||||
description = """Determine whether cdparanoia can defeat the audio cache of the drive."""
|
||||
device_option = True
|
||||
|
||||
def addOptions(self):
|
||||
self.parser.add_option('-d', '--device',
|
||||
action="store", dest="device",
|
||||
help="CD-DA device")
|
||||
|
||||
def handleOptions(self, options):
|
||||
if not options.device:
|
||||
drives = drive.getAllDevicePaths()
|
||||
if not drives:
|
||||
self.error('No CD-DA drives found!')
|
||||
return 3
|
||||
|
||||
# pick the first
|
||||
self.options.device = drives[0]
|
||||
|
||||
# this can be a symlink to another device
|
||||
self.options.device = os.path.realpath(self.options.device)
|
||||
|
||||
def do(self, args):
|
||||
def do(self):
|
||||
runner = task.SyncRunner()
|
||||
t = cdparanoia.AnalyzeTask(self.options.device)
|
||||
runner.run(t)
|
||||
|
||||
if t.defeatsCache is None:
|
||||
self.stdout.write(
|
||||
sys.stdout.write(
|
||||
'Cannot analyze the drive. Is there a CD in it?\n')
|
||||
return
|
||||
if not t.defeatsCache:
|
||||
self.stdout.write(
|
||||
sys.stdout.write(
|
||||
'cdparanoia cannot defeat the audio cache on this drive.\n')
|
||||
else:
|
||||
self.stdout.write(
|
||||
sys.stdout.write(
|
||||
'cdparanoia can defeat the audio cache on this drive.\n')
|
||||
|
||||
info = drive.getDeviceInfo(self.options.device)
|
||||
if not info:
|
||||
self.stdout.write('Drive caching behaviour not saved: could not get device info (requires pycdio).\n')
|
||||
sys.stdout.write('Drive caching behaviour not saved: could not get device info (requires pycdio).\n')
|
||||
return
|
||||
|
||||
self.stdout.write(
|
||||
sys.stdout.write(
|
||||
'Adding drive cache behaviour to configuration file.\n')
|
||||
|
||||
self.getRootCommand().config.setDefeatsCache(info[0], info[1], info[2],
|
||||
config.Config().setDefeatsCache(info[0], info[1], info[2],
|
||||
t.defeatsCache)
|
||||
|
||||
|
||||
class List(logcommand.LogCommand):
|
||||
|
||||
class List(BaseCommand):
|
||||
summary = "list drives"
|
||||
description = """list available CD-DA drives"""
|
||||
|
||||
def do(self, args):
|
||||
def do(self):
|
||||
paths = drive.getAllDevicePaths()
|
||||
self.config = config.Config()
|
||||
|
||||
if not paths:
|
||||
self.stdout.write('No drives found.\n')
|
||||
self.stdout.write('Create /dev/cdrom if you have a CD drive, \n')
|
||||
self.stdout.write('or install pycdio for better detection.\n')
|
||||
sys.stdout.write('No drives found.\n')
|
||||
sys.stdout.write('Create /dev/cdrom if you have a CD drive, \n')
|
||||
sys.stdout.write('or install pycdio for better detection.\n')
|
||||
|
||||
return
|
||||
|
||||
try:
|
||||
import cdio as _
|
||||
except ImportError:
|
||||
self.stdout.write(
|
||||
sys.stdout.write(
|
||||
'Install pycdio for vendor/model/release detection.\n')
|
||||
return
|
||||
|
||||
for path in paths:
|
||||
vendor, model, release = drive.getDeviceInfo(path)
|
||||
self.stdout.write(
|
||||
sys.stdout.write(
|
||||
"drive: %s, vendor: %s, model: %s, release: %s\n" % (
|
||||
path, vendor, model, release))
|
||||
|
||||
try:
|
||||
offset = self.getRootCommand().config.getReadOffset(
|
||||
offset = self.config.getReadOffset(
|
||||
vendor, model, release)
|
||||
self.stdout.write(
|
||||
sys.stdout.write(
|
||||
" Configured read offset: %d\n" % offset)
|
||||
except KeyError:
|
||||
self.stdout.write(
|
||||
sys.stdout.write(
|
||||
" No read offset found. Run 'rip offset find'\n")
|
||||
|
||||
try:
|
||||
defeats = self.getRootCommand().config.getDefeatsCache(
|
||||
defeats = self.config.getDefeatsCache(
|
||||
vendor, model, release)
|
||||
self.stdout.write(
|
||||
sys.stdout.write(
|
||||
" Can defeat audio cache: %s\n" % defeats)
|
||||
except KeyError:
|
||||
self.stdout.write(
|
||||
sys.stdout.write(
|
||||
" Unknown whether audio cache can be defeated. "
|
||||
"Run 'rip drive analyze'\n")
|
||||
|
||||
|
||||
if not paths:
|
||||
self.stdout.write('No drives found.\n')
|
||||
sys.stdout.write('No drives found.\n')
|
||||
|
||||
|
||||
class Drive(logcommand.LogCommand):
|
||||
|
||||
class Drive(BaseCommand):
|
||||
summary = "handle drives"
|
||||
|
||||
subCommandClasses = [Analyze, List, ]
|
||||
description = """Drive utilities."""
|
||||
subcommands = {
|
||||
'analyze': Analyze,
|
||||
'list': List
|
||||
}
|
||||
@@ -21,117 +21,60 @@
|
||||
# along with morituri. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from morituri.common import logcommand, accurip, program
|
||||
from morituri.command.basecommand import BaseCommand
|
||||
from morituri.common import accurip, config, program
|
||||
from morituri.extern.task import task
|
||||
from morituri.image import image
|
||||
from morituri.result import result
|
||||
|
||||
from morituri.extern.task import task
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Encode(logcommand.LogCommand):
|
||||
|
||||
summary = "encode image"
|
||||
|
||||
def addOptions(self):
|
||||
# FIXME: get from config
|
||||
self.parser.add_option('-O', '--output-directory',
|
||||
action="store", dest="output_directory",
|
||||
help="output directory (defaults to current directory)")
|
||||
|
||||
default = 'vorbis'
|
||||
|
||||
# here to avoid import gst eating our options
|
||||
from morituri.common import encode
|
||||
|
||||
self.parser.add_option('', '--profile',
|
||||
action="store", dest="profile",
|
||||
help="profile for encoding (default '%s', choices '%s')" % (
|
||||
default, "', '".join(encode.ALL_PROFILES.keys())),
|
||||
default=default)
|
||||
|
||||
def do(self, args):
|
||||
prog = program.Program(self.getRootCommand().config)
|
||||
prog.outdir = (self.options.output_directory or os.getcwd())
|
||||
prog.outdir = prog.outdir.decode('utf-8')
|
||||
|
||||
# here to avoid import gst eating our options
|
||||
from morituri.common import encode
|
||||
|
||||
profile = encode.ALL_PROFILES[self.options.profile]()
|
||||
|
||||
runner = task.SyncRunner()
|
||||
|
||||
for arg in args:
|
||||
arg = arg.decode('utf-8')
|
||||
indir = os.path.dirname(arg)
|
||||
cueImage = image.Image(arg)
|
||||
cueImage.setup(runner)
|
||||
# FIXME: find a decent way to get an album-specific outdir
|
||||
root = os.path.basename(indir)
|
||||
outdir = os.path.join(prog.outdir, root)
|
||||
try:
|
||||
os.makedirs(outdir)
|
||||
except:
|
||||
# FIXME: handle other exceptions than OSError Errno 17
|
||||
pass
|
||||
# FIXME: handle this nicer
|
||||
assert outdir != indir
|
||||
|
||||
taskk = image.ImageEncodeTask(cueImage, profile, outdir)
|
||||
runner.run(taskk)
|
||||
|
||||
# FIXME: translate .m3u file if it exists
|
||||
root, ext = os.path.splitext(arg)
|
||||
m3upath = root + '.m3u'
|
||||
if os.path.exists(m3upath):
|
||||
self.debug('translating .m3u file')
|
||||
inm3u = open(m3upath)
|
||||
outm3u = open(os.path.join(outdir, os.path.basename(m3upath)),
|
||||
'w')
|
||||
for line in inm3u.readlines():
|
||||
root, ext = os.path.splitext(line)
|
||||
if ext:
|
||||
# newline is swallowed by splitext here
|
||||
outm3u.write('%s.%s\n' % (root, profile.extension))
|
||||
else:
|
||||
outm3u.write('%s' % root)
|
||||
outm3u.close()
|
||||
|
||||
|
||||
class Retag(logcommand.LogCommand):
|
||||
|
||||
class Retag(BaseCommand):
|
||||
summary = "retag image files"
|
||||
description = """
|
||||
Retags the image from the given .cue files with tags obtained from MusicBrainz.
|
||||
"""
|
||||
|
||||
def addOptions(self):
|
||||
self.parser.add_option('-R', '--release-id',
|
||||
def add_arguments(self):
|
||||
self.parser.add_argument('cuefile', nargs='+', action='store',
|
||||
help="cue file to load rip image from")
|
||||
self.parser.add_argument(
|
||||
'-R', '--release-id',
|
||||
action="store", dest="release_id",
|
||||
help="MusicBrainz release id to match to (if there are multiple)")
|
||||
self.parser.add_option('-p', '--prompt',
|
||||
help="MusicBrainz release id to match to (if there are multiple)"
|
||||
)
|
||||
self.parser.add_argument(
|
||||
'-p', '--prompt',
|
||||
action="store_true", dest="prompt",
|
||||
help="Prompt if there are multiple matching releases")
|
||||
self.parser.add_option('-c', '--country',
|
||||
help="Prompt if there are multiple matching releases"
|
||||
)
|
||||
self.parser.add_argument(
|
||||
'-c', '--country',
|
||||
action="store", dest="country",
|
||||
help="Filter releases by country")
|
||||
help="Filter releases by country"
|
||||
)
|
||||
|
||||
|
||||
def do(self, args):
|
||||
def do(self):
|
||||
# here to avoid import gst eating our options
|
||||
from morituri.common import encode
|
||||
|
||||
prog = program.Program(self.getRootCommand().config, stdout=self.stdout)
|
||||
prog = program.Program(config.Config(), stdout=sys.stdout)
|
||||
runner = task.SyncRunner()
|
||||
|
||||
for arg in args:
|
||||
self.stdout.write('Retagging image %r\n' % arg)
|
||||
for arg in self.options.cuefile:
|
||||
sys.stdout.write('Retagging image %r\n' % arg)
|
||||
arg = arg.decode('utf-8')
|
||||
cueImage = image.Image(arg)
|
||||
cueImage.setup(runner)
|
||||
|
||||
mbdiscid = cueImage.table.getMusicBrainzDiscId()
|
||||
self.stdout.write('MusicBrainz disc id is %s\n' % mbdiscid)
|
||||
sys.stdout.write('MusicBrainz disc id is %s\n' % mbdiscid)
|
||||
|
||||
self.stdout.write("MusicBrainz lookup URL %s\n" %
|
||||
sys.stdout.write("MusicBrainz lookup URL %s\n" %
|
||||
cueImage.table.getMusicBrainzSubmitURL())
|
||||
prog.metadata = prog.getMusicBrainz(cueImage.table, mbdiscid,
|
||||
release=self.options.release_id,
|
||||
@@ -151,7 +94,7 @@ class Retag(logcommand.LogCommand):
|
||||
path = cueImage.getRealPath(track.indexes[1].path)
|
||||
|
||||
taglist = prog.getTagList(track.number)
|
||||
self.debug(
|
||||
logger.debug(
|
||||
'possibly retagging %r from cue path %r with taglist %r',
|
||||
path, arg, taglist)
|
||||
t = encode.SafeRetagTask(path, taglist)
|
||||
@@ -164,21 +107,22 @@ class Retag(logcommand.LogCommand):
|
||||
print
|
||||
|
||||
|
||||
class Verify(logcommand.LogCommand):
|
||||
|
||||
usage = '[CUEFILE]...'
|
||||
class Verify(BaseCommand):
|
||||
summary = "verify image"
|
||||
|
||||
description = '''
|
||||
description = """
|
||||
Verifies the image from the given .cue files against the AccurateRip database.
|
||||
'''
|
||||
"""
|
||||
|
||||
def do(self, args):
|
||||
prog = program.Program(self.getRootCommand().config)
|
||||
def add_arguments(self):
|
||||
self.parser.add_argument('cuefile', nargs='+', action='store',
|
||||
help="cue file to load rip image from")
|
||||
|
||||
def do(self):
|
||||
prog = program.Program(config.Config())
|
||||
runner = task.SyncRunner()
|
||||
cache = accurip.AccuCache()
|
||||
|
||||
for arg in args:
|
||||
for arg in self.options.cuefile:
|
||||
arg = arg.decode('utf-8')
|
||||
cueImage = image.Image(arg)
|
||||
cueImage.setup(runner)
|
||||
@@ -199,14 +143,14 @@ Verifies the image from the given .cue files against the AccurateRip database.
|
||||
print "\n".join(prog.getAccurateRipResults()) + "\n"
|
||||
|
||||
|
||||
class Image(logcommand.LogCommand):
|
||||
|
||||
class Image(BaseCommand):
|
||||
summary = "handle images"
|
||||
|
||||
description = """
|
||||
Handle disc images. Disc images are described by a .cue file.
|
||||
Disc images can be encoded to another format (for example, to make a
|
||||
compressed encoding), retagged and verified.
|
||||
"""
|
||||
|
||||
subCommandClasses = [Encode, Retag, Verify, ]
|
||||
subcommands = {
|
||||
'verify': Verify,
|
||||
'retag': Retag
|
||||
}
|
||||
86
morituri/command/main.py
Normal file
86
morituri/command/main.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# -*- Mode: Python -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
import sys
|
||||
import pkg_resources
|
||||
import musicbrainzngs
|
||||
|
||||
from morituri.command import cd, offset, drive, image, accurip, debug
|
||||
from morituri.command.basecommand import BaseCommand
|
||||
from morituri.common import common, directory
|
||||
from morituri.configure import configure
|
||||
from morituri.extern.task import task
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def main():
|
||||
# set user agent
|
||||
musicbrainzngs.set_useragent("whipper", configure.version,
|
||||
"https://github.com/JoeLametta/whipper")
|
||||
# register plugins with pkg_resources
|
||||
distributions, _ = pkg_resources.working_set.find_plugins(
|
||||
pkg_resources.Environment([directory.data_path('plugins')])
|
||||
)
|
||||
map(pkg_resources.working_set.add, distributions)
|
||||
try:
|
||||
ret = Whipper(sys.argv[1:], os.path.basename(sys.argv[0]), None).do()
|
||||
except SystemError, e:
|
||||
sys.stderr.write('whipper: error: %s\n' % e.args)
|
||||
return 255
|
||||
except ImportError, e:
|
||||
raise ImportError(e)
|
||||
except task.TaskException, e:
|
||||
if isinstance(e.exception, ImportError):
|
||||
raise ImportError(e.exception)
|
||||
elif isinstance(e.exception, common.MissingDependencyException):
|
||||
sys.stderr.write('whipper: error: missing dependency "%s"\n' %
|
||||
e.exception.dependency)
|
||||
return 255
|
||||
|
||||
if isinstance(e.exception, common.EmptyError):
|
||||
logger.debug("EmptyError: %r", str(e.exception))
|
||||
sys.stderr.write('whipper: error: Could not create encoded file.\n')
|
||||
return 255
|
||||
|
||||
# in python3 we can instead do `raise e.exception` as that would show
|
||||
# the exception's original context
|
||||
sys.stderr.write(e.exceptionMessage)
|
||||
return 255
|
||||
return ret if ret else 0
|
||||
|
||||
class Whipper(BaseCommand):
|
||||
description = """whipper is a CD ripping utility focusing on accuracy over speed.
|
||||
|
||||
whipper gives you a tree of subcommands to work with.
|
||||
You can get help on subcommands by using the -h option to the subcommand.
|
||||
"""
|
||||
no_add_help = True
|
||||
subcommands = {
|
||||
'accurip': accurip.AccuRip,
|
||||
'cd': cd.CD,
|
||||
'debug': debug.Debug,
|
||||
'drive': drive.Drive,
|
||||
'offset': offset.Offset,
|
||||
'image': image.Image
|
||||
}
|
||||
|
||||
def add_arguments(self):
|
||||
self.parser.add_argument('-R', '--record',
|
||||
action='store_true', dest='record',
|
||||
help="record API requests for playback")
|
||||
self.parser.add_argument('-v', '--version',
|
||||
action="store_true", dest="version",
|
||||
help="show version information")
|
||||
self.parser.add_argument('-h', '--help',
|
||||
action="store_true", dest="help",
|
||||
help="show this help message and exit")
|
||||
|
||||
def handle_arguments(self):
|
||||
if self.options.help:
|
||||
self.parser.print_help()
|
||||
sys.exit(0)
|
||||
if self.options.version:
|
||||
print "whipper %s" % configure.version
|
||||
sys.exit(0)
|
||||
@@ -20,18 +20,24 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with morituri. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import gobject
|
||||
gobject.threads_init()
|
||||
|
||||
from morituri.common import logcommand, accurip, drive, program, common
|
||||
from morituri.command.basecommand import BaseCommand
|
||||
from morituri.common import accurip, common, config, drive, program
|
||||
from morituri.common import task as ctask
|
||||
from morituri.program import cdrdao, cdparanoia
|
||||
|
||||
from morituri.extern.task import task
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# see http://www.accuraterip.com/driveoffsets.htm
|
||||
# and misc/offsets.py
|
||||
OFFSETS = "+6, +48, +102, +667, +12, +30, +618, +594, +738, -472, " + \
|
||||
@@ -47,26 +53,23 @@ OFFSETS = "+6, +48, +102, +667, +12, +30, +618, +594, +738, -472, " + \
|
||||
"+1127"
|
||||
|
||||
|
||||
class Find(logcommand.LogCommand):
|
||||
class Find(BaseCommand):
|
||||
summary = "find drive read offset"
|
||||
description = """Find drive's read offset by ripping tracks from a
|
||||
CD in the AccurateRip database."""
|
||||
formatter_class = argparse.ArgumentDefaultsHelpFormatter
|
||||
device_option = True
|
||||
|
||||
def addOptions(self):
|
||||
default = OFFSETS
|
||||
self.parser.add_option('-o', '--offsets',
|
||||
action="store", dest="offsets",
|
||||
help="list of offsets, comma-separated, "
|
||||
"colon-separated for ranges (defaults to %s)" % default,
|
||||
default=default)
|
||||
self.parser.add_option('-d', '--device',
|
||||
action="store", dest="device",
|
||||
help="CD-DA device")
|
||||
def add_arguments(self):
|
||||
self.parser.add_argument(
|
||||
'-o', '--offsets',
|
||||
action="store", dest="offsets", default=OFFSETS,
|
||||
help="list of offsets, comma-separated, colon-separated for ranges"
|
||||
)
|
||||
|
||||
def handleOptions(self, options):
|
||||
self.options = options
|
||||
def handle_arguments(self):
|
||||
self._offsets = []
|
||||
blocks = options.offsets.split(',')
|
||||
blocks = self.options.offsets.split(',')
|
||||
for b in blocks:
|
||||
if ':' in b:
|
||||
a, b = b.split(':')
|
||||
@@ -74,27 +77,16 @@ CD in the AccurateRip database."""
|
||||
else:
|
||||
self._offsets.append(int(b))
|
||||
|
||||
self.debug('Trying with offsets %r', self._offsets)
|
||||
logger.debug('Trying with offsets %r', self._offsets)
|
||||
|
||||
if not options.device:
|
||||
drives = drive.getAllDevicePaths()
|
||||
if not drives:
|
||||
self.error('No CD-DA drives found!')
|
||||
return 3
|
||||
|
||||
# pick the first
|
||||
self.options.device = drives[0]
|
||||
|
||||
# this can be a symlink to another device
|
||||
|
||||
def do(self, args):
|
||||
prog = program.Program(self.getRootCommand().config)
|
||||
def do(self):
|
||||
prog = program.Program(config.Config())
|
||||
runner = ctask.SyncRunner()
|
||||
|
||||
device = self.options.device
|
||||
|
||||
# if necessary, load and unmount
|
||||
self.stdout.write('Checking device %s\n' % device)
|
||||
sys.stdout.write('Checking device %s\n' % device)
|
||||
|
||||
prog.loadDevice(device)
|
||||
prog.unmountDevice(device)
|
||||
@@ -103,9 +95,9 @@ CD in the AccurateRip database."""
|
||||
t = cdrdao.ReadTOCTask(device)
|
||||
table = t.table
|
||||
|
||||
self.debug("CDDB disc id: %r", table.getCDDBDiscId())
|
||||
logger.debug("CDDB disc id: %r", table.getCDDBDiscId())
|
||||
url = table.getAccurateRipURL()
|
||||
self.debug("AccurateRip URL: %s", url)
|
||||
logger.debug("AccurateRip URL: %s", url)
|
||||
|
||||
# FIXME: download url as a task too
|
||||
responses = []
|
||||
@@ -116,17 +108,17 @@ CD in the AccurateRip database."""
|
||||
responses = accurip.getAccurateRipResponses(data)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
self.stdout.write(
|
||||
sys.stdout.write(
|
||||
'Album not found in AccurateRip database.\n')
|
||||
return 1
|
||||
else:
|
||||
raise
|
||||
|
||||
if responses:
|
||||
self.debug('%d AccurateRip responses found.' % len(responses))
|
||||
logger.debug('%d AccurateRip responses found.' % len(responses))
|
||||
|
||||
if responses[0].cddbDiscId != table.getCDDBDiscId():
|
||||
self.warning("AccurateRip response discid different: %s",
|
||||
logger.warning("AccurateRip response discid different: %s",
|
||||
responses[0].cddbDiscId)
|
||||
|
||||
# now rip the first track at various offsets, calculating AccurateRip
|
||||
@@ -140,7 +132,7 @@ CD in the AccurateRip database."""
|
||||
return None, None
|
||||
|
||||
for offset in self._offsets:
|
||||
self.stdout.write('Trying read offset %d ...\n' % offset)
|
||||
sys.stdout.write('Trying read offset %d ...\n' % offset)
|
||||
try:
|
||||
archecksum = self._arcs(runner, table, 1, offset)
|
||||
except task.TaskException, e:
|
||||
@@ -151,23 +143,23 @@ CD in the AccurateRip database."""
|
||||
raise e
|
||||
|
||||
if isinstance(e.exception, cdparanoia.FileSizeError):
|
||||
self.stdout.write(
|
||||
sys.stdout.write(
|
||||
'WARNING: cannot rip with offset %d...\n' % offset)
|
||||
continue
|
||||
|
||||
self.warning("Unknown task exception for offset %d: %r" % (
|
||||
logger.warning("Unknown task exception for offset %d: %r" % (
|
||||
offset, e))
|
||||
self.stdout.write(
|
||||
sys.stdout.write(
|
||||
'WARNING: cannot rip with offset %d...\n' % offset)
|
||||
continue
|
||||
|
||||
self.debug('AR checksum calculated: %s' % archecksum)
|
||||
logger.debug('AR checksum calculated: %s' % archecksum)
|
||||
|
||||
c, i = match(archecksum, 1, responses)
|
||||
if c:
|
||||
count = 1
|
||||
self.debug('MATCHED against response %d' % i)
|
||||
self.stdout.write(
|
||||
logger.debug('MATCHED against response %d' % i)
|
||||
sys.stdout.write(
|
||||
'Offset of device is likely %d, confirming ...\n' %
|
||||
offset)
|
||||
|
||||
@@ -178,14 +170,14 @@ CD in the AccurateRip database."""
|
||||
archecksum = self._arcs(runner, table, track, offset)
|
||||
except task.TaskException, e:
|
||||
if isinstance(e.exception, cdparanoia.FileSizeError):
|
||||
self.stdout.write(
|
||||
sys.stdout.write(
|
||||
'WARNING: cannot rip with offset %d...\n' %
|
||||
offset)
|
||||
continue
|
||||
|
||||
c, i = match(archecksum, track, responses)
|
||||
if c:
|
||||
self.debug('MATCHED track %d against response %d' % (
|
||||
logger.debug('MATCHED track %d against response %d' % (
|
||||
track, i))
|
||||
count += 1
|
||||
|
||||
@@ -193,17 +185,17 @@ CD in the AccurateRip database."""
|
||||
self._foundOffset(device, offset)
|
||||
return 0
|
||||
else:
|
||||
self.stdout.write(
|
||||
sys.stdout.write(
|
||||
'Only %d of %d tracks matched, continuing ...\n' % (
|
||||
count, len(table.tracks)))
|
||||
|
||||
self.stdout.write('No matching offset found.\n')
|
||||
self.stdout.write('Consider trying again with a different disc.\n')
|
||||
sys.stdout.write('No matching offset found.\n')
|
||||
sys.stdout.write('Consider trying again with a different disc.\n')
|
||||
|
||||
# TODO MW: Update this further for ARv2 code
|
||||
def _arcs(self, runner, table, track, offset):
|
||||
# rips the track with the given offset, return the arcs checksum
|
||||
self.debug('Ripping track %r with offset %d ...', track, offset)
|
||||
logger.debug('Ripping track %r with offset %d ...', track, offset)
|
||||
|
||||
fd, path = tempfile.mkstemp(
|
||||
suffix=u'.track%02d.offset%d.morituri.wav' % (
|
||||
@@ -229,21 +221,25 @@ CD in the AccurateRip database."""
|
||||
return "%08x" % t.checksum
|
||||
|
||||
def _foundOffset(self, device, offset):
|
||||
self.stdout.write('\nRead offset of device is: %d.\n' %
|
||||
sys.stdout.write('\nRead offset of device is: %d.\n' %
|
||||
offset)
|
||||
|
||||
info = drive.getDeviceInfo(device)
|
||||
if not info:
|
||||
self.stdout.write('Offset not saved: could not get device info (requires pycdio).\n')
|
||||
sys.stdout.write('Offset not saved: could not get device info (requires pycdio).\n')
|
||||
return
|
||||
|
||||
self.stdout.write('Adding read offset to configuration file.\n')
|
||||
sys.stdout.write('Adding read offset to configuration file.\n')
|
||||
|
||||
self.getRootCommand().config.setReadOffset(info[0], info[1], info[2],
|
||||
config.Config().setReadOffset(info[0], info[1], info[2],
|
||||
offset)
|
||||
|
||||
|
||||
class Offset(logcommand.LogCommand):
|
||||
class Offset(BaseCommand):
|
||||
summary = "handle drive offsets"
|
||||
|
||||
subCommandClasses = [Find, ]
|
||||
description = """
|
||||
Drive offset detection utility.
|
||||
"""
|
||||
subcommands = {
|
||||
'find': Find,
|
||||
}
|
||||
@@ -26,16 +26,19 @@ import struct
|
||||
import urlparse
|
||||
import urllib2
|
||||
|
||||
from morituri.common import log, directory
|
||||
from morituri.common import directory
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_CACHE_DIR = directory.cache_path()
|
||||
|
||||
|
||||
class AccuCache(log.Loggable):
|
||||
class AccuCache:
|
||||
|
||||
def __init__(self):
|
||||
if not os.path.exists(_CACHE_DIR):
|
||||
self.debug('Creating cache directory %s', _CACHE_DIR)
|
||||
logger.debug('Creating cache directory %s', _CACHE_DIR)
|
||||
os.makedirs(_CACHE_DIR)
|
||||
|
||||
def _getPath(self, url):
|
||||
@@ -43,18 +46,18 @@ class AccuCache(log.Loggable):
|
||||
return os.path.join(_CACHE_DIR, urlparse.urlparse(url)[2][1:])
|
||||
|
||||
def retrieve(self, url, force=False):
|
||||
self.debug("Retrieving AccurateRip URL %s", url)
|
||||
logger.debug("Retrieving AccurateRip URL %s", url)
|
||||
path = self._getPath(url)
|
||||
self.debug("Cached path: %s", path)
|
||||
logger.debug("Cached path: %s", path)
|
||||
if force:
|
||||
self.debug("forced to download")
|
||||
logger.debug("forced to download")
|
||||
self.download(url)
|
||||
elif not os.path.exists(path):
|
||||
self.debug("%s does not exist, downloading", path)
|
||||
logger.debug("%s does not exist, downloading", path)
|
||||
self.download(url)
|
||||
|
||||
if not os.path.exists(path):
|
||||
self.debug("%s does not exist, not in database", path)
|
||||
logger.debug("%s does not exist, not in database", path)
|
||||
return None
|
||||
|
||||
data = self._read(url)
|
||||
@@ -81,8 +84,8 @@ class AccuCache(log.Loggable):
|
||||
try:
|
||||
os.makedirs(os.path.dirname(path))
|
||||
except OSError, e:
|
||||
self.debug('Could not make dir %s: %r' % (
|
||||
path, log.getExceptionMessage(e)))
|
||||
logger.debug('Could not make dir %s: %r' % (
|
||||
path, str(e)))
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
|
||||
@@ -91,7 +94,7 @@ class AccuCache(log.Loggable):
|
||||
handle.close()
|
||||
|
||||
def _read(self, url):
|
||||
self.debug("Reading %s from cache", url)
|
||||
logger.debug("Reading %s from cache", url)
|
||||
path = self._getPath(url)
|
||||
handle = open(path, 'rb')
|
||||
data = handle.read()
|
||||
|
||||
@@ -29,10 +29,10 @@ import shutil
|
||||
from morituri.result import result
|
||||
from morituri.common import directory
|
||||
|
||||
from morituri.extern.log import log
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Persister(log.Loggable):
|
||||
class Persister:
|
||||
"""
|
||||
I wrap an optional pickle to persist an object to disk.
|
||||
|
||||
@@ -88,7 +88,7 @@ class Persister(log.Loggable):
|
||||
handle.close()
|
||||
# do an atomic move
|
||||
shutil.move(path, self._path)
|
||||
self.debug('saved persisted object to %r' % self._path)
|
||||
logger.debug('saved persisted object to %r' % self._path)
|
||||
|
||||
def _unpickle(self, default=None):
|
||||
self.object = default
|
||||
@@ -104,7 +104,7 @@ class Persister(log.Loggable):
|
||||
|
||||
try:
|
||||
self.object = pickle.load(handle)
|
||||
self.debug('loaded persisted object from %r' % self._path)
|
||||
logger.debug('loaded persisted object from %r' % self._path)
|
||||
except:
|
||||
# can fail for various reasons; in that case, pretend we didn't
|
||||
# load it
|
||||
@@ -115,7 +115,7 @@ class Persister(log.Loggable):
|
||||
os.unlink(self._path)
|
||||
|
||||
|
||||
class PersistedCache(log.Loggable):
|
||||
class PersistedCache:
|
||||
"""
|
||||
I wrap a directory of persisted objects.
|
||||
"""
|
||||
@@ -142,7 +142,7 @@ class PersistedCache(log.Loggable):
|
||||
if hasattr(persister.object, 'instanceVersion'):
|
||||
o = persister.object
|
||||
if o.instanceVersion < o.__class__.classVersion:
|
||||
self.debug(
|
||||
logger.debug(
|
||||
'key %r persisted object version %d is outdated',
|
||||
key, o.instanceVersion)
|
||||
persister.object = None
|
||||
@@ -152,7 +152,7 @@ class PersistedCache(log.Loggable):
|
||||
return persister
|
||||
|
||||
|
||||
class ResultCache(log.Loggable):
|
||||
class ResultCache:
|
||||
|
||||
def __init__(self, path=None):
|
||||
self._path = path or directory.cache_path('result')
|
||||
@@ -168,16 +168,16 @@ class ResultCache(log.Loggable):
|
||||
presult = self._pcache.get(cddbdiscid)
|
||||
|
||||
if not presult.object:
|
||||
self.debug('result for cddbdiscid %r not in cache', cddbdiscid)
|
||||
logger.debug('result for cddbdiscid %r not in cache', cddbdiscid)
|
||||
if not create:
|
||||
self.debug('returning None')
|
||||
logger.debug('returning None')
|
||||
return None
|
||||
|
||||
self.debug('creating result')
|
||||
logger.debug('creating result')
|
||||
presult.object = result.RipResult()
|
||||
presult.persist(presult.object)
|
||||
else:
|
||||
self.debug('result for cddbdiscid %r found in cache, reusing',
|
||||
logger.debug('result for cddbdiscid %r found in cache, reusing',
|
||||
cddbdiscid)
|
||||
|
||||
return presult
|
||||
@@ -188,7 +188,7 @@ class ResultCache(log.Loggable):
|
||||
return [os.path.splitext(os.path.basename(path))[0] for path in paths]
|
||||
|
||||
|
||||
class TableCache(log.Loggable):
|
||||
class TableCache:
|
||||
|
||||
"""
|
||||
I read and write entries to and from the cache of tables.
|
||||
@@ -215,11 +215,11 @@ class TableCache(log.Loggable):
|
||||
ptable = self._pcache.get(cddbdiscid)
|
||||
if ptable.object:
|
||||
if ptable.object.getMusicBrainzDiscId() != mbdiscid:
|
||||
self.debug('cached table is for different mb id %r' % (
|
||||
logger.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' %
|
||||
logger.debug('no valid cached table found for %r' %
|
||||
cddbdiscid)
|
||||
|
||||
if not ptable.object:
|
||||
|
||||
@@ -26,21 +26,21 @@ import zlib
|
||||
|
||||
import gst
|
||||
|
||||
from morituri.common import common
|
||||
from morituri.common import common, task
|
||||
from morituri.common import gstreamer as cgstreamer
|
||||
from morituri.common import log
|
||||
from morituri.common import task
|
||||
|
||||
from morituri.extern.task import gstreamer
|
||||
from morituri.extern.task import task as etask
|
||||
|
||||
from morituri.program.arc import accuraterip_checksum
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# checksums are not CRC's. a CRC is a specific type of checksum.
|
||||
|
||||
|
||||
class ChecksumTask(log.Loggable, gstreamer.GstPipelineTask):
|
||||
class ChecksumTask(gstreamer.GstPipelineTask):
|
||||
"""
|
||||
I am a task that calculates a checksum of the decoded audio data.
|
||||
|
||||
@@ -71,11 +71,11 @@ class ChecksumTask(log.Loggable, gstreamer.GstPipelineTask):
|
||||
|
||||
# use repr/%r because path can be unicode
|
||||
if sampleLength < 0:
|
||||
self.debug(
|
||||
logger.debug(
|
||||
'Creating checksum task on %r from sample %d until the end',
|
||||
path, sampleStart)
|
||||
else:
|
||||
self.debug(
|
||||
logger.debug(
|
||||
'Creating checksum task on %r from sample %d for %d samples',
|
||||
path, sampleStart, sampleLength)
|
||||
|
||||
@@ -109,7 +109,7 @@ class ChecksumTask(log.Loggable, gstreamer.GstPipelineTask):
|
||||
# get length in samples of file
|
||||
sink = self.pipeline.get_by_name('sink')
|
||||
|
||||
self.debug('query duration')
|
||||
logger.debug('query duration')
|
||||
try:
|
||||
length, qformat = sink.query_duration(gst.FORMAT_DEFAULT)
|
||||
except gst.QueryError, e:
|
||||
@@ -118,9 +118,9 @@ class ChecksumTask(log.Loggable, gstreamer.GstPipelineTask):
|
||||
|
||||
# wavparse 0.10.14 returns in bytes
|
||||
if qformat == gst.FORMAT_BYTES:
|
||||
self.debug('query returned in BYTES format')
|
||||
logger.debug('query returned in BYTES format')
|
||||
length /= 4
|
||||
self.debug('total sample length of file: %r', length)
|
||||
logger.debug('total sample length of file: %r', length)
|
||||
|
||||
return length
|
||||
|
||||
@@ -134,40 +134,42 @@ class ChecksumTask(log.Loggable, gstreamer.GstPipelineTask):
|
||||
|
||||
if self._sampleLength < 0:
|
||||
self._sampleLength = length - self._sampleStart
|
||||
self.debug('sampleLength is queried as %d samples',
|
||||
logger.debug('sampleLength is queried as %d samples',
|
||||
self._sampleLength)
|
||||
else:
|
||||
self.debug('sampleLength is known, and is %d samples' %
|
||||
logger.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)
|
||||
logger.debug('sampleEnd is sample %d' % self._sampleEnd)
|
||||
|
||||
self.debug('event')
|
||||
logger.debug('event')
|
||||
|
||||
|
||||
if self._sampleStart == 0 and self._sampleEnd + 1 == length:
|
||||
self.debug('No need to seek, crcing full file')
|
||||
logger.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)' % (
|
||||
logger.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)
|
||||
logger.debug('event sent, result %r', result)
|
||||
if not result:
|
||||
self.error('Failed to select samples with GStreamer seek event')
|
||||
msg = 'Failed to select samples with GStreamer seek event'
|
||||
logger.critical(msg)
|
||||
raise Exception(msg)
|
||||
sink.connect('new-buffer', self._new_buffer_cb)
|
||||
sink.connect('eos', self._eos_cb)
|
||||
|
||||
self.debug('scheduling setting to play')
|
||||
logger.debug('scheduling setting to play')
|
||||
# since set_state returns non-False, adding it as timeout_add
|
||||
# will repeatedly call it, and block the main loop; so
|
||||
# gobject.timeout_add(0L, self.pipeline.set_state, gst.STATE_PLAYING)
|
||||
@@ -179,30 +181,30 @@ class ChecksumTask(log.Loggable, gstreamer.GstPipelineTask):
|
||||
self.schedule(0, play)
|
||||
|
||||
#self.pipeline.set_state(gst.STATE_PLAYING)
|
||||
self.debug('scheduled setting to play')
|
||||
logger.debug('scheduled setting to play')
|
||||
|
||||
def stopped(self):
|
||||
self.debug('stopped')
|
||||
logger.debug('stopped')
|
||||
if not self._last:
|
||||
# see http://bugzilla.gnome.org/show_bug.cgi?id=578612
|
||||
self.debug(
|
||||
logger.debug(
|
||||
'not a single buffer gotten, setting exception EmptyError')
|
||||
self.setException(common.EmptyError('not a single buffer gotten'))
|
||||
return
|
||||
else:
|
||||
self._checksum = self._checksum % 2 ** 32
|
||||
self.debug("last buffer's sample offset %r", self._last.offset)
|
||||
self.debug("last buffer's sample size %r", len(self._last) / 4)
|
||||
logger.debug("last buffer's sample offset %r", self._last.offset)
|
||||
logger.debug("last buffer's sample size %r", len(self._last) / 4)
|
||||
last = self._last.offset + len(self._last) / 4 - 1
|
||||
self.debug("last sample offset in buffer: %r", last)
|
||||
self.debug("requested sample end: %r", self._sampleEnd)
|
||||
self.debug("requested sample length: %r", self._sampleLength)
|
||||
self.debug("checksum: %08X", self._checksum)
|
||||
self.debug("bytes: %d", self._bytes)
|
||||
logger.debug("last sample offset in buffer: %r", last)
|
||||
logger.debug("requested sample end: %r", self._sampleEnd)
|
||||
logger.debug("requested sample length: %r", self._sampleLength)
|
||||
logger.debug("checksum: %08X", self._checksum)
|
||||
logger.debug("bytes: %d", self._bytes)
|
||||
if self._sampleEnd != last:
|
||||
msg = 'did not get all samples, %d of %d missing' % (
|
||||
self._sampleEnd - last, self._sampleEnd)
|
||||
self.warning(msg)
|
||||
logger.warning(msg)
|
||||
self.setExceptionAndTraceback(common.MissingFrames(msg))
|
||||
return
|
||||
|
||||
@@ -231,7 +233,7 @@ class ChecksumTask(log.Loggable, gstreamer.GstPipelineTask):
|
||||
buf.offset, buf.size))
|
||||
if self._first is None:
|
||||
self._first = buf.offset
|
||||
self.debug('first sample is sample offset %r', self._first)
|
||||
logger.debug('first sample is sample offset %r', self._first)
|
||||
self._last = buf
|
||||
|
||||
assert len(buf) % 4 == 0, "buffer is not a multiple of 4 bytes"
|
||||
@@ -257,7 +259,7 @@ class ChecksumTask(log.Loggable, gstreamer.GstPipelineTask):
|
||||
def _eos_cb(self, sink):
|
||||
# get the last one; FIXME: why does this not get to us before ?
|
||||
#self._new_buffer_cb(sink)
|
||||
self.debug('eos, scheduling stop')
|
||||
logger.debug('eos, scheduling stop')
|
||||
self.schedule(0, self.stop)
|
||||
|
||||
|
||||
@@ -335,7 +337,7 @@ class AccurateRipChecksumTask(ChecksumTask):
|
||||
if self._trackNumber == self._trackCount:
|
||||
discFrameLength = self._sampleLength / common.SAMPLES_PER_FRAME
|
||||
if self._discFrameCounter > discFrameLength - 5:
|
||||
self.debug('skipping frame %d', self._discFrameCounter)
|
||||
logger.debug('skipping frame %d', self._discFrameCounter)
|
||||
return checksum
|
||||
|
||||
# self._bytes is updated after do_checksum_buffer
|
||||
|
||||
@@ -28,7 +28,9 @@ import math
|
||||
import subprocess
|
||||
|
||||
from morituri.extern import asyncsub
|
||||
from morituri.extern.log import log
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
FRAMES_PER_SECOND = 75
|
||||
|
||||
@@ -273,22 +275,21 @@ def getRelativePath(targetPath, collectionPath):
|
||||
|
||||
Used to determine the path to use in .cue/.m3u files
|
||||
"""
|
||||
log.debug('common', 'getRelativePath: target %r, collection %r' % (
|
||||
logger.debug('getRelativePath: target %r, collection %r' % (
|
||||
targetPath, collectionPath))
|
||||
|
||||
targetDir = os.path.dirname(targetPath)
|
||||
collectionDir = os.path.dirname(collectionPath)
|
||||
if targetDir == collectionDir:
|
||||
log.debug('common',
|
||||
'getRelativePath: target and collection in same dir')
|
||||
logger.debug('getRelativePath: target and collection in same dir')
|
||||
return os.path.basename(targetPath)
|
||||
else:
|
||||
rel = os.path.relpath(
|
||||
targetDir + os.path.sep,
|
||||
collectionDir + os.path.sep)
|
||||
log.debug('common',
|
||||
'getRelativePath: target and collection in different dir, %r' %
|
||||
rel)
|
||||
logger.debug(
|
||||
'getRelativePath: target and collection in different dir, %r' % rel
|
||||
)
|
||||
return os.path.join(rel, os.path.basename(targetPath))
|
||||
|
||||
|
||||
|
||||
@@ -20,17 +20,19 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with morituri. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ConfigParser
|
||||
import codecs
|
||||
import os.path
|
||||
import shutil
|
||||
import urllib
|
||||
import codecs
|
||||
import tempfile
|
||||
import ConfigParser
|
||||
import urllib
|
||||
|
||||
from morituri.common import directory, log
|
||||
from morituri.common import directory
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Config(log.Loggable):
|
||||
class Config:
|
||||
|
||||
def __init__(self, path=None):
|
||||
self._path = path or directory.config_path()
|
||||
@@ -45,8 +47,8 @@ class Config(log.Loggable):
|
||||
with codecs.open(self._path, 'r', encoding='utf-8') as f:
|
||||
self._parser.readfp(f)
|
||||
|
||||
self.info('Loaded %d sections from config file' %
|
||||
len(self._parser.sections()))
|
||||
logger.info('Loaded %d sections from config file' %
|
||||
len(self._parser.sections()))
|
||||
|
||||
def write(self):
|
||||
fd, path = tempfile.mkstemp(suffix=u'.moriturirc')
|
||||
@@ -121,13 +123,14 @@ class Config(log.Loggable):
|
||||
if not name.startswith('drive:'):
|
||||
continue
|
||||
|
||||
self.debug('Looking at section %r' % name)
|
||||
logger.debug('Looking at section %r' % name)
|
||||
conf = {}
|
||||
for key in ['vendor', 'model', 'release']:
|
||||
locals()[key] = locals()[key].strip()
|
||||
conf[key] = self._parser.get(name, key)
|
||||
self.debug("%s: '%s' versus '%s'" % (
|
||||
key, locals()[key], conf[key]))
|
||||
logger.debug("%s: '%s' versus '%s'" % (
|
||||
key, locals()[key], conf[key]
|
||||
))
|
||||
if vendor.strip() != conf['vendor']:
|
||||
continue
|
||||
if model.strip() != conf['model']:
|
||||
@@ -154,5 +157,3 @@ class Config(log.Loggable):
|
||||
self.write()
|
||||
|
||||
return self._findDriveSection(vendor, model, release)
|
||||
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
|
||||
import os
|
||||
|
||||
from morituri.common import log
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def _listify(listOrString):
|
||||
if type(listOrString) == str:
|
||||
@@ -37,7 +37,7 @@ def getAllDevicePaths():
|
||||
# see https://savannah.gnu.org/bugs/index.php?38477
|
||||
return [str(dev) for dev in _getAllDevicePathsPyCdio()]
|
||||
except ImportError:
|
||||
log.info('drive', 'Cannot import pycdio')
|
||||
logger.info('Cannot import pycdio')
|
||||
return _getAllDevicePathsStatic()
|
||||
|
||||
|
||||
|
||||
@@ -25,15 +25,17 @@ import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from morituri.common import common, log
|
||||
from morituri.common import common
|
||||
from morituri.common import gstreamer as cgstreamer
|
||||
from morituri.common import task as ctask
|
||||
|
||||
from morituri.extern.task import task, gstreamer
|
||||
from morituri.program import sox
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Profile(log.Loggable):
|
||||
class Profile:
|
||||
|
||||
name = None
|
||||
extension = None
|
||||
@@ -107,7 +109,7 @@ class _LameProfile(Profile):
|
||||
|
||||
def test(self):
|
||||
version = cgstreamer.elementFactoryVersion('lamemp3enc')
|
||||
self.debug('lamemp3enc version: %r', version)
|
||||
logger.debug('lamemp3enc version: %r', version)
|
||||
if version:
|
||||
t = tuple([int(s) for s in version.split('.')])
|
||||
if t >= (0, 10, 19):
|
||||
@@ -115,7 +117,7 @@ class _LameProfile(Profile):
|
||||
return True
|
||||
|
||||
version = cgstreamer.elementFactoryVersion('lame')
|
||||
self.debug('lame version: %r', version)
|
||||
logger.debug('lame version: %r', version)
|
||||
if version:
|
||||
self.pipeline = self._lame_pipeline
|
||||
return True
|
||||
@@ -246,13 +248,12 @@ class EncodeTask(ctask.GstPipelineTask):
|
||||
try:
|
||||
tagger.merge_tags(self._taglist, self.gst.TAG_MERGE_APPEND)
|
||||
except AttributeError, e:
|
||||
self.warning('Could not merge tags: %r',
|
||||
log.getExceptionMessage(e))
|
||||
logger.warning('Could not merge tags: %r', str(e))
|
||||
|
||||
def paused(self):
|
||||
# get length
|
||||
identity = self.pipeline.get_by_name('identity')
|
||||
self.debug('query duration')
|
||||
logger.debug('query duration')
|
||||
try:
|
||||
length, qformat = identity.query_duration(self.gst.FORMAT_DEFAULT)
|
||||
except self.gst.QueryError, e:
|
||||
@@ -263,16 +264,16 @@ class EncodeTask(ctask.GstPipelineTask):
|
||||
|
||||
# wavparse 0.10.14 returns in bytes
|
||||
if qformat == self.gst.FORMAT_BYTES:
|
||||
self.debug('query returned in BYTES format')
|
||||
logger.debug('query returned in BYTES format')
|
||||
length /= 4
|
||||
self.debug('total length: %r', length)
|
||||
logger.debug('total length: %r', length)
|
||||
self._length = length
|
||||
|
||||
duration = None
|
||||
try:
|
||||
duration, qformat = identity.query_duration(self.gst.FORMAT_TIME)
|
||||
except self.gst.QueryError, e:
|
||||
self.debug('Could not query duration')
|
||||
logger.debug('Could not query duration')
|
||||
self._duration = duration
|
||||
|
||||
# set up level callbacks
|
||||
@@ -289,7 +290,7 @@ class EncodeTask(ctask.GstPipelineTask):
|
||||
interval = self.gst.SECOND
|
||||
if interval > duration:
|
||||
interval = duration / 2
|
||||
self.debug('Setting level interval to %s, duration %s',
|
||||
logger.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
|
||||
@@ -310,7 +311,7 @@ class EncodeTask(ctask.GstPipelineTask):
|
||||
return True
|
||||
|
||||
def bus_eos_cb(self, bus, message):
|
||||
self.debug('eos, scheduling stop')
|
||||
logger.debug('eos, scheduling stop')
|
||||
self.schedule(0, self.stop)
|
||||
|
||||
def _message_element_cb(self, bus, message):
|
||||
@@ -327,7 +328,7 @@ class EncodeTask(ctask.GstPipelineTask):
|
||||
|
||||
for p in s['peak']:
|
||||
if self._peakdB < p:
|
||||
self.log('higher peakdB found, now %r', self._peakdB)
|
||||
logger.debug('higher peakdB found, now %r', self._peakdB)
|
||||
self._peakdB = p
|
||||
|
||||
# FIXME: works around a bug on F-15 where buffer probes don't seem
|
||||
@@ -338,19 +339,19 @@ class EncodeTask(ctask.GstPipelineTask):
|
||||
|
||||
def stopped(self):
|
||||
if self._peakdB is not None:
|
||||
self.debug('peakdB %r', self._peakdB)
|
||||
logger.debug('peakdB %r', self._peakdB)
|
||||
self.peak = math.sqrt(math.pow(10, self._peakdB / 10.0))
|
||||
return
|
||||
|
||||
self.warning('No peak found.')
|
||||
logger.warning('No peak found.')
|
||||
|
||||
self.peak = 0.0
|
||||
|
||||
if self._duration:
|
||||
self.warning('GStreamer level element did not send messages.')
|
||||
logger.warning('GStreamer level element did not send messages.')
|
||||
# workaround for when the file is too short to have volume ?
|
||||
if self._length == common.SAMPLES_PER_FRAME:
|
||||
self.warning('only one frame of audio, setting peak to 0.0')
|
||||
logger.warning('only one frame of audio, setting peak to 0.0')
|
||||
self.peak = 0.0
|
||||
|
||||
class TagReadTask(ctask.GstPipelineTask):
|
||||
@@ -382,12 +383,12 @@ class TagReadTask(ctask.GstPipelineTask):
|
||||
gstreamer.quoteParse(self._path).encode('utf-8'))
|
||||
|
||||
def bus_eos_cb(self, bus, message):
|
||||
self.debug('eos, scheduling stop')
|
||||
logger.debug('eos, scheduling stop')
|
||||
self.schedule(0, self.stop)
|
||||
|
||||
def bus_tag_cb(self, bus, message):
|
||||
taglist = message.parse_tag()
|
||||
self.debug('tag_cb, %d tags' % len(taglist.keys()))
|
||||
logger.debug('tag_cb, %d tags' % len(taglist.keys()))
|
||||
if not self.taglist:
|
||||
self.taglist = taglist
|
||||
else:
|
||||
@@ -434,17 +435,17 @@ class TagWriteTask(ctask.LoggableTask):
|
||||
if self._taglist:
|
||||
tagger.merge_tags(self._taglist, gst.TAG_MERGE_APPEND)
|
||||
|
||||
self.debug('pausing pipeline')
|
||||
logger.debug('pausing pipeline')
|
||||
self._pipeline.set_state(gst.STATE_PAUSED)
|
||||
self._pipeline.get_state()
|
||||
self.debug('paused pipeline')
|
||||
logger.debug('paused pipeline')
|
||||
|
||||
# add eos handling
|
||||
bus = self._pipeline.get_bus()
|
||||
bus.add_signal_watch()
|
||||
bus.connect('message::eos', self._message_eos_cb)
|
||||
|
||||
self.debug('scheduling setting to play')
|
||||
logger.debug('scheduling setting to play')
|
||||
# since set_state returns non-False, adding it as timeout_add
|
||||
# will repeatedly call it, and block the main loop; so
|
||||
# gobject.timeout_add(0L, self._pipeline.set_state,
|
||||
@@ -457,20 +458,20 @@ class TagWriteTask(ctask.LoggableTask):
|
||||
self.schedule(0, play)
|
||||
|
||||
#self._pipeline.set_state(gst.STATE_PLAYING)
|
||||
self.debug('scheduled setting to play')
|
||||
logger.debug('scheduled setting to play')
|
||||
|
||||
def _message_eos_cb(self, bus, message):
|
||||
self.debug('eos, scheduling stop')
|
||||
logger.debug('eos, scheduling stop')
|
||||
self.schedule(0, self.stop)
|
||||
|
||||
def stop(self):
|
||||
# here to avoid import gst eating our options
|
||||
import gst
|
||||
|
||||
self.debug('stopping')
|
||||
self.debug('setting state to NULL')
|
||||
logger.debug('stopping')
|
||||
logger.debug('setting state to NULL')
|
||||
self._pipeline.set_state(gst.STATE_NULL)
|
||||
self.debug('set state to NULL')
|
||||
logger.debug('set state to NULL')
|
||||
task.Task.stop(self)
|
||||
|
||||
|
||||
@@ -512,12 +513,12 @@ class SafeRetagTask(ctask.LoggableMultiSeparateTask):
|
||||
if taskk == self.tasks[0]:
|
||||
taglist = taskk.taglist.copy()
|
||||
if common.tagListEquals(taglist, self._taglist):
|
||||
self.debug('tags are already fine: %r',
|
||||
logger.debug('tags are already fine: %r',
|
||||
common.tagListToDict(taglist))
|
||||
else:
|
||||
# need to retag
|
||||
self.debug('tags need to be rewritten')
|
||||
self.debug('Current tags: %r, new tags: %r',
|
||||
logger.debug('tags need to be rewritten')
|
||||
logger.debug('Current tags: %r, new tags: %r',
|
||||
common.tagListToDict(taglist),
|
||||
common.tagListToDict(self._taglist))
|
||||
assert common.tagListToDict(taglist) \
|
||||
@@ -531,15 +532,15 @@ class SafeRetagTask(ctask.LoggableMultiSeparateTask):
|
||||
self.tasks.append(TagReadTask(self._tmppath))
|
||||
elif len(self.tasks) > 1 and taskk == self.tasks[4]:
|
||||
if common.tagListEquals(self.tasks[4].taglist, self._taglist):
|
||||
self.debug('tags written successfully')
|
||||
logger.debug('tags written successfully')
|
||||
c1 = self.tasks[1].checksum
|
||||
c2 = self.tasks[3].checksum
|
||||
self.debug('comparing checksums %08x and %08x' % (c1, c2))
|
||||
logger.debug('comparing checksums %08x and %08x' % (c1, c2))
|
||||
if c1 == c2:
|
||||
# data is fine, so we can now move
|
||||
# but first, copy original mode to our temporary file
|
||||
shutil.copymode(self._path, self._tmppath)
|
||||
self.debug('moving temporary file to %r' % self._path)
|
||||
logger.debug('moving temporary file to %r' % self._path)
|
||||
os.rename(self._tmppath, self._path)
|
||||
self.changed = True
|
||||
else:
|
||||
@@ -547,9 +548,9 @@ class SafeRetagTask(ctask.LoggableMultiSeparateTask):
|
||||
e = TypeError("Checksums failed")
|
||||
self.setAndRaiseException(e)
|
||||
else:
|
||||
self.debug('failed to update tags, only have %r',
|
||||
logger.debug('failed to update tags, only have %r',
|
||||
common.tagListToDict(self.tasks[4].taglist))
|
||||
self.debug('difference: %r',
|
||||
logger.debug('difference: %r',
|
||||
common.tagListDifference(self.tasks[4].taglist,
|
||||
self._taglist))
|
||||
os.unlink(self._tmppath)
|
||||
|
||||
@@ -23,13 +23,14 @@
|
||||
import re
|
||||
import commands
|
||||
|
||||
from morituri.common import log
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# workaround for issue #64
|
||||
|
||||
|
||||
def removeAudioParsers():
|
||||
log.debug('gstreamer', 'Removing buggy audioparsers plugin if needed')
|
||||
logger.debug('Removing buggy audioparsers plugin if needed')
|
||||
|
||||
import gst
|
||||
registry = gst.registry_get_default()
|
||||
@@ -37,12 +38,12 @@ def removeAudioParsers():
|
||||
plugin = registry.find_plugin("audioparsersbad")
|
||||
if plugin:
|
||||
# always remove from bad
|
||||
log.debug('gstreamer', 'removing audioparsersbad plugin from registry')
|
||||
logger.debug('removing audioparsersbad plugin from registry')
|
||||
registry.remove_plugin(plugin)
|
||||
|
||||
plugin = registry.find_plugin("audioparsers")
|
||||
if plugin:
|
||||
log.debug('gstreamer', 'removing audioparsers plugin from %s %s',
|
||||
logger.debug('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,33 +0,0 @@
|
||||
# -*- Mode: Python -*-
|
||||
# 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/>.
|
||||
|
||||
"""
|
||||
Logging
|
||||
"""
|
||||
|
||||
from morituri.extern.log import log as externlog
|
||||
from morituri.extern.log.log import *
|
||||
|
||||
|
||||
def init():
|
||||
externlog.init('RIP_DEBUG')
|
||||
externlog.setPackageScrubList('morituri')
|
||||
@@ -1,68 +0,0 @@
|
||||
# -*- Mode: Python -*-
|
||||
# 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/>.
|
||||
|
||||
"""
|
||||
Logging Command.
|
||||
"""
|
||||
|
||||
from morituri.extern.command import command
|
||||
from morituri.common import log
|
||||
|
||||
|
||||
class LogCommand(command.Command, log.Loggable):
|
||||
|
||||
def __init__(self, parentCommand=None, **kwargs):
|
||||
command.Command.__init__(self, parentCommand, **kwargs)
|
||||
self.logCategory = self.name
|
||||
|
||||
def parse(self, argv):
|
||||
cmd = self.getRootCommand()
|
||||
if hasattr(cmd, 'config'):
|
||||
config = cmd.config
|
||||
# find section name
|
||||
cmd = self
|
||||
section = []
|
||||
while cmd is not None:
|
||||
section.insert(0, cmd.name)
|
||||
cmd = cmd.parentCommand
|
||||
section = '.'.join(section)
|
||||
# get defaults from config
|
||||
defaults = {}
|
||||
for opt in self.parser.option_list:
|
||||
if opt.dest is None:
|
||||
continue
|
||||
if 'string' == opt.type:
|
||||
val = config.get(section, opt.dest)
|
||||
elif opt.action in ('store_false', 'store_true'):
|
||||
val = config.getboolean(section, opt.dest)
|
||||
else:
|
||||
val = None
|
||||
if val is not None:
|
||||
defaults[opt.dest] = val
|
||||
self.parser.set_defaults(**defaults)
|
||||
command.Command.parse(self, argv)
|
||||
|
||||
# command.Command has a fake debug method, so choose the right one
|
||||
|
||||
def debug(self, format, *args):
|
||||
kwargs = {}
|
||||
log.Loggable.doLog(self, log.DEBUG, -2, format, *args, **kwargs)
|
||||
@@ -26,7 +26,8 @@ Handles communication with the musicbrainz server using NGS.
|
||||
|
||||
import urllib2
|
||||
|
||||
from morituri.common import log
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
VA_ID = "89ad4ac3-39f7-470e-963a-56509c546377" # Various Artists
|
||||
@@ -93,7 +94,7 @@ def _record(record, which, name, what):
|
||||
handle = open(filename, 'w')
|
||||
handle.write(json.dumps(what))
|
||||
handle.close()
|
||||
log.info('mbngs', 'Wrote %s %s to %s', which, name, filename)
|
||||
logger.info('Wrote %s %s to %s', which, name, filename)
|
||||
|
||||
# credit is of the form [dict, str, dict, ... ]
|
||||
# e.g. [
|
||||
@@ -152,16 +153,16 @@ def _getMetadata(releaseShort, release, discid, country=None):
|
||||
|
||||
@rtype: L{DiscMetadata} or None
|
||||
"""
|
||||
log.debug('program', 'getMetadata for release id %r',
|
||||
logger.debug('getMetadata for release id %r',
|
||||
release['id'])
|
||||
if not release['id']:
|
||||
log.warning('program', 'No id for release %r', release)
|
||||
logger.warning('No id for release %r', release)
|
||||
return None
|
||||
|
||||
assert release['id'], 'Release does not have an id'
|
||||
|
||||
if 'country' in release and country and release['country'] != country:
|
||||
log.warning('program', '%r was not released in %r', release, country)
|
||||
logger.warning('%r was not released in %r', release, country)
|
||||
return None
|
||||
|
||||
discMD = DiscMetadata()
|
||||
@@ -176,7 +177,7 @@ def _getMetadata(releaseShort, release, discid, country=None):
|
||||
|
||||
|
||||
if len(discCredit) > 1:
|
||||
log.debug('mbngs', 'artist-credit more than 1: %r', discCredit)
|
||||
logger.debug('artist-credit more than 1: %r', discCredit)
|
||||
|
||||
albumArtistName = discCredit.getName()
|
||||
|
||||
@@ -185,7 +186,7 @@ def _getMetadata(releaseShort, release, discid, country=None):
|
||||
discMD.sortName = discCredit.getSortName()
|
||||
# FIXME: is format str ?
|
||||
if not 'date' in release:
|
||||
log.warning('mbngs', 'Release %r does not have date', release)
|
||||
logger.warning('Release %r does not have date', release)
|
||||
else:
|
||||
discMD.release = release['date']
|
||||
|
||||
@@ -219,8 +220,8 @@ def _getMetadata(releaseShort, release, discid, country=None):
|
||||
track = TrackMetadata()
|
||||
trackCredit = _Credit(t['recording']['artist-credit'])
|
||||
if len(trackCredit) > 1:
|
||||
log.debug('mbngs',
|
||||
'artist-credit more than 1: %r', trackCredit)
|
||||
logger.debug('artist-credit more than 1: %r',
|
||||
trackCredit)
|
||||
|
||||
# FIXME: leftover comment, need an example
|
||||
# various artists discs can have tracks with no artist
|
||||
@@ -234,8 +235,7 @@ def _getMetadata(releaseShort, release, discid, country=None):
|
||||
# FIXME: unit of duration ?
|
||||
track.duration = int(t['recording'].get('length', 0))
|
||||
if not track.duration:
|
||||
log.warning('getMetadata',
|
||||
'track %r (%r) does not have duration' % (
|
||||
logger.warning('track %r (%r) does not have duration' % (
|
||||
track.title, track.mbid))
|
||||
tainted = True
|
||||
else:
|
||||
@@ -266,7 +266,7 @@ def musicbrainz(discid, country=None, record=False):
|
||||
|
||||
@rtype: list of L{DiscMetadata}
|
||||
"""
|
||||
log.debug('musicbrainzngs', 'looking up results for discid %r', discid)
|
||||
logger.debug('looking up results for discid %r', discid)
|
||||
import musicbrainzngs
|
||||
|
||||
ret = []
|
||||
@@ -279,14 +279,13 @@ def musicbrainz(discid, country=None, record=False):
|
||||
if e.cause.code == 404:
|
||||
raise NotFoundException(e)
|
||||
else:
|
||||
log.debug('musicbrainzngs',
|
||||
'received bad response from the server')
|
||||
logger.debug('received bad response from the server')
|
||||
|
||||
raise MusicBrainzException(e)
|
||||
|
||||
# The result can either be a "disc" or a "cdstub"
|
||||
if result.get('disc'):
|
||||
log.debug('musicbrainzngs', 'found %d releases for discid %r',
|
||||
logger.debug('found %d releases for discid %r',
|
||||
len(result['disc']['release-list']), discid)
|
||||
_record(record, 'releases', discid, result)
|
||||
|
||||
@@ -295,7 +294,7 @@ def musicbrainz(discid, country=None, record=False):
|
||||
import json
|
||||
for release in result['disc']['release-list']:
|
||||
formatted = json.dumps(release, sort_keys=False, indent=4)
|
||||
log.debug('program', 'result %s: artist %r, title %r' % (
|
||||
logger.debug('result %s: artist %r, title %r' % (
|
||||
formatted, release['artist-credit-phrase'], release['title']))
|
||||
|
||||
# to get titles of recordings, we need to query the release with
|
||||
@@ -307,16 +306,16 @@ def musicbrainz(discid, country=None, record=False):
|
||||
_record(record, 'release', release['id'], res)
|
||||
releaseDetail = res['release']
|
||||
formatted = json.dumps(releaseDetail, sort_keys=False, indent=4)
|
||||
log.debug('program', 'release %s' % formatted)
|
||||
logger.debug('release %s' % formatted)
|
||||
|
||||
md = _getMetadata(release, releaseDetail, discid, country)
|
||||
if md:
|
||||
log.debug('program', 'duration %r', md.duration)
|
||||
logger.debug('duration %r', md.duration)
|
||||
ret.append(md)
|
||||
|
||||
return ret
|
||||
elif result.get('cdstub'):
|
||||
log.debug('musicbrainzngs', 'query returned cdstub: ignored')
|
||||
logger.debug('query returned cdstub: ignored')
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -24,22 +24,24 @@
|
||||
Common functionality and class for all programs using morituri.
|
||||
"""
|
||||
|
||||
import musicbrainzngs
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from morituri.common import common, log, mbngs, cache, path
|
||||
from morituri.common import common, mbngs, cache, path
|
||||
from morituri.program import cdrdao, cdparanoia
|
||||
from morituri.image import image
|
||||
|
||||
from morituri.extern.task import task, gstreamer
|
||||
import musicbrainzngs
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# FIXME: should Program have a runner ?
|
||||
|
||||
|
||||
class Program(log.Loggable):
|
||||
class Program:
|
||||
"""
|
||||
I maintain program state and functionality.
|
||||
|
||||
@@ -85,7 +87,7 @@ class Program(log.Loggable):
|
||||
|
||||
def setWorkingDirectory(self, workingDirectory):
|
||||
if workingDirectory:
|
||||
self.info('Changing to working directory %s' % workingDirectory)
|
||||
logger.info('Changing to working directory %s' % workingDirectory)
|
||||
os.chdir(workingDirectory)
|
||||
|
||||
def loadDevice(self, device):
|
||||
@@ -108,7 +110,7 @@ class Program(log.Loggable):
|
||||
If the given device is a symlink, the target will be checked.
|
||||
"""
|
||||
device = os.path.realpath(device)
|
||||
self.debug('possibly unmount real path %r' % device)
|
||||
logger.debug('possibly unmount real path %r' % device)
|
||||
proc = open('/proc/mounts').read()
|
||||
if device in proc:
|
||||
print 'Device %s is mounted, unmounting' % device
|
||||
@@ -129,7 +131,7 @@ class Program(log.Loggable):
|
||||
from pkg_resources import parse_version as V
|
||||
version = cdrdao.getCDRDAOVersion()
|
||||
if V(version) < V('1.2.3rc2'):
|
||||
self.stdout.write('Warning: cdrdao older than 1.2.3 has a '
|
||||
sys.stdout.write('Warning: cdrdao older than 1.2.3 has a '
|
||||
'pre-gap length bug.\n'
|
||||
'See http://sourceforge.net/tracker/?func=detail'
|
||||
'&aid=604751&group_id=2171&atid=102171\n')
|
||||
@@ -158,24 +160,24 @@ class Program(log.Loggable):
|
||||
itable = tdict[offset]
|
||||
|
||||
if not itable:
|
||||
self.debug('getTable: cddbdiscid %s, mbdiscid %s not in cache for offset %s, '
|
||||
logger.debug('getTable: cddbdiscid %s, mbdiscid %s not in cache for offset %s, '
|
||||
'reading table' % (
|
||||
cddbdiscid, mbdiscid, offset))
|
||||
t = cdrdao.ReadTableTask(device)
|
||||
itable = t.table
|
||||
tdict[offset] = itable
|
||||
ptable.persist(tdict)
|
||||
self.debug('getTable: read table %r' % itable)
|
||||
logger.debug('getTable: read table %r' % itable)
|
||||
else:
|
||||
self.debug('getTable: cddbdiscid %s, mbdiscid %s in cache for offset %s' % (
|
||||
logger.debug('getTable: cddbdiscid %s, mbdiscid %s in cache for offset %s' % (
|
||||
cddbdiscid, mbdiscid, offset))
|
||||
self.debug('getTable: loaded table %r' % itable)
|
||||
logger.debug('getTable: loaded table %r' % itable)
|
||||
|
||||
assert itable.hasTOC()
|
||||
|
||||
self.result.table = itable
|
||||
|
||||
self.debug('getTable: returning table with mb id %s' %
|
||||
logger.debug('getTable: returning table with mb id %s' %
|
||||
itable.getMusicBrainzDiscId())
|
||||
return itable
|
||||
|
||||
@@ -275,7 +277,7 @@ class Program(log.Loggable):
|
||||
elif self.metadata.barcode:
|
||||
templateParts[-2] += ' (%s)' % self.metadata.barcode
|
||||
template = os.path.join(*templateParts)
|
||||
self.debug('Disambiguated template to %r' % template)
|
||||
logger.debug('Disambiguated template to %r' % template)
|
||||
|
||||
import re
|
||||
template = re.sub(r'%(\w)', r'%(\1)s', template)
|
||||
@@ -296,7 +298,7 @@ class Program(log.Loggable):
|
||||
import CDDB
|
||||
try:
|
||||
code, md = CDDB.query(cddbdiscid)
|
||||
self.debug('CDDB query result: %r, %r', code, md)
|
||||
logger.debug('CDDB query result: %r, %r', code, md)
|
||||
if code == 200:
|
||||
return md['title']
|
||||
|
||||
@@ -317,7 +319,7 @@ class Program(log.Loggable):
|
||||
self._stdout.write('Disc duration: %s, %d audio tracks\n' % (
|
||||
common.formatTime(ittoc.duration() / 1000.0),
|
||||
ittoc.getAudioTracks()))
|
||||
self.debug('MusicBrainz submit url: %r',
|
||||
logger.debug('MusicBrainz submit url: %r',
|
||||
ittoc.getMusicBrainzSubmitURL())
|
||||
ret = None
|
||||
|
||||
@@ -385,7 +387,7 @@ class Program(log.Loggable):
|
||||
|
||||
if release:
|
||||
metadatas = [m for m in metadatas if m.url.endswith(release)]
|
||||
self.debug('Asked for release %r, only kept %r',
|
||||
logger.debug('Asked for release %r, only kept %r',
|
||||
release, metadatas)
|
||||
if len(metadatas) == 1:
|
||||
self._stdout.write('\n')
|
||||
@@ -410,11 +412,11 @@ class Program(log.Loggable):
|
||||
releaseTitle = metadatas[0].releaseTitle
|
||||
for i, metadata in enumerate(metadatas):
|
||||
if not artist == metadata.artist:
|
||||
self.warning("artist 0: %r and artist %d: %r "
|
||||
logger.warning("artist 0: %r and artist %d: %r "
|
||||
"are not the same" % (
|
||||
artist, i, metadata.artist))
|
||||
if not releaseTitle == metadata.releaseTitle:
|
||||
self.warning("title 0: %r and title %d: %r "
|
||||
logger.warning("title 0: %r and title %d: %r "
|
||||
"are not the same" % (
|
||||
releaseTitle, i, metadata.releaseTitle))
|
||||
|
||||
@@ -505,8 +507,7 @@ class Program(log.Loggable):
|
||||
# Jan and 1st if MM and DD are missing
|
||||
date = self.metadata.release
|
||||
if date:
|
||||
log.debug('metadata',
|
||||
'Converting release date %r to structure', date)
|
||||
logger.debug('Converting release date %r to structure', date)
|
||||
if len(date) == 4:
|
||||
date += '-01'
|
||||
if len(date) == 7:
|
||||
@@ -554,18 +555,17 @@ class Program(log.Loggable):
|
||||
runner.run(t)
|
||||
except task.TaskException, e:
|
||||
if isinstance(e.exception, common.MissingFrames):
|
||||
self.warning('missing frames for %r' % trackResult.filename)
|
||||
logger.warning('missing frames for %r' % trackResult.filename)
|
||||
return False
|
||||
elif isinstance(e.exception, gstreamer.GstException):
|
||||
self.warning('GstException %r' % (e.exception, ))
|
||||
logger.warning('GstException %r' % (e.exception, ))
|
||||
return False
|
||||
else:
|
||||
raise
|
||||
|
||||
ret = trackResult.testcrc == t.checksum
|
||||
log.debug('program',
|
||||
'verifyTrack: track result crc %r, file crc %r, result %r',
|
||||
trackResult.testcrc, t.checksum, ret)
|
||||
logger.debug('verifyTrack: track result crc %r, file crc %r, result %r',
|
||||
trackResult.testcrc, t.checksum, ret)
|
||||
return ret
|
||||
|
||||
def ripTrack(self, runner, trackResult, offset, device, profile, taglist,
|
||||
@@ -602,10 +602,10 @@ class Program(log.Loggable):
|
||||
|
||||
runner.run(t)
|
||||
|
||||
self.debug('ripped track')
|
||||
self.debug('test speed %.3f/%.3f seconds' % (
|
||||
logger.debug('ripped track')
|
||||
logger.debug('test speed %.3f/%.3f seconds' % (
|
||||
t.testspeed, t.testduration))
|
||||
self.debug('copy speed %.3f/%.3f seconds' % (
|
||||
logger.debug('copy speed %.3f/%.3f seconds' % (
|
||||
t.copyspeed, t.copyduration))
|
||||
trackResult.testcrc = t.testchecksum
|
||||
trackResult.copycrc = t.copychecksum
|
||||
@@ -619,7 +619,7 @@ class Program(log.Loggable):
|
||||
|
||||
if trackResult.filename != t.path:
|
||||
trackResult.filename = t.path
|
||||
self.info('Filename changed to %r', trackResult.filename)
|
||||
logger.info('Filename changed to %r', trackResult.filename)
|
||||
|
||||
def retagImage(self, runner, taglists):
|
||||
cueImage = image.Image(self.cuePath)
|
||||
@@ -634,7 +634,7 @@ class Program(log.Loggable):
|
||||
Will set accurip and friends on each TrackResult.
|
||||
"""
|
||||
|
||||
self.debug('verifying Image against %d AccurateRip responses',
|
||||
logger.debug('verifying Image against %d AccurateRip responses',
|
||||
len(responses or []))
|
||||
|
||||
cueImage = image.Image(self.cuePath)
|
||||
@@ -653,7 +653,7 @@ class Program(log.Loggable):
|
||||
|
||||
|
||||
if not responses:
|
||||
self.warning('No AccurateRip responses, cannot verify.')
|
||||
logger.warning('No AccurateRip responses, cannot verify.')
|
||||
return
|
||||
|
||||
# now loop to match responses
|
||||
@@ -667,7 +667,7 @@ class Program(log.Loggable):
|
||||
for j, r in enumerate(responses):
|
||||
if "%08x" % csum == r.checksums[i]:
|
||||
response = r
|
||||
self.debug(
|
||||
logger.debug(
|
||||
"Track %02d matched response %d of %d in "
|
||||
"AccurateRip database",
|
||||
i + 1, j + 1, len(responses))
|
||||
@@ -679,7 +679,7 @@ class Program(log.Loggable):
|
||||
trackResult.ARDBConfidence = confidence
|
||||
|
||||
if not trackResult.accurip:
|
||||
self.warning("Track %02d: not matched in AccurateRip database",
|
||||
logger.warning("Track %02d: not matched in AccurateRip database",
|
||||
i + 1)
|
||||
|
||||
# I have seen AccurateRip responses with 0 as confidence
|
||||
@@ -691,11 +691,11 @@ class Program(log.Loggable):
|
||||
maxConfidence = r.confidences[i]
|
||||
maxResponse = r
|
||||
|
||||
self.debug('Track %02d: found max confidence %d' % (
|
||||
logger.debug('Track %02d: found max confidence %d' % (
|
||||
i + 1, maxConfidence))
|
||||
trackResult.ARDBMaxConfidence = maxConfidence
|
||||
if not response:
|
||||
self.warning('Track %02d: none of the responses matched.',
|
||||
logger.warning('Track %02d: none of the responses matched.',
|
||||
i + 1)
|
||||
trackResult.ARDBCRC = int(
|
||||
maxResponse.checksums[i], 16)
|
||||
@@ -743,7 +743,7 @@ class Program(log.Loggable):
|
||||
def writeCue(self, discName):
|
||||
assert self.result.table.canCue()
|
||||
cuePath = '%s.cue' % discName
|
||||
self.debug('write .cue file to %s', cuePath)
|
||||
logger.debug('write .cue file to %s', cuePath)
|
||||
handle = open(cuePath, 'w')
|
||||
# FIXME: do we always want utf-8 ?
|
||||
handle.write(self.result.table.cue(cuePath).encode('utf-8'))
|
||||
|
||||
@@ -6,27 +6,29 @@ import signal
|
||||
import subprocess
|
||||
|
||||
from morituri.extern import asyncsub
|
||||
from morituri.extern.log import log
|
||||
from morituri.extern.task import task, gstreamer
|
||||
|
||||
# log.Loggable first to get logging
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SyncRunner(log.Loggable, task.SyncRunner):
|
||||
class SyncRunner(task.SyncRunner):
|
||||
pass
|
||||
|
||||
|
||||
class LoggableTask(log.Loggable, task.Task):
|
||||
pass
|
||||
|
||||
class LoggableMultiSeparateTask(log.Loggable, task.MultiSeparateTask):
|
||||
pass
|
||||
|
||||
class GstPipelineTask(log.Loggable, gstreamer.GstPipelineTask):
|
||||
class LoggableTask(task.Task):
|
||||
pass
|
||||
|
||||
|
||||
class PopenTask(log.Loggable, task.Task):
|
||||
class LoggableMultiSeparateTask(task.MultiSeparateTask):
|
||||
pass
|
||||
|
||||
|
||||
class GstPipelineTask(gstreamer.GstPipelineTask):
|
||||
pass
|
||||
|
||||
|
||||
class PopenTask(task.Task):
|
||||
"""
|
||||
I am a task that runs a command using Popen.
|
||||
"""
|
||||
@@ -51,7 +53,7 @@ class PopenTask(log.Loggable, task.Task):
|
||||
|
||||
raise
|
||||
|
||||
self.debug('Started %r with pid %d', self.command,
|
||||
logger.debug('Started %r with pid %d', self.command,
|
||||
self._popen.pid)
|
||||
|
||||
self.schedule(1.0, self._read, runner)
|
||||
@@ -63,14 +65,14 @@ class PopenTask(log.Loggable, task.Task):
|
||||
ret = self._popen.recv()
|
||||
|
||||
if ret:
|
||||
self.log("read from stdout: %s", ret)
|
||||
logger.debug("read from stdout: %s", ret)
|
||||
self.readbytesout(ret)
|
||||
read = True
|
||||
|
||||
ret = self._popen.recv_err()
|
||||
|
||||
if ret:
|
||||
self.log("read from stderr: %s", ret)
|
||||
logger.debug("read from stderr: %s", ret)
|
||||
self.readbyteserr(ret)
|
||||
read = True
|
||||
|
||||
@@ -89,8 +91,7 @@ class PopenTask(log.Loggable, task.Task):
|
||||
|
||||
self._done()
|
||||
except Exception, e:
|
||||
self.debug('exception during _read()')
|
||||
self.debug(log.getExceptionMessage(e))
|
||||
logger.debug('exception during _read(): %r', str(e))
|
||||
self.setException(e)
|
||||
self.stop()
|
||||
|
||||
@@ -98,9 +99,9 @@ class PopenTask(log.Loggable, task.Task):
|
||||
assert self._popen.returncode is not None, "No returncode"
|
||||
|
||||
if self._popen.returncode >= 0:
|
||||
self.debug('Return code was %d', self._popen.returncode)
|
||||
logger.debug('Return code was %d', self._popen.returncode)
|
||||
else:
|
||||
self.debug('Terminated with signal %d',
|
||||
logger.debug('Terminated with signal %d',
|
||||
-self._popen.returncode)
|
||||
|
||||
self.setProgress(1.0)
|
||||
@@ -114,7 +115,7 @@ class PopenTask(log.Loggable, task.Task):
|
||||
return
|
||||
|
||||
def abort(self):
|
||||
self.debug('Aborting, sending SIGTERM to %d', self._popen.pid)
|
||||
logger.debug('Aborting, sending SIGTERM to %d', self._popen.pid)
|
||||
os.kill(self._popen.pid, signal.SIGTERM)
|
||||
# self.stop()
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from morituri.common import common
|
||||
|
||||
config_dict = {
|
||||
'revision': common.getRevision(),
|
||||
'version': '0.3.0',
|
||||
'version': '0.4.0',
|
||||
}
|
||||
|
||||
for key, value in config_dict.items():
|
||||
|
||||
1
morituri/extern/command
vendored
1
morituri/extern/command
vendored
@@ -1 +0,0 @@
|
||||
python-command/command
|
||||
1
morituri/extern/flog
vendored
1
morituri/extern/flog
vendored
Submodule morituri/extern/flog deleted from a38ebac80e
1
morituri/extern/log
vendored
1
morituri/extern/log
vendored
@@ -1 +0,0 @@
|
||||
flog/log
|
||||
1
morituri/extern/python-command
vendored
1
morituri/extern/python-command
vendored
Submodule morituri/extern/python-command deleted from bea37f88ec
@@ -29,9 +29,12 @@ See http://digitalx.org/cuesheetsyntax.php
|
||||
import re
|
||||
import codecs
|
||||
|
||||
from morituri.common import common, log
|
||||
from morituri.common import common
|
||||
from morituri.image import table
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_REM_RE = re.compile("^REM\s(\w+)\s(.*)$")
|
||||
_PERFORMER_RE = re.compile("^PERFORMER\s(.*)$")
|
||||
_TITLE_RE = re.compile("^TITLE\s(.*)$")
|
||||
@@ -57,7 +60,7 @@ _INDEX_RE = re.compile(r"""
|
||||
""", re.VERBOSE)
|
||||
|
||||
|
||||
class CueFile(object, log.Loggable):
|
||||
class CueFile(object):
|
||||
"""
|
||||
I represent a .cue file as an object.
|
||||
|
||||
@@ -84,7 +87,7 @@ class CueFile(object, log.Loggable):
|
||||
currentTrack = None
|
||||
counter = 0
|
||||
|
||||
self.info('Parsing .cue file %r', self._path)
|
||||
logger.info('Parsing .cue file %r', self._path)
|
||||
handle = codecs.open(self._path, 'r', 'utf-8')
|
||||
|
||||
for number, line in enumerate(handle.readlines()):
|
||||
@@ -120,7 +123,7 @@ class CueFile(object, log.Loggable):
|
||||
trackNumber = int(m.group('track'))
|
||||
#trackMode = m.group('mode')
|
||||
|
||||
self.debug('found track %d', trackNumber)
|
||||
logger.debug('found track %d', trackNumber)
|
||||
currentTrack = table.Track(trackNumber)
|
||||
self.table.tracks.append(currentTrack)
|
||||
continue
|
||||
@@ -141,7 +144,7 @@ class CueFile(object, log.Loggable):
|
||||
+ seconds * common.FRAMES_PER_SECOND \
|
||||
+ minutes * common.FRAMES_PER_SECOND * 60
|
||||
|
||||
self.debug('found index %d of track %r in %r:%d',
|
||||
logger.debug('found index %d of track %r in %r:%d',
|
||||
indexNumber, currentTrack, currentFile.path, frameOffset)
|
||||
# FIXME: what do we do about File's FORMAT ?
|
||||
currentTrack.index(indexNumber,
|
||||
|
||||
@@ -26,15 +26,16 @@ Wrap on-disk CD images based on the .cue file.
|
||||
|
||||
import os
|
||||
|
||||
from morituri.common import log, common
|
||||
from morituri.common import common
|
||||
from morituri.image import cue, table
|
||||
|
||||
from morituri.extern.task import task
|
||||
|
||||
from morituri.program.soxi import AudioLengthTask
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Image(object, log.Loggable):
|
||||
|
||||
class Image(object):
|
||||
"""
|
||||
@ivar table: The Table of Contents for this image.
|
||||
@type table: L{table.Table}
|
||||
@@ -71,11 +72,11 @@ class Image(object, log.Loggable):
|
||||
Do initial setup, like figuring out track lengths, and
|
||||
constructing the Table of Contents.
|
||||
"""
|
||||
self.debug('setup image start')
|
||||
logger.debug('setup image start')
|
||||
verify = ImageVerifyTask(self)
|
||||
self.debug('verifying image')
|
||||
logger.debug('verifying image')
|
||||
runner.run(verify)
|
||||
self.debug('verified image')
|
||||
logger.debug('verified image')
|
||||
|
||||
# calculate offset and length for each track
|
||||
|
||||
@@ -104,10 +105,10 @@ class Image(object, log.Loggable):
|
||||
|
||||
self.table = table.Table(tracks)
|
||||
self.table.leadout = offset
|
||||
self.debug('setup image done')
|
||||
logger.debug('setup image done')
|
||||
|
||||
|
||||
class AccurateRipChecksumTask(log.Loggable, task.MultiSeparateTask):
|
||||
class AccurateRipChecksumTask(task.MultiSeparateTask):
|
||||
"""
|
||||
I calculate the AccurateRip checksums of all tracks.
|
||||
"""
|
||||
@@ -122,14 +123,14 @@ class AccurateRipChecksumTask(log.Loggable, task.MultiSeparateTask):
|
||||
cue = image.cue
|
||||
self.checksums = []
|
||||
|
||||
self.debug('Checksumming %d tracks' % len(cue.table.tracks))
|
||||
logger.debug('Checksumming %d tracks' % len(cue.table.tracks))
|
||||
for trackIndex, track in enumerate(cue.table.tracks):
|
||||
index = track.indexes[1]
|
||||
length = cue.getTrackLength(track)
|
||||
if length < 0:
|
||||
self.debug('track %d has unknown length' % (trackIndex + 1, ))
|
||||
logger.debug('track %d has unknown length' % (trackIndex + 1, ))
|
||||
else:
|
||||
self.debug('track %d is %d samples long' % (
|
||||
logger.debug('track %d is %d samples long' % (
|
||||
trackIndex + 1, length))
|
||||
|
||||
path = image.getRealPath(index.path)
|
||||
@@ -148,7 +149,7 @@ class AccurateRipChecksumTask(log.Loggable, task.MultiSeparateTask):
|
||||
task.MultiSeparateTask.stop(self)
|
||||
|
||||
|
||||
class ImageVerifyTask(log.Loggable, task.MultiSeparateTask):
|
||||
class ImageVerifyTask(task.MultiSeparateTask):
|
||||
"""
|
||||
I verify a disk image and get the necessary track lengths.
|
||||
"""
|
||||
@@ -171,32 +172,32 @@ class ImageVerifyTask(log.Loggable, task.MultiSeparateTask):
|
||||
track = cue.table.tracks[0]
|
||||
path = image.getRealPath(htoa.path)
|
||||
assert type(path) is unicode, "%r is not unicode" % path
|
||||
self.debug('schedule scan of audio length of %r', path)
|
||||
logger.debug('schedule scan of audio length of %r', path)
|
||||
taskk = AudioLengthTask(path)
|
||||
self.addTask(taskk)
|
||||
self._tasks.append((0, track, taskk))
|
||||
except (KeyError, IndexError):
|
||||
self.debug('no htoa track')
|
||||
logger.debug('no htoa track')
|
||||
|
||||
for trackIndex, track in enumerate(cue.table.tracks):
|
||||
self.debug('verifying track %d', trackIndex + 1)
|
||||
logger.debug('verifying track %d', trackIndex + 1)
|
||||
index = track.indexes[1]
|
||||
length = cue.getTrackLength(track)
|
||||
|
||||
if length == -1:
|
||||
path = image.getRealPath(index.path)
|
||||
assert type(path) is unicode, "%r is not unicode" % path
|
||||
self.debug('schedule scan of audio length of %r', path)
|
||||
logger.debug('schedule scan of audio length of %r', path)
|
||||
taskk = AudioLengthTask(path)
|
||||
self.addTask(taskk)
|
||||
self._tasks.append((trackIndex + 1, track, taskk))
|
||||
else:
|
||||
self.debug('track %d has length %d', trackIndex + 1, length)
|
||||
logger.debug('track %d has length %d', trackIndex + 1, length)
|
||||
|
||||
def stop(self):
|
||||
for trackIndex, track, taskk in self._tasks:
|
||||
if taskk.exception:
|
||||
self.debug('subtask %r had exception %r, shutting down' % (
|
||||
logger.debug('subtask %r had exception %r, shutting down' % (
|
||||
taskk, taskk.exception))
|
||||
self.setException(taskk.exception)
|
||||
break
|
||||
@@ -213,7 +214,7 @@ class ImageVerifyTask(log.Loggable, task.MultiSeparateTask):
|
||||
task.MultiSeparateTask.stop(self)
|
||||
|
||||
|
||||
class ImageEncodeTask(log.Loggable, task.MultiSeparateTask):
|
||||
class ImageEncodeTask(task.MultiSeparateTask):
|
||||
"""
|
||||
I encode a disk image to a different format.
|
||||
"""
|
||||
@@ -235,23 +236,23 @@ class ImageEncodeTask(log.Loggable, task.MultiSeparateTask):
|
||||
|
||||
path = image.getRealPath(index.path)
|
||||
assert type(path) is unicode, "%r is not unicode" % path
|
||||
self.debug('schedule encode of %r', path)
|
||||
logger.debug('schedule encode of %r', path)
|
||||
root, ext = os.path.splitext(os.path.basename(path))
|
||||
outpath = os.path.join(outdir, root + '.' + profile.extension)
|
||||
self.debug('schedule encode to %r', outpath)
|
||||
logger.debug('schedule encode to %r', outpath)
|
||||
taskk = encode.EncodeTask(path, os.path.join(outdir,
|
||||
root + '.' + profile.extension), profile)
|
||||
self.addTask(taskk)
|
||||
|
||||
try:
|
||||
htoa = cue.table.tracks[0].indexes[0]
|
||||
self.debug('encoding htoa track')
|
||||
logger.debug('encoding htoa track')
|
||||
add(htoa)
|
||||
except (KeyError, IndexError):
|
||||
self.debug('no htoa track')
|
||||
logger.debug('no htoa track')
|
||||
pass
|
||||
|
||||
for trackIndex, track in enumerate(cue.table.tracks):
|
||||
self.debug('encoding track %d', trackIndex + 1)
|
||||
logger.debug('encoding track %d', trackIndex + 1)
|
||||
index = track.indexes[1]
|
||||
add(index)
|
||||
|
||||
@@ -28,9 +28,12 @@ import copy
|
||||
import urllib
|
||||
import urlparse
|
||||
|
||||
from morituri.common import common, log
|
||||
from morituri.common import common
|
||||
from morituri.configure import configure
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# FIXME: taken from libcdio, but no reference found for these
|
||||
|
||||
CDTEXT_FIELDS = [
|
||||
@@ -156,7 +159,7 @@ class Index:
|
||||
self.number, self.absolute, self.path, self.relative, self.counter)
|
||||
|
||||
|
||||
class Table(object, log.Loggable):
|
||||
class Table(object):
|
||||
"""
|
||||
I represent a table of indexes on a CD.
|
||||
|
||||
@@ -188,7 +191,7 @@ class Table(object, log.Loggable):
|
||||
|
||||
def unpickled(self):
|
||||
self.logName = "Table 0x%08x v%d" % (id(self), self.instanceVersion)
|
||||
self.debug('set logName')
|
||||
logger.debug('set logName')
|
||||
|
||||
def getTrackStart(self, number):
|
||||
"""
|
||||
@@ -294,7 +297,7 @@ class Table(object, log.Loggable):
|
||||
# print 'THOMAS: disc leadout', self.leadout
|
||||
last = self.tracks[-1]
|
||||
leadout = self.getTrackEnd(last.number) + 1
|
||||
self.debug('leadout LBA: %d', leadout)
|
||||
logger.debug('leadout LBA: %d', leadout)
|
||||
|
||||
# FIXME: we can't replace these calculations with the getFrameLength
|
||||
# call because the start and leadout in the algorithm get rounded
|
||||
@@ -313,9 +316,9 @@ class Table(object, log.Loggable):
|
||||
result.insert(0, value)
|
||||
|
||||
# compare this debug line to cd-discid output
|
||||
self.debug('cddb values: %r', result)
|
||||
logger.debug('cddb values: %r', result)
|
||||
|
||||
self.debug('cddb disc id debug: %s',
|
||||
logger.debug('cddb disc id debug: %s',
|
||||
" ".join(["%08x" % value, ] + debug))
|
||||
|
||||
return result
|
||||
@@ -338,8 +341,8 @@ class Table(object, log.Loggable):
|
||||
@returns: the 28-character base64-encoded disc ID
|
||||
"""
|
||||
if self.mbdiscid:
|
||||
self.log('getMusicBrainzDiscId: returning cached %r'
|
||||
% self.mbdiscid)
|
||||
logger.debug('getMusicBrainzDiscId: returning cached %r'
|
||||
% self.mbdiscid)
|
||||
return self.mbdiscid
|
||||
values = self._getMusicBrainzValues()
|
||||
|
||||
@@ -387,7 +390,7 @@ class Table(object, log.Loggable):
|
||||
assert len(result) == 28, \
|
||||
"Result should be 28 characters, not %d" % len(result)
|
||||
|
||||
self.log('getMusicBrainzDiscId: returning %r' % result)
|
||||
logger.debug('getMusicBrainzDiscId: returning %r' % result)
|
||||
self.mbdiscid = result
|
||||
return result
|
||||
|
||||
@@ -419,7 +422,7 @@ class Table(object, log.Loggable):
|
||||
last = self.tracks[self.getAudioTracks() - 1]
|
||||
|
||||
leadout = self.getTrackEnd(last.number) + 1
|
||||
self.debug('leadout LBA: %d', leadout)
|
||||
logger.debug('leadout LBA: %d', leadout)
|
||||
durationFrames = leadout - self.getTrackStart(1)
|
||||
|
||||
return durationFrames
|
||||
@@ -475,7 +478,7 @@ class Table(object, log.Loggable):
|
||||
pass
|
||||
|
||||
|
||||
self.log('Musicbrainz values: %r', result)
|
||||
logger.debug('Musicbrainz values: %r', result)
|
||||
return result
|
||||
|
||||
def getAccurateRipIds(self):
|
||||
@@ -533,7 +536,7 @@ class Table(object, log.Loggable):
|
||||
|
||||
@rtype: C{unicode}
|
||||
"""
|
||||
self.debug('generating .cue for cuePath %r', cuePath)
|
||||
logger.debug('generating .cue for cuePath %r', cuePath)
|
||||
|
||||
lines = []
|
||||
|
||||
@@ -541,7 +544,7 @@ class Table(object, log.Loggable):
|
||||
targetPath = common.getRelativePath(path, cuePath)
|
||||
line = 'FILE "%s" WAVE' % targetPath
|
||||
lines.append(line)
|
||||
self.debug('writeFile: %r' % line)
|
||||
logger.debug('writeFile: %r' % line)
|
||||
|
||||
# header
|
||||
main = ['PERFORMER', 'TITLE']
|
||||
@@ -582,11 +585,11 @@ class Table(object, log.Loggable):
|
||||
counter = index.counter
|
||||
|
||||
if index.path:
|
||||
self.debug('counter %d, writeFile' % counter)
|
||||
logger.debug('counter %d, writeFile' % counter)
|
||||
writeFile(index.path)
|
||||
|
||||
for i, track in enumerate(self.tracks):
|
||||
self.debug('track i %r, track %r' % (i, track))
|
||||
logger.debug('track i %r, track %r' % (i, track))
|
||||
# FIXME: skip data tracks for now
|
||||
if not track.audio:
|
||||
continue
|
||||
@@ -598,7 +601,7 @@ class Table(object, log.Loggable):
|
||||
|
||||
for number in indexes:
|
||||
index = track.indexes[number]
|
||||
self.debug('index %r, %r' % (number, index))
|
||||
logger.debug('index %r, %r' % (number, index))
|
||||
|
||||
# any time the source counter changes to a higher value,
|
||||
# write a FILE statement
|
||||
@@ -606,9 +609,9 @@ class Table(object, log.Loggable):
|
||||
# at counter 0 here
|
||||
if index.counter > counter:
|
||||
if index.path:
|
||||
self.debug('counter %d, writeFile' % counter)
|
||||
logger.debug('counter %d, writeFile' % counter)
|
||||
writeFile(index.path)
|
||||
self.debug('setting counter to index.counter %r' %
|
||||
logger.debug('setting counter to index.counter %r' %
|
||||
index.counter)
|
||||
counter = index.counter
|
||||
|
||||
@@ -617,7 +620,7 @@ class Table(object, log.Loggable):
|
||||
wroteTrack = True
|
||||
line = " TRACK %02d %s" % (i + 1, 'AUDIO')
|
||||
lines.append(line)
|
||||
self.debug('%r' % line)
|
||||
logger.debug('%r' % line)
|
||||
|
||||
for key in CDTEXT_FIELDS:
|
||||
if key in track.cdtext:
|
||||
@@ -667,11 +670,11 @@ class Table(object, log.Loggable):
|
||||
index = self.tracks[0].getFirstIndex()
|
||||
i = index.number
|
||||
|
||||
self.debug('clearing path')
|
||||
logger.debug('clearing path')
|
||||
while True:
|
||||
track = self.tracks[t - 1]
|
||||
index = track.getIndex(i)
|
||||
self.debug('Clearing path on track %d, index %d', t, i)
|
||||
logger.debug('Clearing path on track %d, index %d', t, i)
|
||||
index.path = None
|
||||
index.relative = None
|
||||
try:
|
||||
@@ -690,7 +693,7 @@ class Table(object, log.Loggable):
|
||||
@type track: C{int}
|
||||
@type index: C{int}
|
||||
"""
|
||||
self.debug('setFile: track %d, index %d, path %r, '
|
||||
logger.debug('setFile: track %d, index %d, path %r, '
|
||||
'length %r, counter %r', track, index, path, length, counter)
|
||||
|
||||
t = self.tracks[track - 1]
|
||||
@@ -704,7 +707,7 @@ class Table(object, log.Loggable):
|
||||
i.path = path
|
||||
i.relative = i.absolute - start
|
||||
i.counter = counter
|
||||
self.debug('Setting path %r, relative %r on '
|
||||
logger.debug('Setting path %r, relative %r on '
|
||||
'track %d, index %d, counter %r',
|
||||
path, i.relative, track, index, counter)
|
||||
try:
|
||||
@@ -726,19 +729,19 @@ class Table(object, log.Loggable):
|
||||
counter = index.counter
|
||||
|
||||
#for t in self.tracks: print t, t.indexes
|
||||
self.debug('absolutizing')
|
||||
logger.debug('absolutizing')
|
||||
while True:
|
||||
track = self.tracks[t - 1]
|
||||
index = track.getIndex(i)
|
||||
assert track.number == t
|
||||
assert index.number == i
|
||||
if index.counter is None:
|
||||
self.debug('Track %d, index %d has no counter', t, i)
|
||||
logger.debug('Track %d, index %d has no counter', t, i)
|
||||
break
|
||||
if index.counter != counter:
|
||||
self.debug('Track %d, index %d has a different counter', t, i)
|
||||
logger.debug('Track %d, index %d has a different counter', t, i)
|
||||
break
|
||||
self.debug('Setting absolute offset %d on track %d, index %d',
|
||||
logger.debug('Setting absolute offset %d on track %d, index %d',
|
||||
index.relative, t, i)
|
||||
if index.absolute is not None:
|
||||
if index.absolute != index.relative:
|
||||
@@ -772,16 +775,16 @@ class Table(object, log.Loggable):
|
||||
for i in t.indexes.values():
|
||||
if i.absolute is not None:
|
||||
i.absolute += self.leadout + gap
|
||||
self.debug('Fixing track %02d, index %02d, absolute %d' % (
|
||||
logger.debug('Fixing track %02d, index %02d, absolute %d' % (
|
||||
t.number, i.number, i.absolute))
|
||||
if i.counter is not None:
|
||||
i.counter += sourceCounter
|
||||
self.debug('Fixing track %02d, index %02d, counter %d' % (
|
||||
logger.debug('Fixing track %02d, index %02d, counter %d' % (
|
||||
t.number, i.number, i.counter))
|
||||
self.tracks.append(t)
|
||||
|
||||
self.leadout += other.leadout + gap # FIXME
|
||||
self.debug('Fixing leadout, now %d', self.leadout)
|
||||
logger.debug('Fixing leadout, now %d', self.leadout)
|
||||
|
||||
def _getSessionGap(self, session):
|
||||
# From cdrecord multi-session info:
|
||||
@@ -836,15 +839,15 @@ class Table(object, log.Loggable):
|
||||
offsets, as well as the leadout.
|
||||
"""
|
||||
if not self.leadout:
|
||||
self.debug('no leadout, no TOC')
|
||||
logger.debug('no leadout, no TOC')
|
||||
return False
|
||||
|
||||
for t in self.tracks:
|
||||
if 1 not in t.indexes.keys():
|
||||
self.debug('no index 1, no TOC')
|
||||
logger.debug('no index 1, no TOC')
|
||||
return False
|
||||
if t.indexes[1].absolute is None:
|
||||
self.debug('no absolute index 1, no TOC')
|
||||
logger.debug('no absolute index 1, no TOC')
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -854,13 +857,13 @@ class Table(object, log.Loggable):
|
||||
Check if this table can be used to generate a .cue file
|
||||
"""
|
||||
if not self.hasTOC():
|
||||
self.debug('No TOC, cannot cue')
|
||||
logger.debug('No TOC, cannot cue')
|
||||
return False
|
||||
|
||||
for t in self.tracks:
|
||||
for i in t.indexes.values():
|
||||
if i.relative is None:
|
||||
self.debug('Track %02d, Index %02d does not have relative',
|
||||
logger.debug('Track %02d, Index %02d does not have relative',
|
||||
t.number, i.number)
|
||||
return False
|
||||
|
||||
|
||||
@@ -29,9 +29,12 @@ The .toc file format is described in the man page of cdrdao
|
||||
import re
|
||||
import codecs
|
||||
|
||||
from morituri.common import common, log
|
||||
from morituri.common import common
|
||||
from morituri.image import table
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# shared
|
||||
_CDTEXT_CANDIDATE_RE = re.compile(r'(?P<key>\w+) "(?P<value>.+)"')
|
||||
|
||||
@@ -91,7 +94,7 @@ _INDEX_RE = re.compile(r"""
|
||||
""", re.VERBOSE)
|
||||
|
||||
|
||||
class Sources(log.Loggable):
|
||||
class Sources:
|
||||
"""
|
||||
I represent the list of sources used in the .toc file.
|
||||
Each SILENCE and each FILE is a source.
|
||||
@@ -108,7 +111,7 @@ class Sources(log.Loggable):
|
||||
@type counter: int
|
||||
@param offset: the absolute disc offset where this source starts
|
||||
"""
|
||||
self.debug('Appending source, counter %d, abs offset %d, source %r' % (
|
||||
logger.debug('Appending source, counter %d, abs offset %d, source %r' % (
|
||||
counter, offset, source))
|
||||
self._sources.append((counter, offset, source))
|
||||
|
||||
@@ -133,7 +136,7 @@ class Sources(log.Loggable):
|
||||
return self._sources[-1][1]
|
||||
|
||||
|
||||
class TocFile(object, log.Loggable):
|
||||
class TocFile(object):
|
||||
|
||||
def __init__(self, path):
|
||||
"""
|
||||
@@ -151,7 +154,7 @@ class TocFile(object, log.Loggable):
|
||||
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' % (
|
||||
logger.debug('at abs offset %d, we are in source %r' % (
|
||||
absolute, s))
|
||||
counterStart = self._sources.getCounterStart(c)
|
||||
relative = absolute - counterStart
|
||||
@@ -160,7 +163,7 @@ class TocFile(object, log.Loggable):
|
||||
absolute=absolute,
|
||||
relative=relative,
|
||||
counter=c)
|
||||
self.debug(
|
||||
logger.debug(
|
||||
'[track %02d index %02d] trackOffset %r, added %r',
|
||||
currentTrack.number, i, trackOffset,
|
||||
currentTrack.getIndex(i))
|
||||
@@ -207,11 +210,11 @@ class TocFile(object, log.Loggable):
|
||||
# is a limitation of our parser approach
|
||||
if state == 'HEADER':
|
||||
self.table.cdtext[key] = value
|
||||
self.debug('Found disc CD-Text %s: %r', key, value)
|
||||
logger.debug('Found disc CD-Text %s: %r', key, value)
|
||||
elif state == 'TRACK':
|
||||
if key != 'ISRC' or not currentTrack \
|
||||
or currentTrack.isrc is not None:
|
||||
self.debug('Found track CD-Text %s: %r',
|
||||
logger.debug('Found track CD-Text %s: %r',
|
||||
key, value)
|
||||
currentTrack.cdtext[key] = value
|
||||
|
||||
@@ -219,7 +222,7 @@ class TocFile(object, log.Loggable):
|
||||
m = _CATALOG_RE.search(line)
|
||||
if m:
|
||||
self.table.catalog = m.group('catalog')
|
||||
self.debug("Found catalog number %s", self.table.catalog)
|
||||
logger.debug("Found catalog number %s", self.table.catalog)
|
||||
|
||||
# look for TRACK lines
|
||||
m = _TRACK_RE.search(line)
|
||||
@@ -244,7 +247,7 @@ class TocFile(object, log.Loggable):
|
||||
totalLength += currentLength
|
||||
|
||||
# FIXME: track mode
|
||||
self.debug('found track %d, mode %s, at absoluteOffset %d',
|
||||
logger.debug('found track %d, mode %s, at absoluteOffset %d',
|
||||
trackNumber, trackMode, absoluteOffset)
|
||||
|
||||
# reset counters relative to a track
|
||||
@@ -258,23 +261,23 @@ class TocFile(object, log.Loggable):
|
||||
m = _PRE_EMPHASIS_RE.search(line)
|
||||
if m:
|
||||
currentTrack.pre_emphasis = True
|
||||
self.debug('Track has PRE_EMPHASIS')
|
||||
logger.debug('Track has PRE_EMPHASIS')
|
||||
|
||||
# look for ISRC lines
|
||||
m = _ISRC_RE.search(line)
|
||||
if m:
|
||||
isrc = m.group('isrc')
|
||||
currentTrack.isrc = isrc
|
||||
self.debug('Found ISRC code %s', isrc)
|
||||
logger.debug('Found ISRC code %s', isrc)
|
||||
|
||||
# look for SILENCE lines
|
||||
m = _SILENCE_RE.search(line)
|
||||
if m:
|
||||
length = m.group('length')
|
||||
self.debug('SILENCE of %r', length)
|
||||
logger.debug('SILENCE of %r', length)
|
||||
self._sources.append(counter, absoluteOffset, None)
|
||||
if currentFile is not None:
|
||||
self.debug('SILENCE after FILE, increasing counter')
|
||||
logger.debug('SILENCE after FILE, increasing counter')
|
||||
counter += 1
|
||||
relativeOffset = 0
|
||||
currentFile = None
|
||||
@@ -284,7 +287,7 @@ class TocFile(object, log.Loggable):
|
||||
m = _ZERO_RE.search(line)
|
||||
if m:
|
||||
if currentFile is not None:
|
||||
self.debug('ZERO after FILE, increasing counter')
|
||||
logger.debug('ZERO after FILE, increasing counter')
|
||||
counter += 1
|
||||
relativeOffset = 0
|
||||
currentFile = None
|
||||
@@ -297,13 +300,13 @@ class TocFile(object, log.Loggable):
|
||||
filePath = m.group('name')
|
||||
start = m.group('start')
|
||||
length = m.group('length')
|
||||
self.debug('FILE %s, start %r, length %r',
|
||||
logger.debug('FILE %s, start %r, length %r',
|
||||
filePath, common.msfToFrames(start),
|
||||
common.msfToFrames(length))
|
||||
if not currentFile or filePath != currentFile.path:
|
||||
counter += 1
|
||||
relativeOffset = 0
|
||||
self.debug('track %d, switched to new FILE, '
|
||||
logger.debug('track %d, switched to new FILE, '
|
||||
'increased counter to %d',
|
||||
trackNumber, counter)
|
||||
currentFile = File(filePath, common.msfToFrames(start),
|
||||
@@ -319,12 +322,12 @@ class TocFile(object, log.Loggable):
|
||||
filePath = m.group('name')
|
||||
length = m.group('length')
|
||||
# print 'THOMAS', length
|
||||
self.debug('FILE %s, length %r',
|
||||
logger.debug('FILE %s, length %r',
|
||||
filePath, common.msfToFrames(length))
|
||||
if not currentFile or filePath != currentFile.path:
|
||||
counter += 1
|
||||
relativeOffset = 0
|
||||
self.debug('track %d, switched to new FILE, '
|
||||
logger.debug('track %d, switched to new FILE, '
|
||||
'increased counter to %d',
|
||||
trackNumber, counter)
|
||||
# FIXME: assume that a MODE2_FORM_MIX track always starts at 0
|
||||
@@ -345,7 +348,7 @@ class TocFile(object, log.Loggable):
|
||||
|
||||
length = common.msfToFrames(m.group('length'))
|
||||
c, o, s = self._sources.get(absoluteOffset)
|
||||
self.debug('at abs offset %d, we are in source %r' % (
|
||||
logger.debug('at abs offset %d, we are in source %r' % (
|
||||
absoluteOffset, s))
|
||||
counterStart = self._sources.getCounterStart(c)
|
||||
relativeOffset = absoluteOffset - counterStart
|
||||
@@ -353,7 +356,7 @@ class TocFile(object, log.Loggable):
|
||||
currentTrack.index(0, path=s and s.path or None,
|
||||
absolute=absoluteOffset,
|
||||
relative=relativeOffset, counter=c)
|
||||
self.debug('[track %02d index 00] added %r',
|
||||
logger.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
|
||||
@@ -377,7 +380,7 @@ class TocFile(object, log.Loggable):
|
||||
|
||||
# totalLength was added up to the penultimate track
|
||||
self.table.leadout = totalLength + currentLength
|
||||
self.debug('parse: leadout: %r', self.table.leadout)
|
||||
logger.debug('parse: leadout: %r', self.table.leadout)
|
||||
|
||||
def message(self, number, message):
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import logging
|
||||
from subprocess import Popen, PIPE
|
||||
from os.path import exists
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ARB = 'accuraterip-checksum'
|
||||
FLAC = 'flac'
|
||||
@@ -33,18 +35,18 @@ def accuraterip_checksum(f, track, tracks, wave=False, v2=False):
|
||||
arc_rc = arc.returncode
|
||||
|
||||
if not wave and flac_rc != 0:
|
||||
logging.warning('ARC calculation failed: flac return code is non zero')
|
||||
logger.warning('ARC calculation failed: flac return code is non zero')
|
||||
return None
|
||||
|
||||
if arc_rc != 0:
|
||||
logging.warning('ARC calculation failed: arc return code is non zero')
|
||||
logger.warning('ARC calculation failed: arc return code is non zero')
|
||||
return None
|
||||
|
||||
out = out.strip()
|
||||
try:
|
||||
outh = int('0x%s' % out, base=16)
|
||||
except ValueError:
|
||||
logging.warning('ARC output is not usable')
|
||||
logger.warning('ARC output is not usable')
|
||||
return None
|
||||
|
||||
return outh
|
||||
|
||||
@@ -20,21 +20,23 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with morituri. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import errno
|
||||
import time
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from morituri.common import log, common
|
||||
from morituri.common import common
|
||||
from morituri.common import task as ctask
|
||||
|
||||
from morituri.extern import asyncsub
|
||||
from morituri.extern.task import task
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FileSizeError(Exception):
|
||||
|
||||
@@ -78,7 +80,7 @@ _ERROR_RE = re.compile("^scsi_read error:")
|
||||
# number of single-channel samples, ie. 2 bytes (word) per unit, and absolute
|
||||
|
||||
|
||||
class ProgressParser(log.Loggable):
|
||||
class ProgressParser:
|
||||
read = 0 # last [read] frame
|
||||
wrote = 0 # last [wrote] frame
|
||||
errors = 0 # count of number of scsi errors
|
||||
@@ -130,12 +132,12 @@ class ProgressParser(log.Loggable):
|
||||
# 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)
|
||||
logger.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)
|
||||
logger.debug('set firstFrames to %r', self._firstFrames)
|
||||
|
||||
markStart = None
|
||||
markEnd = None # the next unread frame (half-inclusive)
|
||||
@@ -192,7 +194,7 @@ class ProgressParser(log.Loggable):
|
||||
"""
|
||||
frames = self.stop - self.start + 1 # + 1 since stop is inclusive
|
||||
reads = self.reads
|
||||
self.debug('getTrackQuality: frames %d, reads %d' % (frames, reads))
|
||||
logger.debug('getTrackQuality: frames %d, reads %d' % (frames, reads))
|
||||
|
||||
# don't go over a 100%; we know cdparanoia reads each frame at least
|
||||
# twice
|
||||
@@ -202,7 +204,7 @@ class ProgressParser(log.Loggable):
|
||||
# FIXME: handle errors
|
||||
|
||||
|
||||
class ReadTrackTask(log.Loggable, task.Task):
|
||||
class ReadTrackTask(task.Task):
|
||||
"""
|
||||
I am a task that reads a track using cdparanoia.
|
||||
|
||||
@@ -271,11 +273,11 @@ class ReadTrackTask(log.Loggable, task.Task):
|
||||
stopTrack = i + 1
|
||||
stopOffset = self._stop - self._table.getTrackStart(i + 1)
|
||||
|
||||
self.debug('Ripping from %d to %d (inclusive)',
|
||||
logger.debug('Ripping from %d to %d (inclusive)',
|
||||
self._start, self._stop)
|
||||
self.debug('Starting at track %d, offset %d',
|
||||
logger.debug('Starting at track %d, offset %d',
|
||||
startTrack, startOffset)
|
||||
self.debug('Stopping at track %d, offset %d',
|
||||
logger.debug('Stopping at track %d, offset %d',
|
||||
stopTrack, stopOffset)
|
||||
|
||||
bufsize = 1024
|
||||
@@ -291,7 +293,7 @@ class ReadTrackTask(log.Loggable, task.Task):
|
||||
startTrack, common.framesToHMSF(startOffset),
|
||||
stopTrack, common.framesToHMSF(stopOffset)),
|
||||
self.path])
|
||||
self.debug('Running %s' % (" ".join(argv), ))
|
||||
logger.debug('Running %s' % (" ".join(argv), ))
|
||||
try:
|
||||
self._popen = asyncsub.Popen(argv,
|
||||
bufsize=bufsize,
|
||||
@@ -333,7 +335,7 @@ class ReadTrackTask(log.Loggable, task.Task):
|
||||
|
||||
# fail if too many errors
|
||||
if self._parser.errors > self._MAXERROR:
|
||||
self.debug('%d errors, terminating', self._parser.errors)
|
||||
logger.debug('%d errors, terminating', self._parser.errors)
|
||||
self._popen.terminate()
|
||||
|
||||
num = self._parser.wrote - self._start + 1
|
||||
@@ -366,13 +368,13 @@ class ReadTrackTask(log.Loggable, task.Task):
|
||||
expected = offsetLength * common.BYTES_PER_FRAME + 44
|
||||
if size != expected:
|
||||
# FIXME: handle errors better
|
||||
self.warning('file size %d did not match expected size %d',
|
||||
logger.warning('file size %d did not match expected size %d',
|
||||
size, expected)
|
||||
if (size - expected) % common.BYTES_PER_FRAME == 0:
|
||||
self.warning('%d frames difference' % (
|
||||
logger.warning('%d frames difference' % (
|
||||
(size - expected) / common.BYTES_PER_FRAME))
|
||||
else:
|
||||
self.warning('non-integral amount of frames difference')
|
||||
logger.warning('non-integral amount of frames difference')
|
||||
|
||||
self.setAndRaiseException(FileSizeError(self.path,
|
||||
"File size %d did not match expected size %d" % (
|
||||
@@ -382,7 +384,7 @@ class ReadTrackTask(log.Loggable, task.Task):
|
||||
if self._errors:
|
||||
print "\n".join(self._errors)
|
||||
else:
|
||||
self.warning('exit code %r', self._popen.returncode)
|
||||
logger.warning('exit code %r', self._popen.returncode)
|
||||
self.exception = ReturnCodeError(self._popen.returncode)
|
||||
|
||||
self.quality = self._parser.getTrackQuality()
|
||||
@@ -393,7 +395,7 @@ class ReadTrackTask(log.Loggable, task.Task):
|
||||
return
|
||||
|
||||
|
||||
class ReadVerifyTrackTask(log.Loggable, task.MultiSeparateTask):
|
||||
class ReadVerifyTrackTask(task.MultiSeparateTask):
|
||||
"""
|
||||
I am a task that reads and verifies a track using cdparanoia.
|
||||
I also encode the track.
|
||||
@@ -449,10 +451,10 @@ class ReadVerifyTrackTask(log.Loggable, task.MultiSeparateTask):
|
||||
"""
|
||||
task.MultiSeparateTask.__init__(self)
|
||||
|
||||
self.debug('Creating read and verify task on %r', path)
|
||||
logger.debug('Creating read and verify task on %r', path)
|
||||
|
||||
if taglist:
|
||||
self.debug('read and verify with taglist %r', taglist)
|
||||
logger.debug('read and verify with taglist %r', taglist)
|
||||
# FIXME: choose a dir on the same disk/dir as the final path
|
||||
fd, tmppath = tempfile.mkstemp(suffix='.morituri.wav')
|
||||
tmppath = unicode(tmppath)
|
||||
@@ -504,7 +506,7 @@ class ReadVerifyTrackTask(log.Loggable, task.MultiSeparateTask):
|
||||
self.quality = max(self.tasks[0].quality,
|
||||
self.tasks[2].quality)
|
||||
self.peak = self.tasks[6].peak
|
||||
self.debug('peak: %r', self.peak)
|
||||
logger.debug('peak: %r', self.peak)
|
||||
self.testspeed = self.tasks[0].speed
|
||||
self.copyspeed = self.tasks[2].speed
|
||||
self.testduration = self.tasks[0].duration
|
||||
@@ -513,11 +515,11 @@ class ReadVerifyTrackTask(log.Loggable, task.MultiSeparateTask):
|
||||
self.testchecksum = c1 = self.tasks[1].checksum
|
||||
self.copychecksum = c2 = self.tasks[3].checksum
|
||||
if c1 == c2:
|
||||
self.info('Checksums match, %08x' % c1)
|
||||
logger.info('Checksums match, %08x' % c1)
|
||||
self.checksum = self.testchecksum
|
||||
else:
|
||||
# FIXME: detect this before encoding
|
||||
self.info('Checksums do not match, %08x %08x' % (
|
||||
logger.info('Checksums do not match, %08x %08x' % (
|
||||
c1, c2))
|
||||
self.exception = ChecksumException(
|
||||
'read and verify failed: test checksum')
|
||||
@@ -531,17 +533,17 @@ class ReadVerifyTrackTask(log.Loggable, task.MultiSeparateTask):
|
||||
|
||||
if not self.exception:
|
||||
try:
|
||||
self.debug('Moving to final path %r', self.path)
|
||||
logger.debug('Moving to final path %r', self.path)
|
||||
os.rename(self._tmppath, self.path)
|
||||
except Exception, e:
|
||||
self.debug('Exception while moving to final path %r: '
|
||||
logger.debug('Exception while moving to final path %r: '
|
||||
'%r',
|
||||
self.path, log.getExceptionMessage(e))
|
||||
self.path, str(e))
|
||||
self.exception = e
|
||||
else:
|
||||
os.unlink(self._tmppath)
|
||||
else:
|
||||
self.debug('stop: exception %r', self.exception)
|
||||
logger.debug('stop: exception %r', self.exception)
|
||||
except Exception, e:
|
||||
print 'WARNING: unhandled exception %r' % (e, )
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
@@ -6,6 +5,9 @@ from subprocess import check_call, Popen, PIPE, CalledProcessError
|
||||
|
||||
from morituri.image.toc import TocFile
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CDRDAO = 'cdrdao'
|
||||
|
||||
def read_toc(device, fast_toc=False):
|
||||
@@ -28,7 +30,7 @@ def read_toc(device, fast_toc=False):
|
||||
try:
|
||||
check_call(cmd, stdout=PIPE, stderr=PIPE)
|
||||
except CalledProcessError, e:
|
||||
logging.warning('cdrdao read-toc failed: return code is non-zero: ' +
|
||||
logger.warning('cdrdao read-toc failed: return code is non-zero: ' +
|
||||
str(e.returncode))
|
||||
raise e
|
||||
toc = TocFile(tocfile)
|
||||
@@ -43,14 +45,14 @@ def version():
|
||||
cdrdao = Popen(CDRDAO, stderr=PIPE)
|
||||
out, err = cdrdao.communicate()
|
||||
if cdrdao.returncode != 1:
|
||||
logging.warning("cdrdao version detection failed: "
|
||||
"return code is " + str(cdrdao.returncode))
|
||||
logger.warning("cdrdao version detection failed: "
|
||||
"return code is " + str(cdrdao.returncode))
|
||||
return None
|
||||
m = re.compile(r'^Cdrdao version (?P<version>.*) - \(C\)').search(
|
||||
err.decode('utf-8'))
|
||||
if not m:
|
||||
logging.warning("cdrdao version detection failed: "
|
||||
+ "could not find version")
|
||||
logger.warning("cdrdao version detection failed: "
|
||||
"could not find version")
|
||||
return None
|
||||
return m.group('version')
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import logging
|
||||
import os
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SOX = 'sox'
|
||||
|
||||
def peak_level(track_path):
|
||||
@@ -12,12 +14,12 @@ def peak_level(track_path):
|
||||
Returns None on error.
|
||||
"""
|
||||
if not os.path.exists(track_path):
|
||||
logging.warning("SoX peak detection failed: file not found")
|
||||
logger.warning("SoX peak detection failed: file not found")
|
||||
return None
|
||||
sox = Popen([SOX, track_path, "-n", "stat"], stderr=PIPE)
|
||||
out, err = sox.communicate()
|
||||
if sox.returncode:
|
||||
logging.warning("SoX peak detection failed: " + str(sox.returncode))
|
||||
logger.warning("SoX peak detection failed: " + str(sox.returncode))
|
||||
return None
|
||||
# relevant captured line looks like:
|
||||
# Maximum amplitude: 0.123456
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import os
|
||||
|
||||
from morituri.common import log, common
|
||||
from morituri.common import common
|
||||
from morituri.common import task as ctask
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SOXI = 'soxi'
|
||||
|
||||
class AudioLengthTask(ctask.PopenTask, log.Loggable):
|
||||
class AudioLengthTask(ctask.PopenTask):
|
||||
"""
|
||||
I calculate the length of a track in audio samples.
|
||||
|
||||
@@ -42,5 +45,5 @@ class AudioLengthTask(ctask.PopenTask, log.Loggable):
|
||||
|
||||
def done(self):
|
||||
if self._error:
|
||||
self.warning("soxi reported on stderr: %s", "".join(self._error))
|
||||
logger.warning("soxi reported on stderr: %s", "".join(self._error))
|
||||
self.length = int("".join(self._output))
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
# -*- Mode: Python -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
# options and arguments shared between commands
|
||||
|
||||
DEFAULT_TRACK_TEMPLATE = u'%r/%A - %d/%t. %a - %n'
|
||||
DEFAULT_DISC_TEMPLATE = u'%r/%A - %d/%A - %d'
|
||||
|
||||
TEMPLATE_DESCRIPTION = '''
|
||||
Tracks are named according to the track template, filling in the variables
|
||||
and adding the file extension. Variables exclusive to the track template are:
|
||||
- %t: track number
|
||||
- %a: track artist
|
||||
- %n: track title
|
||||
- %s: track sort name
|
||||
|
||||
Disc files (.cue, .log, .m3u) are named according to the disc template,
|
||||
filling in the variables and adding the file extension. Variables for both
|
||||
disc and track template are:
|
||||
- %A: album artist
|
||||
- %S: album sort name
|
||||
- %d: disc title
|
||||
- %y: release year
|
||||
- %r: release type, lowercase
|
||||
- %R: Release type, normal case
|
||||
- %x: audio extension, lowercase
|
||||
- %X: audio extension, uppercase
|
||||
|
||||
'''
|
||||
|
||||
def addTemplate(obj):
|
||||
# FIXME: get from config
|
||||
obj.parser.add_option('', '--track-template',
|
||||
action="store", dest="track_template",
|
||||
help="template for track file naming (default %default)",
|
||||
default=DEFAULT_TRACK_TEMPLATE)
|
||||
obj.parser.add_option('', '--disc-template',
|
||||
action="store", dest="disc_template",
|
||||
help="template for disc file naming (default %default)",
|
||||
default=DEFAULT_DISC_TEMPLATE)
|
||||
@@ -1,97 +0,0 @@
|
||||
# -*- Mode: Python -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
import sys
|
||||
import pkg_resources
|
||||
import musicbrainzngs
|
||||
|
||||
from morituri.common import log, logcommand, common, config, directory
|
||||
from morituri.configure import configure
|
||||
from morituri.extern.command import command
|
||||
from morituri.extern.task import task
|
||||
from morituri.rip import cd, offset, drive, image, accurip, debug
|
||||
|
||||
|
||||
def main():
|
||||
# set user agent
|
||||
musicbrainzngs.set_useragent("morituri", configure.version,
|
||||
'https://thomas.apestaart.org/morituri/trac')
|
||||
# register plugins with pkg_resources
|
||||
distributions, _ = pkg_resources.working_set.find_plugins(
|
||||
pkg_resources.Environment([directory.data_path('plugins')])
|
||||
)
|
||||
map(pkg_resources.working_set.add, distributions)
|
||||
c = Rip()
|
||||
try:
|
||||
ret = c.parse(sys.argv[1:])
|
||||
except SystemError, e:
|
||||
sys.stderr.write('rip: error: %s\n' % e.args)
|
||||
return 255
|
||||
except ImportError, e:
|
||||
raise ImportError(e)
|
||||
except task.TaskException, e:
|
||||
if isinstance(e.exception, ImportError):
|
||||
raise ImportError(e.exception)
|
||||
elif isinstance(e.exception, common.MissingDependencyException):
|
||||
sys.stderr.write('rip: error: missing dependency "%s"\n' %
|
||||
e.exception.dependency)
|
||||
return 255
|
||||
|
||||
if isinstance(e.exception, common.EmptyError):
|
||||
log.debug('main',
|
||||
"EmptyError: %r", log.getExceptionMessage(e.exception))
|
||||
sys.stderr.write(
|
||||
'rip: error: Could not create encoded file.\n')
|
||||
return 255
|
||||
|
||||
# in python3 we can instead do `raise e.exception` as that would show
|
||||
# the exception's original context
|
||||
sys.stderr.write(e.exceptionMessage)
|
||||
return 255
|
||||
except command.CommandError, e:
|
||||
sys.stderr.write('rip: error: %s\n' % e.output)
|
||||
return e.status
|
||||
|
||||
if ret is None:
|
||||
return 0
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
class Rip(logcommand.LogCommand):
|
||||
usage = "%prog %command"
|
||||
description = """Rip rips CD's.
|
||||
|
||||
Rip gives you a tree of subcommands to work with.
|
||||
You can get help on subcommands by using the -h option to the subcommand.
|
||||
"""
|
||||
|
||||
subCommandClasses = [accurip.AccuRip,
|
||||
cd.CD, debug.Debug, drive.Drive, offset.Offset, image.Image, ]
|
||||
|
||||
def addOptions(self):
|
||||
# FIXME: is this the right place ?
|
||||
log.init()
|
||||
log.debug("morituri", "This is morituri version %s (%s)",
|
||||
configure.version, configure.revision)
|
||||
|
||||
self.parser.add_option('-R', '--record',
|
||||
action="store_true", dest="record",
|
||||
help="record API requests for playback")
|
||||
self.parser.add_option('-v', '--version',
|
||||
action="store_true", dest="version",
|
||||
help="show version information")
|
||||
|
||||
def handleOptions(self, options):
|
||||
if options.version:
|
||||
print "rip %s" % configure.version
|
||||
sys.exit(0)
|
||||
|
||||
self.record = options.record
|
||||
|
||||
self.config = config.Config()
|
||||
|
||||
def parse(self, argv):
|
||||
log.debug("morituri", "rip %s" % " ".join(argv))
|
||||
logcommand.LogCommand.parse(self, argv)
|
||||
@@ -1,5 +1,5 @@
|
||||
REM DISCID AD0BE00D
|
||||
REM COMMENT "morituri 0.3.0"
|
||||
REM COMMENT "morituri 0.4.0"
|
||||
FILE "data.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
PREGAP 03:22:70
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
REM DISCID BE08990D
|
||||
REM COMMENT "morituri 0.3.0"
|
||||
REM COMMENT "morituri 0.4.0"
|
||||
CATALOG 0652637280326
|
||||
PERFORMER "THE BREEDERS"
|
||||
TITLE "MOUNTAIN BATTLES"
|
||||
|
||||
@@ -8,11 +8,8 @@ import sys
|
||||
# twisted's unittests have skip support, standard unittest don't
|
||||
from twisted.trial import unittest
|
||||
|
||||
from morituri.common import log
|
||||
from morituri.configure import configure
|
||||
|
||||
log.init()
|
||||
|
||||
# lifted from flumotion
|
||||
|
||||
|
||||
@@ -44,7 +41,7 @@ def diffStrings(orig, new, desc='input'):
|
||||
desc=desc)
|
||||
|
||||
|
||||
class TestCase(log.Loggable, unittest.TestCase):
|
||||
class TestCase(unittest.TestCase):
|
||||
# unittest.TestCase.failUnlessRaises does not return the exception,
|
||||
# and we'd like to check for the actual exception under TaskException,
|
||||
# so override the way twisted.trial.unittest does, without failure
|
||||
@@ -55,13 +52,13 @@ class TestCase(log.Loggable, unittest.TestCase):
|
||||
except exception, inst:
|
||||
return inst
|
||||
except exception, e:
|
||||
raise self.failureException('%s raised instead of %s:\n %s'
|
||||
% (sys.exc_info()[0],
|
||||
exception.__name__,
|
||||
log.getExceptionMessage(e)))
|
||||
raise Exception('%s raised instead of %s:\n %s' %
|
||||
(sys.exec_info()[0], exception.__name__, str(e))
|
||||
)
|
||||
else:
|
||||
raise self.failureException('%s not raised (%r returned)'
|
||||
% (exception.__name__, result))
|
||||
raise Exception('%s not raised (%r returned)' %
|
||||
(exception.__name__, result)
|
||||
)
|
||||
|
||||
assertRaises = failUnlessRaises
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
REM DISCID B90C650D
|
||||
REM COMMENT "morituri 0.3.0"
|
||||
REM COMMENT "morituri 0.4.0"
|
||||
CATALOG 0602517642256
|
||||
FILE "data.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
|
||||
@@ -9,7 +9,7 @@ import unittest
|
||||
|
||||
from morituri.result import result
|
||||
from morituri.common import program, accurip, mbngs, config
|
||||
from morituri.rip import common as rcommon
|
||||
from morituri.command.cd import DEFAULT_DISC_TEMPLATE
|
||||
|
||||
|
||||
class TrackImageVerifyTestCase(unittest.TestCase):
|
||||
@@ -89,7 +89,7 @@ class PathTestCase(unittest.TestCase):
|
||||
def testStandardTemplateEmpty(self):
|
||||
prog = program.Program(config.Config())
|
||||
|
||||
path = prog.getPath(u'/tmp', rcommon.DEFAULT_DISC_TEMPLATE,
|
||||
path = prog.getPath(u'/tmp', DEFAULT_DISC_TEMPLATE,
|
||||
'mbdiscid', 0)
|
||||
self.assertEquals(path,
|
||||
u'/tmp/unknown/Unknown Artist - mbdiscid/Unknown Artist - mbdiscid')
|
||||
@@ -101,7 +101,7 @@ class PathTestCase(unittest.TestCase):
|
||||
md.title = 'Grace'
|
||||
prog.metadata = md
|
||||
|
||||
path = prog.getPath(u'/tmp', rcommon.DEFAULT_DISC_TEMPLATE,
|
||||
path = prog.getPath(u'/tmp', DEFAULT_DISC_TEMPLATE,
|
||||
'mbdiscid', 0)
|
||||
self.assertEquals(path,
|
||||
u'/tmp/unknown/Jeff Buckley - Grace/Jeff Buckley - Grace')
|
||||
|
||||
@@ -10,14 +10,12 @@ gobject.threads_init()
|
||||
import gst
|
||||
|
||||
from morituri.image import image
|
||||
from morituri.common import common, log
|
||||
from morituri.common import common
|
||||
|
||||
from morituri.extern.task import task, gstreamer
|
||||
|
||||
from morituri.test import common as tcommon
|
||||
|
||||
log.init()
|
||||
|
||||
|
||||
def h(i):
|
||||
return "0x%08x" % i
|
||||
|
||||
Reference in New Issue
Block a user