Rename "morituri" module to "whipper".
Fixes https://github.com/JoeLametta/whipper/issues/100
This commit is contained in:
0
whipper/command/__init__.py
Normal file
0
whipper/command/__init__.py
Normal file
102
whipper/command/accurip.py
Normal file
102
whipper/command/accurip.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# -*- 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 whipper.
|
||||
#
|
||||
# whipper 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.
|
||||
#
|
||||
# whipper 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 whipper. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
|
||||
from whipper.command.basecommand import BaseCommand
|
||||
from whipper.common import accurip
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Show(BaseCommand):
|
||||
summary = "show accuraterip data"
|
||||
description = """
|
||||
retrieves and display accuraterip data from the given URL
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
sys.stdout.write("Found %d responses for %d tracks\n\n" % (
|
||||
len(responses), count))
|
||||
|
||||
for (i, r) in enumerate(responses):
|
||||
if r.trackCount != count:
|
||||
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):
|
||||
sys.stdout.write("Track %d:\n" % (track + 1))
|
||||
checksums = {}
|
||||
|
||||
for (i, r) in enumerate(responses):
|
||||
if r.trackCount != count:
|
||||
continue
|
||||
|
||||
assert len(r.checksums) == r.trackCount
|
||||
assert len(r.confidences) == r.trackCount
|
||||
|
||||
entry = {}
|
||||
entry["confidence"] = r.confidences[track]
|
||||
entry["response"] = i + 1
|
||||
checksum = r.checksums[track]
|
||||
if checksum in checksums:
|
||||
checksums[checksum].append(entry)
|
||||
else:
|
||||
checksums[checksum] = [entry, ]
|
||||
|
||||
# now sort track results in checksum by highest confidence
|
||||
sortedChecksums = []
|
||||
for checksum, entries in checksums.items():
|
||||
highest = max(d['confidence'] for d in entries)
|
||||
sortedChecksums.append((highest, checksum))
|
||||
|
||||
sortedChecksums.sort()
|
||||
sortedChecksums.reverse()
|
||||
|
||||
for highest, checksum in sortedChecksums:
|
||||
sys.stdout.write(" %d result(s) for checksum %s: %s\n" % (
|
||||
len(checksums[checksum]), checksum,
|
||||
str(checksums[checksum])))
|
||||
|
||||
|
||||
class AccuRip(BaseCommand):
|
||||
summary = "handle AccurateRip information"
|
||||
description = """
|
||||
Handle AccurateRip information. Retrieves AccurateRip disc entries and
|
||||
displays diagnostic information.
|
||||
"""
|
||||
subcommands = {
|
||||
'show': Show
|
||||
}
|
||||
129
whipper/command/basecommand.py
Normal file
129
whipper/command/basecommand.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# -*- Mode: Python -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
from whipper.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
|
||||
589
whipper/command/cd.py
Normal file
589
whipper/command/cd.py
Normal file
@@ -0,0 +1,589 @@
|
||||
# -*- 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 whipper.
|
||||
#
|
||||
# whipper 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.
|
||||
#
|
||||
# whipper 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 whipper. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import glob
|
||||
import urllib2
|
||||
import socket
|
||||
import sys
|
||||
|
||||
import gobject
|
||||
gobject.threads_init()
|
||||
|
||||
from whipper.command.basecommand import BaseCommand
|
||||
from whipper.common import (
|
||||
accurip, common, config, drive, program, task
|
||||
)
|
||||
from whipper.program import cdrdao, cdparanoia, utils
|
||||
from whipper.result import result
|
||||
|
||||
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(BaseCommand):
|
||||
|
||||
"""
|
||||
@type program: L{program.Program}
|
||||
@ivar eject: whether to eject the drive after completing
|
||||
"""
|
||||
|
||||
eject = True
|
||||
|
||||
@staticmethod
|
||||
def add_arguments(parser):
|
||||
# FIXME: have a cache of these pickles somewhere
|
||||
parser.add_argument('-T', '--toc-pickle',
|
||||
action="store", dest="toc_pickle",
|
||||
help="pickle to use for reading and writing the TOC")
|
||||
parser.add_argument('-R', '--release-id',
|
||||
action="store", dest="release_id",
|
||||
help="MusicBrainz release id to match to (if there are multiple)")
|
||||
parser.add_argument('-p', '--prompt',
|
||||
action="store_true", dest="prompt",
|
||||
help="Prompt if there are multiple matching releases")
|
||||
parser.add_argument('-c', '--country',
|
||||
action="store", dest="country",
|
||||
help="Filter releases by country")
|
||||
|
||||
|
||||
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.device = self.options.device
|
||||
sys.stdout.write('Checking device %s\n' % self.device)
|
||||
|
||||
utils.load_device(self.device)
|
||||
utils.unmount_device(self.device)
|
||||
|
||||
# first, read the normal TOC, which is fast
|
||||
self.ittoc = self.program.getFastToc(self.runner,
|
||||
self.options.toc_pickle,
|
||||
self.device)
|
||||
|
||||
# already show us some info based on this
|
||||
self.program.getRipResult(self.ittoc.getCDDBDiscId())
|
||||
sys.stdout.write("CDDB disc id: %s\n" % self.ittoc.getCDDBDiscId())
|
||||
self.mbdiscid = self.ittoc.getMusicBrainzDiscId()
|
||||
sys.stdout.write("MusicBrainz disc id %s\n" % self.mbdiscid)
|
||||
|
||||
sys.stdout.write("MusicBrainz lookup URL %s\n" %
|
||||
self.ittoc.getMusicBrainzSubmitURL())
|
||||
|
||||
self.program.metadata = self.program.getMusicBrainz(self.ittoc,
|
||||
self.mbdiscid,
|
||||
release=self.options.release_id,
|
||||
country=self.options.country,
|
||||
prompt=self.options.prompt)
|
||||
|
||||
if not self.program.metadata:
|
||||
# fall back to FreeDB for lookup
|
||||
cddbid = self.ittoc.getCDDBValues()
|
||||
cddbmd = self.program.getCDDB(cddbid)
|
||||
if cddbmd:
|
||||
sys.stdout.write('FreeDB identifies disc as %s\n' % cddbmd)
|
||||
|
||||
# also used by rip cd info
|
||||
if not getattr(self.options, 'unknown', False):
|
||||
logger.critical("unable to retrieve disc metadata, "
|
||||
"--unknown not passed")
|
||||
return -1
|
||||
|
||||
# FIXME ?????
|
||||
# Hackish fix for broken commit
|
||||
offset = 0
|
||||
info = drive.getDeviceInfo(self.device)
|
||||
if info:
|
||||
try:
|
||||
offset = self.config.getReadOffset(*info)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# now, read the complete index table, which is slower
|
||||
self.itable = self.program.getTable(self.runner,
|
||||
self.ittoc.getCDDBDiscId(),
|
||||
self.ittoc.getMusicBrainzDiscId(), self.device, offset)
|
||||
|
||||
assert self.itable.getCDDBDiscId() == self.ittoc.getCDDBDiscId(), \
|
||||
"full table's id %s differs from toc id %s" % (
|
||||
self.itable.getCDDBDiscId(), self.ittoc.getCDDBDiscId())
|
||||
assert self.itable.getMusicBrainzDiscId() == \
|
||||
self.ittoc.getMusicBrainzDiscId(), \
|
||||
"full table's mb id %s differs from toc id mb %s" % (
|
||||
self.itable.getMusicBrainzDiscId(),
|
||||
self.ittoc.getMusicBrainzDiscId())
|
||||
assert self.itable.getAccurateRipURL() == \
|
||||
self.ittoc.getAccurateRipURL(), \
|
||||
"full table's AR URL %s differs from toc AR URL %s" % (
|
||||
self.itable.getAccurateRipURL(), self.ittoc.getAccurateRipURL())
|
||||
|
||||
if self.program.metadata:
|
||||
self.program.metadata.discid = self.ittoc.getMusicBrainzDiscId()
|
||||
|
||||
# result
|
||||
|
||||
self.program.result.cdrdaoVersion = cdrdao.getCDRDAOVersion()
|
||||
self.program.result.cdparanoiaVersion = \
|
||||
cdparanoia.getCdParanoiaVersion()
|
||||
info = drive.getDeviceInfo(self.device)
|
||||
if info:
|
||||
try:
|
||||
self.program.result.cdparanoiaDefeatsCache = \
|
||||
self.config.getDefeatsCache(*info)
|
||||
except KeyError, e:
|
||||
logger.debug('Got key error: %r' % (e, ))
|
||||
self.program.result.artist = self.program.metadata \
|
||||
and self.program.metadata.artist \
|
||||
or 'Unknown Artist'
|
||||
self.program.result.title = self.program.metadata \
|
||||
and self.program.metadata.title \
|
||||
or 'Unknown Title'
|
||||
try:
|
||||
import cdio
|
||||
_, self.program.result.vendor, self.program.result.model, \
|
||||
self.program.result.release = \
|
||||
cdio.Device(self.device).get_hwinfo()
|
||||
except ImportError:
|
||||
raise ImportError("Pycdio module import failed.\n"
|
||||
"This is a hard dependency: if not "
|
||||
"available please install it")
|
||||
|
||||
self.doCommand()
|
||||
|
||||
if self.options.eject in ('success', 'always'):
|
||||
utils.eject_device(self.device)
|
||||
|
||||
def doCommand(self):
|
||||
pass
|
||||
|
||||
|
||||
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 whipper.common.program.Program.getPath for expansion
|
||||
description = """
|
||||
Rips a CD.
|
||||
|
||||
%s
|
||||
|
||||
Paths to track files referenced in .cue and .m3u files will be made
|
||||
relative to the directory of the disc files.
|
||||
|
||||
All files will be created relative to the given output directory.
|
||||
Log files will log the path to tracks relative to this directory.
|
||||
""" % TEMPLATE_DESCRIPTION
|
||||
formatter_class = argparse.ArgumentDefaultsHelpFormatter
|
||||
|
||||
# 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_argument('-L', '--logger',
|
||||
action="store", dest="logger", default='morituri',
|
||||
help="logger to use (choose from '" + "', '".join(loggers) + "')")
|
||||
# FIXME: get from config
|
||||
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. ")
|
||||
self.parser.add_argument('-O', '--output-directory',
|
||||
action="store", dest="output_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")
|
||||
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=False)
|
||||
|
||||
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 'whipper offset "
|
||||
"find' to detect your drive's offset or set it "
|
||||
"manually in the configuration file. It can "
|
||||
"also be specified at runtime using the "
|
||||
"'--offset=value' argument")
|
||||
|
||||
|
||||
if self.options.working_directory is not None:
|
||||
self.options.working_directory = os.path.expanduser(self.options.working_directory)
|
||||
|
||||
if self.options.logger:
|
||||
try:
|
||||
self.logger = result.getLoggers()[self.options.logger]()
|
||||
except KeyError:
|
||||
msg = "No logger named %s found!" % self.options.logger
|
||||
logger.critical(msg)
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def doCommand(self):
|
||||
self.program.setWorkingDirectory(self.options.working_directory)
|
||||
self.program.outdir = self.options.output_directory.decode('utf-8')
|
||||
self.program.result.offset = int(self.options.offset)
|
||||
self.program.result.overread = self.options.overread
|
||||
self.program.result.logger = self.options.logger
|
||||
|
||||
### write disc files
|
||||
disambiguate = False
|
||||
while True:
|
||||
discName = self.program.getPath(self.program.outdir,
|
||||
self.options.disc_template, self.mbdiscid, 0,
|
||||
disambiguate=disambiguate)
|
||||
dirname = os.path.dirname(discName)
|
||||
if os.path.exists(dirname):
|
||||
sys.stdout.write("Output directory %s already exists\n" %
|
||||
dirname.encode('utf-8'))
|
||||
logs = glob.glob(os.path.join(dirname, '*.log'))
|
||||
if logs:
|
||||
sys.stdout.write(
|
||||
"Output directory %s is a finished rip\n" %
|
||||
dirname.encode('utf-8'))
|
||||
if not disambiguate:
|
||||
disambiguate = True
|
||||
continue
|
||||
return
|
||||
else:
|
||||
break
|
||||
|
||||
else:
|
||||
sys.stdout.write("Creating output directory %s\n" %
|
||||
dirname.encode('utf-8'))
|
||||
os.makedirs(dirname)
|
||||
break
|
||||
|
||||
# FIXME: say when we're continuing a rip
|
||||
# FIXME: disambiguate if the pre-existing rip is different
|
||||
|
||||
|
||||
# FIXME: turn this into a method
|
||||
|
||||
def ripIfNotRipped(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:
|
||||
logger.debug('ripIfNotRipped have trackresult, path %r' %
|
||||
trackResult.filename)
|
||||
|
||||
path = self.program.getPath(self.program.outdir,
|
||||
self.options.track_template,
|
||||
self.mbdiscid, number,
|
||||
disambiguate=disambiguate) \
|
||||
+ '.' + 'flac'
|
||||
logger.debug('ripIfNotRipped: path %r' % path)
|
||||
trackResult.number = number
|
||||
|
||||
assert type(path) is unicode, "%r is not unicode" % path
|
||||
trackResult.filename = path
|
||||
if number > 0:
|
||||
trackResult.pregap = self.itable.tracks[number - 1].getPregap()
|
||||
|
||||
trackResult.pre_emphasis = self.itable.tracks[number - 1].pre_emphasis
|
||||
|
||||
# FIXME: optionally allow overriding reripping
|
||||
if os.path.exists(path):
|
||||
if path != trackResult.filename:
|
||||
# the path is different (different name/template ?)
|
||||
# but we can copy it
|
||||
logger.debug('previous result %r, expected %r' % (
|
||||
trackResult.filename, path))
|
||||
|
||||
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):
|
||||
sys.stdout.write('Verification failed, reripping...\n')
|
||||
os.unlink(path)
|
||||
|
||||
if not os.path.exists(path):
|
||||
logger.debug('path %r does not exist, ripping...' % path)
|
||||
tries = 0
|
||||
# we reset durations for test and copy here
|
||||
trackResult.testduration = 0.0
|
||||
trackResult.copyduration = 0.0
|
||||
extra = ""
|
||||
while tries < MAX_TRIES:
|
||||
tries += 1
|
||||
if tries > 1:
|
||||
extra = " (try %d)" % tries
|
||||
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:
|
||||
logger.debug('ripIfNotRipped: track %d, try %d',
|
||||
number, tries)
|
||||
self.program.ripTrack(self.runner, trackResult,
|
||||
offset=int(self.options.offset),
|
||||
device=self.device,
|
||||
taglist=self.program.getTagList(number),
|
||||
overread=self.options.overread,
|
||||
what='track %d of %d%s' % (
|
||||
number, len(self.itable.tracks), extra))
|
||||
break
|
||||
except Exception, e:
|
||||
logger.debug('Got exception %r on try %d',
|
||||
e, tries)
|
||||
|
||||
|
||||
if tries == MAX_TRIES:
|
||||
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:
|
||||
sys.stdout.write('Checksums match for track %d\n' %
|
||||
number)
|
||||
else:
|
||||
sys.stdout.write(
|
||||
'ERROR: checksums did not match for track %d\n' %
|
||||
number)
|
||||
raise
|
||||
|
||||
sys.stdout.write('Peak level: {:.2%} \n'.format(trackResult.peak))
|
||||
|
||||
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:
|
||||
logger.debug('HTOA peak %r is below SILENT threshold, disregarding', trackResult.peak)
|
||||
self.itable.setFile(1, 0, None,
|
||||
self.ittoc.getTrackStart(1), number)
|
||||
logger.debug('Unlinking %r', trackResult.filename)
|
||||
os.unlink(trackResult.filename)
|
||||
trackResult.filename = None
|
||||
sys.stdout.write('HTOA discarded, contains digital silence\n')
|
||||
else:
|
||||
self.itable.setFile(1, 0, trackResult.filename,
|
||||
self.ittoc.getTrackStart(1), number)
|
||||
else:
|
||||
self.itable.setFile(number, 1, trackResult.filename,
|
||||
self.ittoc.getTrackLength(number), number)
|
||||
|
||||
self.program.saveRipResult()
|
||||
|
||||
|
||||
# check for hidden track one audio
|
||||
htoapath = None
|
||||
htoa = self.program.getHTOA()
|
||||
if htoa:
|
||||
start, stop = htoa
|
||||
sys.stdout.write(
|
||||
'Found Hidden Track One Audio from frame %d to %d\n' % (
|
||||
start, stop))
|
||||
|
||||
# rip it
|
||||
ripIfNotRipped(0)
|
||||
htoapath = self.program.result.tracks[0].filename
|
||||
|
||||
for i, track in enumerate(self.itable.tracks):
|
||||
# FIXME: rip data tracks differently
|
||||
if not track.audio:
|
||||
sys.stdout.write(
|
||||
'WARNING: skipping data track %d, not implemented\n' % (
|
||||
i + 1, ))
|
||||
# FIXME: make it work for now
|
||||
track.indexes[1].relative = 0
|
||||
continue
|
||||
|
||||
ripIfNotRipped(i + 1)
|
||||
|
||||
### write disc files
|
||||
discName = self.program.getPath(self.program.outdir,
|
||||
self.options.disc_template, self.mbdiscid, 0,
|
||||
disambiguate=disambiguate)
|
||||
dirname = os.path.dirname(discName)
|
||||
if not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
|
||||
logger.debug('writing cue file for %r', discName)
|
||||
self.program.writeCue(discName)
|
||||
|
||||
# write .m3u file
|
||||
logger.debug('writing m3u file for %r', discName)
|
||||
m3uPath = u'%s.m3u' % discName
|
||||
handle = open(m3uPath, 'w')
|
||||
u = u'#EXTM3U\n'
|
||||
handle.write(u.encode('utf-8'))
|
||||
|
||||
def writeFile(handle, path, length):
|
||||
targetPath = common.getRelativePath(path, m3uPath)
|
||||
u = u'#EXTINF:%d,%s\n' % (length, targetPath)
|
||||
handle.write(u.encode('utf-8'))
|
||||
u = '%s\n' % targetPath
|
||||
handle.write(u.encode('utf-8'))
|
||||
|
||||
|
||||
if htoapath:
|
||||
writeFile(handle, htoapath,
|
||||
self.itable.getTrackStart(1) / common.FRAMES_PER_SECOND)
|
||||
|
||||
for i, track in enumerate(self.itable.tracks):
|
||||
if not track.audio:
|
||||
continue
|
||||
|
||||
path = self.program.getPath(self.program.outdir,
|
||||
self.options.track_template, self.mbdiscid, i + 1,
|
||||
disambiguate=disambiguate) + '.' + 'flac'
|
||||
writeFile(handle, path,
|
||||
self.itable.getTrackLength(i + 1) / common.FRAMES_PER_SECOND)
|
||||
|
||||
handle.close()
|
||||
|
||||
# verify using accuraterip
|
||||
url = self.ittoc.getAccurateRipURL()
|
||||
sys.stdout.write("AccurateRip URL %s\n" % url)
|
||||
|
||||
accucache = accurip.AccuCache()
|
||||
try:
|
||||
responses = accucache.retrieve(url)
|
||||
except urllib2.URLError, e:
|
||||
if isinstance(e.args[0], socket.gaierror):
|
||||
if e.args[0].errno == -2:
|
||||
sys.stdout.write("Warning: network error: %r\n" % (
|
||||
e.args[0], ))
|
||||
responses = None
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
raise
|
||||
|
||||
if not responses:
|
||||
sys.stdout.write('Album not found in AccurateRip database\n')
|
||||
|
||||
if responses:
|
||||
sys.stdout.write('%d AccurateRip reponses found\n' %
|
||||
len(responses))
|
||||
|
||||
if responses[0].cddbDiscId != self.itable.getCDDBDiscId():
|
||||
sys.stdout.write(
|
||||
"AccurateRip response discid different: %s\n" %
|
||||
responses[0].cddbDiscId)
|
||||
|
||||
|
||||
self.program.verifyImage(self.runner, responses)
|
||||
|
||||
sys.stdout.write("\n".join(
|
||||
self.program.getAccurateRipResults()) + "\n")
|
||||
|
||||
self.program.saveRipResult()
|
||||
|
||||
# write log file
|
||||
self.program.writeLog(discName, self.logger)
|
||||
|
||||
|
||||
class CD(BaseCommand):
|
||||
summary = "handle CDs"
|
||||
description = "Display and rip CD-DA and metadata."
|
||||
device_option = True
|
||||
|
||||
subcommands = {
|
||||
'info': Info,
|
||||
'rip': Rip
|
||||
}
|
||||
304
whipper/command/debug.py
Normal file
304
whipper/command/debug.py
Normal file
@@ -0,0 +1,304 @@
|
||||
# -*- 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 whipper.
|
||||
#
|
||||
# whipper 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.
|
||||
#
|
||||
# whipper 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 whipper. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from whipper.command.basecommand import BaseCommand
|
||||
from whipper.common import cache, task
|
||||
from whipper.result import result
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class RCCue(BaseCommand):
|
||||
summary = "write a cue file for the cached result"
|
||||
description = summary
|
||||
|
||||
def do(self, args):
|
||||
self._cache = cache.ResultCache()
|
||||
|
||||
try:
|
||||
discid = args[0]
|
||||
except IndexError:
|
||||
sys.stderr.write(
|
||||
'Please specify a cddb disc id\n')
|
||||
return 3
|
||||
|
||||
persisted = self._cache.getRipResult(discid, create=False)
|
||||
|
||||
if not persisted:
|
||||
sys.stderr.write(
|
||||
'Could not find a result for cddb disc id %s\n' % discid)
|
||||
return 3
|
||||
|
||||
sys.stdout.write(persisted.object.table.cue().encode('utf-8'))
|
||||
|
||||
|
||||
class RCList(BaseCommand):
|
||||
summary = "list cached results"
|
||||
description = summary
|
||||
|
||||
def do(self, args):
|
||||
self._cache = cache.ResultCache()
|
||||
results = []
|
||||
|
||||
for i in self._cache.getIds():
|
||||
r = self._cache.getRipResult(i, create=False)
|
||||
results.append((r.object.artist, r.object.title, i))
|
||||
|
||||
results.sort()
|
||||
|
||||
for artist, title, cddbid in results:
|
||||
if artist is None:
|
||||
artist = '(None)'
|
||||
if title is None:
|
||||
title = '(None)'
|
||||
|
||||
sys.stdout.write('%s: %s - %s\n' % (
|
||||
cddbid, artist.encode('utf-8'), title.encode('utf-8')))
|
||||
|
||||
|
||||
class RCLog(BaseCommand):
|
||||
summary = "write a log file for the cached result"
|
||||
description = summary
|
||||
formatter_class = argparse.ArgumentDefaultsHelpFormatter
|
||||
|
||||
def add_arguments(self):
|
||||
loggers = result.getLoggers().keys()
|
||||
|
||||
self.parser.add_argument(
|
||||
'-L', '--logger',
|
||||
action="store", dest="logger",
|
||||
default='morituri',
|
||||
help="logger to use (choose from '" + "', '".join(loggers) + "')"
|
||||
)
|
||||
|
||||
def do(self, args):
|
||||
self._cache = cache.ResultCache()
|
||||
|
||||
persisted = self._cache.getRipResult(args[0], create=False)
|
||||
|
||||
if not persisted:
|
||||
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:
|
||||
sys.stderr.write("No logger named %s found!\n" % (
|
||||
self.options.logger))
|
||||
return 3
|
||||
|
||||
logger = klazz()
|
||||
sys.stdout.write(logger.log(persisted.object).encode('utf-8'))
|
||||
|
||||
|
||||
class ResultCache(BaseCommand):
|
||||
summary = "debug result cache"
|
||||
description = summary
|
||||
|
||||
subcommands = {
|
||||
'cue': RCCue,
|
||||
'list': RCList,
|
||||
'log': RCLog,
|
||||
}
|
||||
|
||||
|
||||
class Checksum(BaseCommand):
|
||||
summary = "run a checksum task"
|
||||
description = summary
|
||||
|
||||
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 whipper.common import checksum
|
||||
|
||||
for f in self.options.files:
|
||||
fromPath = unicode(f)
|
||||
checksumtask = checksum.CRC32Task(fromPath)
|
||||
runner.run(checksumtask)
|
||||
sys.stdout.write('Checksum: %08x\n' % checksumtask.checksum)
|
||||
|
||||
|
||||
class Encode(BaseCommand):
|
||||
summary = "run an encode task"
|
||||
description = summary
|
||||
|
||||
def add_arguments(self):
|
||||
# here to avoid import gst eating our options
|
||||
from whipper.common import encode
|
||||
|
||||
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):
|
||||
from whipper.common import encode
|
||||
|
||||
try:
|
||||
fromPath = unicode(self.options.input)
|
||||
except IndexError:
|
||||
# unexercised after BaseCommand
|
||||
sys.stdout.write('Please specify an input file.\n')
|
||||
return 3
|
||||
|
||||
try:
|
||||
toPath = unicode(self.options.output)
|
||||
except IndexError:
|
||||
toPath = fromPath + '.flac'
|
||||
|
||||
runner = task.SyncRunner()
|
||||
|
||||
logger.debug('Encoding %s to %s',
|
||||
fromPath.encode('utf-8'),
|
||||
toPath.encode('utf-8'))
|
||||
encodetask = encode.FlacEncodeTask(fromPath, toPath)
|
||||
|
||||
runner.run(encodetask)
|
||||
|
||||
# I think we want this to be
|
||||
# fromPath, not toPath, since the sox peak task, afaik, works on wave
|
||||
# files
|
||||
peaktask = encode.SoxPeakTask(fromPath)
|
||||
runner.run(peaktask)
|
||||
|
||||
sys.stdout.write('Peak level: %r\n' % peaktask.peak)
|
||||
sys.stdout.write('Encoded to %s\n' % toPath.encode('utf-8'))
|
||||
|
||||
|
||||
class Tag(BaseCommand):
|
||||
summary = "run a tag reading task"
|
||||
description = summary
|
||||
|
||||
def add_arguments(self):
|
||||
self.parser.add_argument('file', action='store',
|
||||
help="audio file to tag")
|
||||
|
||||
def do(self):
|
||||
try:
|
||||
path = unicode(self.options.file)
|
||||
except IndexError:
|
||||
sys.stdout.write('Please specify an input file.\n')
|
||||
return 3
|
||||
|
||||
runner = task.SyncRunner()
|
||||
|
||||
from whipper.common import encode
|
||||
logger.debug('Reading tags from %s' % path.encode('utf-8'))
|
||||
tagtask = encode.TagReadTask(path)
|
||||
|
||||
runner.run(tagtask)
|
||||
|
||||
for key in tagtask.taglist.keys():
|
||||
sys.stdout.write('%s: %r\n' % (key, tagtask.taglist[key]))
|
||||
|
||||
|
||||
class MusicBrainzNGS(BaseCommand):
|
||||
summary = "examine MusicBrainz NGS info"
|
||||
description = """Look up a MusicBrainz disc id and output information.
|
||||
|
||||
You can get the MusicBrainz disc id with whipper cd info.
|
||||
|
||||
Example disc id: KnpGsLhvH.lPrNc1PBL21lb9Bg4-"""
|
||||
|
||||
def add_arguments(self):
|
||||
self.parser.add_argument('mbdiscid', action='store',
|
||||
help="MB disc id to look up")
|
||||
|
||||
def do(self):
|
||||
try:
|
||||
discId = unicode(self.options.mbdiscid)
|
||||
except IndexError:
|
||||
sys.stdout.write('Please specify a MusicBrainz disc id.\n')
|
||||
return 3
|
||||
|
||||
from whipper.common import mbngs
|
||||
metadatas = mbngs.musicbrainz(discId, record=self.options.record)
|
||||
|
||||
sys.stdout.write('%d releases\n' % len(metadatas))
|
||||
for i, md in enumerate(metadatas):
|
||||
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:
|
||||
sys.stdout.write(' Cat no: %s\n' % md.catalogNumber)
|
||||
if md.barcode:
|
||||
sys.stdout.write(' Barcode: %s\n' % md.barcode)
|
||||
|
||||
for j, track in enumerate(md.tracks):
|
||||
sys.stdout.write(' Track %2d: %s - %s\n' % (
|
||||
j + 1, track.artist.encode('utf-8'),
|
||||
track.title.encode('utf-8')))
|
||||
|
||||
|
||||
class CDParanoia(BaseCommand):
|
||||
summary = "show cdparanoia version"
|
||||
description = summary
|
||||
|
||||
def do(self):
|
||||
from whipper.program import cdparanoia
|
||||
version = cdparanoia.getCdParanoiaVersion()
|
||||
sys.stdout.write("cdparanoia version: %s\n" % version)
|
||||
|
||||
|
||||
class CDRDAO(BaseCommand):
|
||||
summary = "show cdrdao version"
|
||||
description = summary
|
||||
|
||||
def do(self):
|
||||
from whipper.program import cdrdao
|
||||
version = cdrdao.getCDRDAOVersion()
|
||||
sys.stdout.write("cdrdao version: %s\n" % version)
|
||||
|
||||
|
||||
class Version(BaseCommand):
|
||||
summary = "debug version getting"
|
||||
description = summary
|
||||
|
||||
subcommands = {
|
||||
'cdparanoia': CDParanoia,
|
||||
'cdrdao': CDRDAO,
|
||||
}
|
||||
|
||||
|
||||
class Debug(BaseCommand):
|
||||
summary = "debug internals"
|
||||
description = "debug internals"
|
||||
|
||||
subcommands = {
|
||||
'checksum': Checksum,
|
||||
'encode': Encode,
|
||||
'tag': Tag,
|
||||
'musicbrainzngs': MusicBrainzNGS,
|
||||
'resultcache': ResultCache,
|
||||
'version': Version,
|
||||
}
|
||||
124
whipper/command/drive.py
Normal file
124
whipper/command/drive.py
Normal file
@@ -0,0 +1,124 @@
|
||||
# -*- 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 whipper.
|
||||
#
|
||||
# whipper 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.
|
||||
#
|
||||
# whipper 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 whipper. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
|
||||
from whipper.command.basecommand import BaseCommand
|
||||
from whipper.common import config, drive
|
||||
from whipper.extern.task import task
|
||||
from whipper.program import cdparanoia
|
||||
|
||||
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 do(self):
|
||||
runner = task.SyncRunner()
|
||||
t = cdparanoia.AnalyzeTask(self.options.device)
|
||||
runner.run(t)
|
||||
|
||||
if t.defeatsCache is None:
|
||||
sys.stdout.write(
|
||||
'Cannot analyze the drive. Is there a CD in it?\n')
|
||||
return
|
||||
if not t.defeatsCache:
|
||||
sys.stdout.write(
|
||||
'cdparanoia cannot defeat the audio cache on this drive.\n')
|
||||
else:
|
||||
sys.stdout.write(
|
||||
'cdparanoia can defeat the audio cache on this drive.\n')
|
||||
|
||||
info = drive.getDeviceInfo(self.options.device)
|
||||
if not info:
|
||||
sys.stdout.write('Drive caching behaviour not saved: could not get device info (requires pycdio).\n')
|
||||
return
|
||||
|
||||
sys.stdout.write(
|
||||
'Adding drive cache behaviour to configuration file.\n')
|
||||
|
||||
config.Config().setDefeatsCache(info[0], info[1], info[2],
|
||||
t.defeatsCache)
|
||||
|
||||
|
||||
class List(BaseCommand):
|
||||
summary = "list drives"
|
||||
description = """list available CD-DA drives"""
|
||||
|
||||
def do(self):
|
||||
paths = drive.getAllDevicePaths()
|
||||
self.config = config.Config()
|
||||
|
||||
if not paths:
|
||||
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:
|
||||
sys.stdout.write(
|
||||
'Install pycdio for vendor/model/release detection.\n')
|
||||
return
|
||||
|
||||
for path in paths:
|
||||
vendor, model, release = drive.getDeviceInfo(path)
|
||||
sys.stdout.write(
|
||||
"drive: %s, vendor: %s, model: %s, release: %s\n" % (
|
||||
path, vendor, model, release))
|
||||
|
||||
try:
|
||||
offset = self.config.getReadOffset(
|
||||
vendor, model, release)
|
||||
sys.stdout.write(
|
||||
" Configured read offset: %d\n" % offset)
|
||||
except KeyError:
|
||||
sys.stdout.write(
|
||||
" No read offset found. Run 'whipper offset find'\n")
|
||||
|
||||
try:
|
||||
defeats = self.config.getDefeatsCache(
|
||||
vendor, model, release)
|
||||
sys.stdout.write(
|
||||
" Can defeat audio cache: %s\n" % defeats)
|
||||
except KeyError:
|
||||
sys.stdout.write(
|
||||
" Unknown whether audio cache can be defeated. "
|
||||
"Run 'whipper drive analyze'\n")
|
||||
|
||||
|
||||
if not paths:
|
||||
sys.stdout.write('No drives found.\n')
|
||||
|
||||
|
||||
class Drive(BaseCommand):
|
||||
summary = "handle drives"
|
||||
description = """Drive utilities."""
|
||||
subcommands = {
|
||||
'analyze': Analyze,
|
||||
'list': List
|
||||
}
|
||||
155
whipper/command/image.py
Normal file
155
whipper/command/image.py
Normal file
@@ -0,0 +1,155 @@
|
||||
# -*- 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 whipper.
|
||||
#
|
||||
# whipper 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.
|
||||
#
|
||||
# whipper 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 whipper. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from whipper.command.basecommand import BaseCommand
|
||||
from whipper.common import accurip, config, program
|
||||
from whipper.common import encode
|
||||
from whipper.extern.task import task
|
||||
from whipper.image import image
|
||||
from whipper.result import result
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Retag(BaseCommand):
|
||||
summary = "retag image files"
|
||||
description = """
|
||||
Retags the image from the given .cue files with tags obtained from MusicBrainz.
|
||||
"""
|
||||
|
||||
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_argument(
|
||||
'-p', '--prompt',
|
||||
action="store_true", dest="prompt",
|
||||
help="Prompt if there are multiple matching releases"
|
||||
)
|
||||
self.parser.add_argument(
|
||||
'-c', '--country',
|
||||
action="store", dest="country",
|
||||
help="Filter releases by country"
|
||||
)
|
||||
|
||||
def do(self):
|
||||
|
||||
prog = program.Program(config.Config(), stdout=sys.stdout)
|
||||
runner = task.SyncRunner()
|
||||
|
||||
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()
|
||||
sys.stdout.write('MusicBrainz disc id is %s\n' % mbdiscid)
|
||||
|
||||
sys.stdout.write("MusicBrainz lookup URL %s\n" %
|
||||
cueImage.table.getMusicBrainzSubmitURL())
|
||||
prog.metadata = prog.getMusicBrainz(cueImage.table, mbdiscid,
|
||||
release=self.options.release_id,
|
||||
country=self.options.country,
|
||||
prompt=self.options.prompt)
|
||||
|
||||
if not prog.metadata:
|
||||
print 'Not in MusicBrainz database, skipping'
|
||||
continue
|
||||
|
||||
prog.metadata.discid = mbdiscid
|
||||
|
||||
# FIXME: this feels like we're poking at internals.
|
||||
prog.cuePath = arg
|
||||
prog.result = result.RipResult()
|
||||
for track in cueImage.table.tracks:
|
||||
path = cueImage.getRealPath(track.indexes[1].path)
|
||||
|
||||
taglist = prog.getTagList(track.number)
|
||||
logger.debug(
|
||||
'possibly retagging %r from cue path %r with taglist %r',
|
||||
path, arg, taglist)
|
||||
t = encode.SafeRetagTask(path, taglist)
|
||||
runner.run(t)
|
||||
path = os.path.basename(path)
|
||||
if t.changed:
|
||||
print 'Retagged %s' % path
|
||||
else:
|
||||
print '%s already tagged correctly' % path
|
||||
print
|
||||
|
||||
|
||||
class Verify(BaseCommand):
|
||||
summary = "verify image"
|
||||
description = """
|
||||
Verifies the image from the given .cue files against the AccurateRip database.
|
||||
"""
|
||||
|
||||
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 self.options.cuefile:
|
||||
arg = arg.decode('utf-8')
|
||||
cueImage = image.Image(arg)
|
||||
cueImage.setup(runner)
|
||||
|
||||
url = cueImage.table.getAccurateRipURL()
|
||||
responses = cache.retrieve(url)
|
||||
|
||||
# FIXME: this feels like we're poking at internals.
|
||||
prog.cuePath = arg
|
||||
prog.result = result.RipResult()
|
||||
for track in cueImage.table.tracks:
|
||||
tr = result.TrackResult()
|
||||
tr.number = track.number
|
||||
prog.result.tracks.append(tr)
|
||||
|
||||
prog.verifyImage(runner, responses)
|
||||
|
||||
print "\n".join(prog.getAccurateRipResults()) + "\n"
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
subcommands = {
|
||||
'verify': Verify,
|
||||
'retag': Retag
|
||||
}
|
||||
96
whipper/command/main.py
Normal file
96
whipper/command/main.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# -*- Mode: Python -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
import sys
|
||||
import pkg_resources
|
||||
import musicbrainzngs
|
||||
|
||||
import whipper
|
||||
|
||||
from whipper.command import cd, offset, drive, image, accurip, debug
|
||||
from whipper.command.basecommand import BaseCommand
|
||||
from whipper.common import common, directory
|
||||
from whipper.extern.task import task
|
||||
from whipper.program.utils import eject_device
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def main():
|
||||
# set user agent
|
||||
musicbrainzngs.set_useragent("whipper", whipper.__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:
|
||||
cmd = Whipper(sys.argv[1:], os.path.basename(sys.argv[0]), None)
|
||||
ret = cmd.do()
|
||||
except SystemError, e:
|
||||
sys.stderr.write('whipper: error: %s\n' % e)
|
||||
if (type(e) is common.EjectError and
|
||||
cmd.options.eject in ('failure', 'always')):
|
||||
eject_device(e.device)
|
||||
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")
|
||||
self.parser.add_argument('-e', '--eject',
|
||||
action="store", dest="eject", default="always",
|
||||
choices=('never', 'failure', 'success', 'always'),
|
||||
help="when to eject disc (default: always)")
|
||||
|
||||
def handle_arguments(self):
|
||||
if self.options.help:
|
||||
self.parser.print_help()
|
||||
sys.exit(0)
|
||||
if self.options.version:
|
||||
print "whipper %s" % whipper.__version__
|
||||
sys.exit(0)
|
||||
243
whipper/command/offset.py
Normal file
243
whipper/command/offset.py
Normal file
@@ -0,0 +1,243 @@
|
||||
# -*- 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 whipper.
|
||||
#
|
||||
# whipper 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.
|
||||
#
|
||||
# whipper 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 whipper. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import gobject
|
||||
gobject.threads_init()
|
||||
|
||||
from whipper.command.basecommand import BaseCommand
|
||||
from whipper.common import accurip, common, config, drive, program
|
||||
from whipper.common import task as ctask
|
||||
from whipper.program import cdrdao, cdparanoia, utils
|
||||
from whipper.common import checksum
|
||||
|
||||
from whipper.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, " + \
|
||||
"+98, +116, +96, +733, +120, +691, +685, +97, +600, " + \
|
||||
"+690, +1292, +99, +676, +686, +1182, -24, +704, +572, " + \
|
||||
"+688, +91, +696, +103, -491, +689, +145, +708, +697, " + \
|
||||
"+564, +86, +679, +355, -496, -1164, +1160, +694, 0, " + \
|
||||
"-436, +79, +94, +684, +681, +106, +692, +943, +1194, " + \
|
||||
"+92, +117, +680, +682, +1268, +678, -582, +1473, +1279, " + \
|
||||
"-54, +1508, +740, +1272, +534, +976, +687, +675, +1303, " + \
|
||||
"+674, +1263, +108, +974, +122, +111, -489, +772, +732, " + \
|
||||
"-495, -494, +975, +935, +87, +668, +1776, +1364, +1336, " + \
|
||||
"+1127"
|
||||
|
||||
|
||||
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 handle_arguments(self):
|
||||
self._offsets = []
|
||||
blocks = self.options.offsets.split(',')
|
||||
for b in blocks:
|
||||
if ':' in b:
|
||||
a, b = b.split(':')
|
||||
self._offsets.extend(range(int(a), int(b) + 1))
|
||||
else:
|
||||
self._offsets.append(int(b))
|
||||
|
||||
logger.debug('Trying with offsets %r', self._offsets)
|
||||
|
||||
def do(self):
|
||||
runner = ctask.SyncRunner()
|
||||
|
||||
device = self.options.device
|
||||
|
||||
# if necessary, load and unmount
|
||||
sys.stdout.write('Checking device %s\n' % device)
|
||||
|
||||
utils.load_device(device)
|
||||
utils.unmount_device(device)
|
||||
|
||||
# first get the Table Of Contents of the CD
|
||||
t = cdrdao.ReadTOCTask(device)
|
||||
table = t.table
|
||||
|
||||
logger.debug("CDDB disc id: %r", table.getCDDBDiscId())
|
||||
url = table.getAccurateRipURL()
|
||||
logger.debug("AccurateRip URL: %s", url)
|
||||
|
||||
# FIXME: download url as a task too
|
||||
responses = []
|
||||
import urllib2
|
||||
try:
|
||||
handle = urllib2.urlopen(url)
|
||||
data = handle.read()
|
||||
responses = accurip.getAccurateRipResponses(data)
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
sys.stdout.write(
|
||||
'Album not found in AccurateRip database.\n')
|
||||
return 1
|
||||
else:
|
||||
raise
|
||||
|
||||
if responses:
|
||||
logger.debug('%d AccurateRip responses found.' % len(responses))
|
||||
|
||||
if responses[0].cddbDiscId != table.getCDDBDiscId():
|
||||
logger.warning("AccurateRip response discid different: %s",
|
||||
responses[0].cddbDiscId)
|
||||
|
||||
# now rip the first track at various offsets, calculating AccurateRip
|
||||
# CRC, and matching it against the retrieved ones
|
||||
|
||||
def match(archecksum, track, responses):
|
||||
for i, r in enumerate(responses):
|
||||
if archecksum == r.checksums[track - 1]:
|
||||
return archecksum, i
|
||||
|
||||
return None, None
|
||||
|
||||
for offset in self._offsets:
|
||||
sys.stdout.write('Trying read offset %d ...\n' % offset)
|
||||
try:
|
||||
archecksum = self._arcs(runner, table, 1, offset)
|
||||
except task.TaskException, e:
|
||||
|
||||
# let MissingDependency fall through
|
||||
if isinstance(e.exception,
|
||||
common.MissingDependencyException):
|
||||
raise e
|
||||
|
||||
if isinstance(e.exception, cdparanoia.FileSizeError):
|
||||
sys.stdout.write(
|
||||
'WARNING: cannot rip with offset %d...\n' % offset)
|
||||
continue
|
||||
|
||||
logger.warning("Unknown task exception for offset %d: %r" % (
|
||||
offset, e))
|
||||
sys.stdout.write(
|
||||
'WARNING: cannot rip with offset %d...\n' % offset)
|
||||
continue
|
||||
|
||||
logger.debug('AR checksum calculated: %s' % archecksum)
|
||||
|
||||
c, i = match(archecksum, 1, responses)
|
||||
if c:
|
||||
count = 1
|
||||
logger.debug('MATCHED against response %d' % i)
|
||||
sys.stdout.write(
|
||||
'Offset of device is likely %d, confirming ...\n' %
|
||||
offset)
|
||||
|
||||
# now try and rip all other tracks as well, except for the
|
||||
# last one (to avoid readers that can't do overread
|
||||
for track in range(2, (len(table.tracks) + 1) - 1):
|
||||
try:
|
||||
archecksum = self._arcs(runner, table, track, offset)
|
||||
except task.TaskException, e:
|
||||
if isinstance(e.exception, cdparanoia.FileSizeError):
|
||||
sys.stdout.write(
|
||||
'WARNING: cannot rip with offset %d...\n' %
|
||||
offset)
|
||||
continue
|
||||
|
||||
c, i = match(archecksum, track, responses)
|
||||
if c:
|
||||
logger.debug('MATCHED track %d against response %d' % (
|
||||
track, i))
|
||||
count += 1
|
||||
|
||||
if count == len(table.tracks) - 1:
|
||||
self._foundOffset(device, offset)
|
||||
return 0
|
||||
else:
|
||||
sys.stdout.write(
|
||||
'Only %d of %d tracks matched, continuing ...\n' % (
|
||||
count, len(table.tracks)))
|
||||
|
||||
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
|
||||
logger.debug('Ripping track %r with offset %d ...', track, offset)
|
||||
|
||||
fd, path = tempfile.mkstemp(
|
||||
suffix=u'.track%02d.offset%d.morituri.wav' % (
|
||||
track, offset))
|
||||
os.close(fd)
|
||||
|
||||
t = cdparanoia.ReadTrackTask(path, table,
|
||||
table.getTrackStart(track), table.getTrackEnd(track),
|
||||
overread=False, offset=offset, device=self.options.device)
|
||||
t.description = 'Ripping track %d with read offset %d' % (
|
||||
track, offset)
|
||||
runner.run(t)
|
||||
|
||||
|
||||
# TODO MW: Update this to also use the v2 checksum(s)
|
||||
t = checksum.FastAccurateRipChecksumTask(path, trackNumber=track,
|
||||
trackCount=len(table.tracks), wave=True, v2=False)
|
||||
runner.run(t)
|
||||
|
||||
os.unlink(path)
|
||||
return "%08x" % t.checksum
|
||||
|
||||
def _foundOffset(self, device, offset):
|
||||
sys.stdout.write('\nRead offset of device is: %d.\n' %
|
||||
offset)
|
||||
|
||||
info = drive.getDeviceInfo(device)
|
||||
if not info:
|
||||
sys.stdout.write('Offset not saved: could not get device info (requires pycdio).\n')
|
||||
return
|
||||
|
||||
sys.stdout.write('Adding read offset to configuration file.\n')
|
||||
|
||||
config.Config().setReadOffset(info[0], info[1], info[2],
|
||||
offset)
|
||||
|
||||
|
||||
class Offset(BaseCommand):
|
||||
summary = "handle drive offsets"
|
||||
description = """
|
||||
Drive offset detection utility.
|
||||
"""
|
||||
subcommands = {
|
||||
'find': Find,
|
||||
}
|
||||
Reference in New Issue
Block a user