diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 59e679ef..00000000 --- a/.gitmodules +++ /dev/null @@ -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 diff --git a/README.md b/README.md index dd519a56..4ca9bce4 100644 --- a/README.md +++ b/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. diff --git a/morituri/__init__.py b/morituri/__init__.py index e69de29b..bdf7a362 100644 --- a/morituri/__init__.py +++ b/morituri/__init__.py @@ -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) diff --git a/morituri/rip/__init__.py b/morituri/command/__init__.py similarity index 100% rename from morituri/rip/__init__.py rename to morituri/command/__init__.py diff --git a/morituri/rip/accurip.py b/morituri/command/accurip.py similarity index 71% rename from morituri/rip/accurip.py rename to morituri/command/accurip.py index bda12d00..09ae0875 100644 --- a/morituri/rip/accurip.py +++ b/morituri/command/accurip.py @@ -20,39 +20,44 @@ # You should have received a copy of the GNU General Public License # along with morituri. If not, see . -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 @@ def do(self, args): 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 + } diff --git a/morituri/command/basecommand.py b/morituri/command/basecommand.py new file mode 100644 index 00000000..563dde03 --- /dev/null +++ b/morituri/command/basecommand.py @@ -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 diff --git a/morituri/rip/cd.py b/morituri/command/cd.py similarity index 69% rename from morituri/rip/cd.py rename to morituri/command/cd.py index 3939a60a..f2804689 100644 --- a/morituri/rip/cd.py +++ b/morituri/command/cd.py @@ -20,29 +20,57 @@ # You should have received a copy of the GNU General Public License # along with morituri. If not, see . +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' + +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(logcommand.LogCommand): +''' + + +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 @@ def do(self, args): # 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 @@ def do(self, args): 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 @@ def do(self, args): 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 @@ def do(self, args): 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 @@ def doCommand(self): 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 @@ class Rip(_CD): 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 + + _CD.add_arguments(self.parser) - self.parser.add_option('-L', '--logger', - action="store", dest="logger", - default='morituri', - help="logger to use " - "(default '%default', choose from '" + - "', '".join(loggers) + "')") + 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') - - 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 - - if options.offset is None: + def handle_arguments(self): + self.options.output_directory = os.path.expanduser(self.options.output_directory) + + self.options.track_template = self.options.track_template.decode('utf-8') + self.options.disc_template = self.options.disc_template.decode('utf-8') + + 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 @@ def handleOptions(self, options): "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 @@ def doCommand(self): 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 @@ def doCommand(self): 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 @@ def doCommand(self): # 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 @@ def ripIfNotRipped(number): 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 @@ def ripIfNotRipped(number): 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 @@ def ripIfNotRipped(number): 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 @@ def ripIfNotRipped(number): 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 @@ def ripIfNotRipped(number): 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 @@ def ripIfNotRipped(number): 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 @@ def ripIfNotRipped(number): 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 @@ def writeFile(handle, path, length): # 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 @@ def writeFile(handle, path, length): 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 @@ def writeFile(handle, path, length): 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 @@ def writeFile(handle, path, length): self.program.ejectDevice(self.device) -class CD(logcommand.LogCommand): - - 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] +class CD(BaseCommand): + summary = "handle CDs" + description = "Display and rip CD-DA and metadata." + device_option = True - # this can be a symlink to another device - self.options.device = os.path.realpath(self.options.device) + subcommands = { + 'info': Info, + 'rip': Rip + } diff --git a/morituri/rip/debug.py b/morituri/command/debug.py similarity index 50% rename from morituri/rip/debug.py rename to morituri/command/debug.py index f6d28945..ede50799 100644 --- a/morituri/rip/debug.py +++ b/morituri/command/debug.py @@ -20,15 +20,19 @@ # You should have received a copy of the GNU General Public License # along with morituri. If not, see . -from morituri.common import logcommand -from morituri.result import result +import argparse +import sys -from morituri.common import task, cache +from morituri.command.basecommand import BaseCommand +from morituri.common import cache, task +from morituri.result import result -class RCCue(logcommand.LogCommand): +import logging +logger = logging.getLogger(__name__) - 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 @@ def do(self, args): 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 @@ def do(self, args): 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 @@ def do(self, args): 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', ] - - subCommandClasses = [RCCue, RCList, RCLog, ] + description = summary + 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 @@ class MusicBrainzNGS(logcommand.LogCommand): 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" - - subCommandClasses = [Checksum, Encode, MaxSample, Tag, MusicBrainzNGS, - ResultCache, Version] + description = "debug internals" + + subcommands = { + 'checksum': Checksum, + 'encode': Encode, + 'maxsample': MaxSample, + 'tag': Tag, + 'musicbrainzngs': MusicBrainzNGS, + 'resultcache': ResultCache, + 'version': Version, + } diff --git a/morituri/rip/drive.py b/morituri/command/drive.py similarity index 60% rename from morituri/rip/drive.py rename to morituri/command/drive.py index 2e136d46..91b43ed0 100644 --- a/morituri/rip/drive.py +++ b/morituri/command/drive.py @@ -20,116 +20,105 @@ # You should have received a copy of the GNU General Public License # along with morituri. If not, see . -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 + } diff --git a/morituri/rip/image.py b/morituri/command/image.py similarity index 51% rename from morituri/rip/image.py rename to morituri/command/image.py index 156ec341..52a22d3b 100644 --- a/morituri/rip/image.py +++ b/morituri/command/image.py @@ -21,117 +21,60 @@ # along with morituri. If not, see . 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 - - -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]() +import logging +logger = logging.getLogger(__name__) - 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 @@ def do(self, args): 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 @@ def do(self, args): 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 @@ def do(self, args): 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 + } diff --git a/morituri/command/main.py b/morituri/command/main.py new file mode 100644 index 00000000..0f123384 --- /dev/null +++ b/morituri/command/main.py @@ -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) diff --git a/morituri/rip/offset.py b/morituri/command/offset.py similarity index 71% rename from morituri/rip/offset.py rename to morituri/command/offset.py index a9b5982c..f8e97746 100644 --- a/morituri/rip/offset.py +++ b/morituri/command/offset.py @@ -20,18 +20,24 @@ # You should have received a copy of the GNU General Public License # along with morituri. If not, see . +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 @@ "+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 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 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 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 @@ def handleOptions(self, options): else: self._offsets.append(int(b)) - self.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 + logger.debug('Trying with offsets %r', self._offsets) - # 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 @@ def do(self, args): 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 @@ def do(self, args): 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 @@ def match(archecksum, track, responses): 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 @@ def match(archecksum, track, responses): 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 @@ def match(archecksum, track, responses): 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 @@ def match(archecksum, track, responses): 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 @@ def _arcs(self, runner, table, track, offset): 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, + } diff --git a/morituri/common/accurip.py b/morituri/common/accurip.py index 74518034..e9fb31f0 100644 --- a/morituri/common/accurip.py +++ b/morituri/common/accurip.py @@ -26,16 +26,19 @@ 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 @@ def _getPath(self, url): 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 @@ def _cache(self, url, data): 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 @@ def _cache(self, url, data): 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() diff --git a/morituri/common/cache.py b/morituri/common/cache.py index 0a2d473a..260abe18 100644 --- a/morituri/common/cache.py +++ b/morituri/common/cache.py @@ -29,10 +29,10 @@ 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 @@ def persist(self, obj=None): 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 @@ def _unpickle(self, default=None): 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 @@ def delete(self): os.unlink(self._path) -class PersistedCache(log.Loggable): +class PersistedCache: """ I wrap a directory of persisted objects. """ @@ -142,7 +142,7 @@ def get(self, key): 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 @@ def get(self, key): 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 @@ def getRipResult(self, cddbdiscid, create=True): 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 @@ def getIds(self): 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 @@ def get(self, cddbdiscid, mbdiscid): 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: diff --git a/morituri/common/checksum.py b/morituri/common/checksum.py index da850265..8786d158 100644 --- a/morituri/common/checksum.py +++ b/morituri/common/checksum.py @@ -26,21 +26,21 @@ 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 @@ def __init__(self, path, sampleStart=0, sampleLength=-1): # 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 @@ def _getSampleLength(self): # 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 @@ def _getSampleLength(self): # 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 @@ def paused(self): 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 @@ def play(): 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 @@ def _new_buffer_cb(self, sink): 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 @@ def _new_buffer_cb(self, sink): 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 @@ def do_checksum_buffer(self, buf, checksum): 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 diff --git a/morituri/common/common.py b/morituri/common/common.py index 76efeeaf..b7d7ecd7 100644 --- a/morituri/common/common.py +++ b/morituri/common/common.py @@ -28,7 +28,9 @@ 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)) diff --git a/morituri/common/config.py b/morituri/common/config.py index 2ff3078b..d9229e64 100644 --- a/morituri/common/config.py +++ b/morituri/common/config.py @@ -20,17 +20,19 @@ # You should have received a copy of the GNU General Public License # along with morituri. If not, see . +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 @@ def open(self): 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 @@ def _findDriveSection(self, vendor, model, release): 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 @@ def _findOrCreateDriveSection(self, vendor, model, release): self.write() return self._findDriveSection(vendor, model, release) - - diff --git a/morituri/common/drive.py b/morituri/common/drive.py index a206ac00..f0e58679 100644 --- a/morituri/common/drive.py +++ b/morituri/common/drive.py @@ -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() diff --git a/morituri/common/encode.py b/morituri/common/encode.py index 29fedc93..1203fe7f 100644 --- a/morituri/common/encode.py +++ b/morituri/common/encode.py @@ -25,15 +25,17 @@ 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 @@ def test(self): 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 @@ def parsed(self): 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 @@ def paused(self): # 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 @@ def paused(self): 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 @@ def _probe_handler(self, pad, buffer): 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 @@ def _message_element_cb(self, bus, message): 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 @@ def _message_element_cb(self, bus, message): 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 @@ def getPipelineDesc(self): 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 @@ def start(self, runner): 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 @@ def play(): 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 @@ def stopped(self, taskk): 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 @@ def stopped(self, taskk): 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 @@ def stopped(self, taskk): 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) diff --git a/morituri/common/gstreamer.py b/morituri/common/gstreamer.py index fd5c38a9..ed1e5c35 100644 --- a/morituri/common/gstreamer.py +++ b/morituri/common/gstreamer.py @@ -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 diff --git a/morituri/common/log.py b/morituri/common/log.py deleted file mode 100644 index f0244ea4..00000000 --- a/morituri/common/log.py +++ /dev/null @@ -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 . - -""" -Logging -""" - -from morituri.extern.log import log as externlog -from morituri.extern.log.log import * - - -def init(): - externlog.init('RIP_DEBUG') - externlog.setPackageScrubList('morituri') diff --git a/morituri/common/logcommand.py b/morituri/common/logcommand.py deleted file mode 100644 index 32c3afc4..00000000 --- a/morituri/common/logcommand.py +++ /dev/null @@ -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 . - -""" -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) diff --git a/morituri/common/mbngs.py b/morituri/common/mbngs.py index 58ec0990..f29982a8 100644 --- a/morituri/common/mbngs.py +++ b/morituri/common/mbngs.py @@ -26,7 +26,8 @@ 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 diff --git a/morituri/common/program.py b/morituri/common/program.py index 3bc8400d..1d40cd27 100644 --- a/morituri/common/program.py +++ b/morituri/common/program.py @@ -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 @@ def __init__(self, config, record=False, stdout=sys.stdout): 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 @@ def unmountDevice(self, device): 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 @@ def function(r, t): 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 @@ def getTable(self, runner, cddbdiscid, mbdiscid, device, offset): 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 @@ def getPath(self, outdir, template, mbdiscid, i, profile=None, 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 @@ def getCDDB(self, cddbdiscid): 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 @@ def getMusicBrainz(self, ittoc, mbdiscid, release=None, country=None, prompt=Fal 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 @@ def getMusicBrainz(self, ittoc, mbdiscid, release=None, country=None, prompt=Fal 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 @@ def getMusicBrainz(self, ittoc, mbdiscid, release=None, country=None, prompt=Fal 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 @@ def getTagList(self, number): # 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 @@ def verifyTrack(self, runner, trackResult): 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 @@ def ripTrack(self, runner, trackResult, offset, device, profile, taglist, 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 @@ def ripTrack(self, runner, trackResult, offset, device, profile, taglist, 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 @@ def verifyImage(self, runner, responses): 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 @@ def _verifyImageWithChecksums(self, responses, checksums): 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 @@ def _verifyImageWithChecksums(self, responses, checksums): 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 @@ def _verifyImageWithChecksums(self, responses, checksums): 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 @@ def _verifyImageWithChecksums(self, responses, checksums): 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 @@ def getAccurateRipResults(self): 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')) diff --git a/morituri/common/task.py b/morituri/common/task.py index a40acc23..f43cf172 100644 --- a/morituri/common/task.py +++ b/morituri/common/task.py @@ -6,27 +6,29 @@ 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): +class LoggableTask(task.Task): pass -class LoggableMultiSeparateTask(log.Loggable, task.MultiSeparateTask): + +class LoggableMultiSeparateTask(task.MultiSeparateTask): pass -class GstPipelineTask(log.Loggable, gstreamer.GstPipelineTask): + +class GstPipelineTask(gstreamer.GstPipelineTask): pass -class PopenTask(log.Loggable, task.Task): +class PopenTask(task.Task): """ I am a task that runs a command using Popen. """ @@ -51,7 +53,7 @@ def start(self, runner): 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 @@ def _read(self, runner): 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 @@ def _read(self, runner): 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 @@ def _done(self): 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 @@ def _done(self): 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() diff --git a/morituri/configure/configure.py b/morituri/configure/configure.py index eac69c65..bb0f5b86 100644 --- a/morituri/configure/configure.py +++ b/morituri/configure/configure.py @@ -7,7 +7,7 @@ config_dict = { 'revision': common.getRevision(), - 'version': '0.3.0', + 'version': '0.4.0', } for key, value in config_dict.items(): diff --git a/morituri/extern/command b/morituri/extern/command deleted file mode 120000 index 7625d612..00000000 --- a/morituri/extern/command +++ /dev/null @@ -1 +0,0 @@ -python-command/command \ No newline at end of file diff --git a/morituri/extern/flog b/morituri/extern/flog deleted file mode 160000 index a38ebac8..00000000 --- a/morituri/extern/flog +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a38ebac80eb21002a3b63e7e30c059ab028f943b diff --git a/morituri/extern/log b/morituri/extern/log deleted file mode 120000 index bc6368c2..00000000 --- a/morituri/extern/log +++ /dev/null @@ -1 +0,0 @@ -flog/log \ No newline at end of file diff --git a/morituri/extern/python-command b/morituri/extern/python-command deleted file mode 160000 index bea37f88..00000000 --- a/morituri/extern/python-command +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bea37f88ecb02db5342e52d3ab0f61ec33d85b1f diff --git a/morituri/image/cue.py b/morituri/image/cue.py index 18502ad0..c774ccf5 100644 --- a/morituri/image/cue.py +++ b/morituri/image/cue.py @@ -29,9 +29,12 @@ 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 @@ """, re.VERBOSE) -class CueFile(object, log.Loggable): +class CueFile(object): """ I represent a .cue file as an object. @@ -84,7 +87,7 @@ def parse(self): 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 @@ def parse(self): 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 @@ def parse(self): + 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, diff --git a/morituri/image/image.py b/morituri/image/image.py index 33c166ad..be2de710 100644 --- a/morituri/image/image.py +++ b/morituri/image/image.py @@ -26,15 +26,16 @@ 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 @@ def setup(self, runner): 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 @@ def setup(self, runner): 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 @@ def __init__(self, image): 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 @@ def stop(self): 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 @@ def __init__(self, image): 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 @@ def stop(self): 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 @@ def add(index): 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) diff --git a/morituri/image/table.py b/morituri/image/table.py index 590cefe9..193a0c7a 100644 --- a/morituri/image/table.py +++ b/morituri/image/table.py @@ -28,9 +28,12 @@ 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 @@ def __repr__(self): 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 @@ def __init__(self, tracks=None): 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 @@ def getCDDBValues(self): # 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 @@ def getCDDBValues(self): 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 @@ def getMusicBrainzDiscId(self): @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 @@ def getMusicBrainzDiscId(self): 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 @@ def getFrameLength(self, data=False): 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 @@ def _getMusicBrainzValues(self): pass - self.log('Musicbrainz values: %r', result) + logger.debug('Musicbrainz values: %r', result) return result def getAccurateRipIds(self): @@ -533,7 +536,7 @@ def cue(self, cuePath='', program='morituri'): @rtype: C{unicode} """ - self.debug('generating .cue for cuePath %r', cuePath) + logger.debug('generating .cue for cuePath %r', cuePath) lines = [] @@ -541,7 +544,7 @@ def writeFile(path): 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 @@ def writeFile(path): 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 @@ def writeFile(path): 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 @@ def writeFile(path): # 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 @@ def writeFile(path): 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 @@ def clearFiles(self): 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 @@ def setFile(self, track, index, path, length, counter=None): @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 @@ def setFile(self, track, index, path, length, counter=None): 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 @@ def absolutize(self): 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 @@ def merge(self, other, session=2): 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 @@ def hasTOC(self): 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 @@ def canCue(self): 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 diff --git a/morituri/image/toc.py b/morituri/image/toc.py index 622c2620..45ae12ff 100644 --- a/morituri/image/toc.py +++ b/morituri/image/toc.py @@ -29,9 +29,12 @@ 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\w+) "(?P.+)"') @@ -91,7 +94,7 @@ """, 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 @@ def append(self, counter, offset, source): @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 @@ def getCounterStart(self, counter): return self._sources[-1][1] -class TocFile(object, log.Loggable): +class TocFile(object): def __init__(self, path): """ @@ -151,7 +154,7 @@ def _index(self, currentTrack, i, absoluteOffset, trackOffset): absolute = absoluteOffset + trackOffset # this may be in a new source, so calculate relative c, o, s = self._sources.get(absolute) - self.debug('at abs offset %d, we are in source %r' % ( + 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 @@ def _index(self, currentTrack, i, absoluteOffset, trackOffset): 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 @@ def parse(self): # 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 @@ def parse(self): 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 @@ def parse(self): 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 @@ def parse(self): 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 @@ def parse(self): 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 @@ def parse(self): 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 @@ def parse(self): 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 @@ def parse(self): 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 @@ def parse(self): 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 @@ def parse(self): # 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): """ diff --git a/morituri/program/arc.py b/morituri/program/arc.py index 1d9a4134..1473fd27 100644 --- a/morituri/program/arc.py +++ b/morituri/program/arc.py @@ -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 diff --git a/morituri/program/cdparanoia.py b/morituri/program/cdparanoia.py index 918b9107..2bd61621 100644 --- a/morituri/program/cdparanoia.py +++ b/morituri/program/cdparanoia.py @@ -20,21 +20,23 @@ # You should have received a copy of the GNU General Public License # along with morituri. If not, see . -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 @@ class ChecksumException(Exception): # 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 @@ def _parse_read(self, wordOffset): # 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 @@ def getTrackQuality(self): """ 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 @@ def getTrackQuality(self): # 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 @@ def start(self, runner): 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 @@ def start(self, runner): 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 @@ def _read(self, runner): # 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 @@ def _done(self): 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 @@ def _done(self): 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 @@ def _done(self): 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 @@ def __init__(self, path, table, start, stop, overread, offset=0, """ 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 @@ def stop(self): 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 @@ def stop(self): 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 @@ def stop(self): 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, ) diff --git a/morituri/program/cdrdao.py b/morituri/program/cdrdao.py index 95ba4f69..97a79289 100644 --- a/morituri/program/cdrdao.py +++ b/morituri/program/cdrdao.py @@ -1,4 +1,3 @@ -import logging import os import re import tempfile @@ -6,6 +5,9 @@ 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.*) - \(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') diff --git a/morituri/program/sox.py b/morituri/program/sox.py index 93030e35..c8efa283 100644 --- a/morituri/program/sox.py +++ b/morituri/program/sox.py @@ -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 diff --git a/morituri/program/soxi.py b/morituri/program/soxi.py index 4473000f..36fb03a7 100644 --- a/morituri/program/soxi.py +++ b/morituri/program/soxi.py @@ -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 @@ def failed(self): 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)) diff --git a/morituri/rip/common.py b/morituri/rip/common.py deleted file mode 100644 index e931646a..00000000 --- a/morituri/rip/common.py +++ /dev/null @@ -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) diff --git a/morituri/rip/main.py b/morituri/rip/main.py deleted file mode 100644 index 93ad0001..00000000 --- a/morituri/rip/main.py +++ /dev/null @@ -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) diff --git a/morituri/test/bloc.cue b/morituri/test/bloc.cue index 5b5519c4..8c2fd080 100644 --- a/morituri/test/bloc.cue +++ b/morituri/test/bloc.cue @@ -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 diff --git a/morituri/test/breeders.cue b/morituri/test/breeders.cue index ab4390d5..aa326f12 100644 --- a/morituri/test/breeders.cue +++ b/morituri/test/breeders.cue @@ -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" diff --git a/morituri/test/common.py b/morituri/test/common.py index aebf004d..5195ce22 100644 --- a/morituri/test/common.py +++ b/morituri/test/common.py @@ -8,11 +8,8 @@ # 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 _tolines(s): 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 @@ def failUnlessRaises(self, exception, f, *args, **kwargs): 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 diff --git a/morituri/test/cure.cue b/morituri/test/cure.cue index 2eab2b8c..8b7ac9a4 100644 --- a/morituri/test/cure.cue +++ b/morituri/test/cure.cue @@ -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 diff --git a/morituri/test/test_common_program.py b/morituri/test/test_common_program.py index d49c8bb2..0cc4601a 100644 --- a/morituri/test/test_common_program.py +++ b/morituri/test/test_common_program.py @@ -9,7 +9,7 @@ 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 @@ def testStandardTemplateFilled(self): 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') diff --git a/morituri/test/test_image_image.py b/morituri/test/test_image_image.py index b9a1d02c..c0221c88 100644 --- a/morituri/test/test_image_image.py +++ b/morituri/test/test_image_image.py @@ -10,14 +10,12 @@ 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 diff --git a/setup.py b/setup.py index 9ac84b12..a1b5c691 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ packages=find_packages(), entry_points = { 'console_scripts': [ - 'whipper = morituri.rip.main:main' + 'whipper = morituri.command.main:main' ] } )