Rename "morituri" module to "whipper".
Fixes https://github.com/JoeLametta/whipper/issues/100
This commit is contained in:
14
whipper/__init__.py
Normal file
14
whipper/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
__version__ = '0.5.1'
|
||||
|
||||
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)
|
||||
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,
|
||||
}
|
||||
0
whipper/common/__init__.py
Normal file
0
whipper/common/__init__.py
Normal file
147
whipper/common/accurip.py
Normal file
147
whipper/common/accurip.py
Normal file
@@ -0,0 +1,147 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_accurip -*-
|
||||
# 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 errno
|
||||
import os
|
||||
import struct
|
||||
import urlparse
|
||||
import urllib2
|
||||
|
||||
from whipper.common import directory
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_CACHE_DIR = directory.cache_path()
|
||||
|
||||
|
||||
class AccuCache:
|
||||
|
||||
def __init__(self):
|
||||
if not os.path.exists(_CACHE_DIR):
|
||||
logger.debug('Creating cache directory %s', _CACHE_DIR)
|
||||
os.makedirs(_CACHE_DIR)
|
||||
|
||||
def _getPath(self, url):
|
||||
# split path starts with /
|
||||
return os.path.join(_CACHE_DIR, urlparse.urlparse(url)[2][1:])
|
||||
|
||||
def retrieve(self, url, force=False):
|
||||
logger.debug("Retrieving AccurateRip URL %s", url)
|
||||
path = self._getPath(url)
|
||||
logger.debug("Cached path: %s", path)
|
||||
if force:
|
||||
logger.debug("forced to download")
|
||||
self.download(url)
|
||||
elif not os.path.exists(path):
|
||||
logger.debug("%s does not exist, downloading", path)
|
||||
self.download(url)
|
||||
|
||||
if not os.path.exists(path):
|
||||
logger.debug("%s does not exist, not in database", path)
|
||||
return None
|
||||
|
||||
data = self._read(url)
|
||||
|
||||
return getAccurateRipResponses(data)
|
||||
|
||||
def download(self, url):
|
||||
# FIXME: download url as a task too
|
||||
try:
|
||||
handle = urllib2.urlopen(url)
|
||||
data = handle.read()
|
||||
|
||||
except urllib2.HTTPError, e:
|
||||
if e.code == 404:
|
||||
return None
|
||||
else:
|
||||
raise
|
||||
|
||||
self._cache(url, data)
|
||||
return data
|
||||
|
||||
def _cache(self, url, data):
|
||||
path = self._getPath(url)
|
||||
try:
|
||||
os.makedirs(os.path.dirname(path))
|
||||
except OSError, e:
|
||||
logger.debug('Could not make dir %s: %r' % (
|
||||
path, str(e)))
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
|
||||
handle = open(path, 'wb')
|
||||
handle.write(data)
|
||||
handle.close()
|
||||
|
||||
def _read(self, url):
|
||||
logger.debug("Reading %s from cache", url)
|
||||
path = self._getPath(url)
|
||||
handle = open(path, 'rb')
|
||||
data = handle.read()
|
||||
handle.close()
|
||||
return data
|
||||
|
||||
|
||||
def getAccurateRipResponses(data):
|
||||
ret = []
|
||||
|
||||
while data:
|
||||
trackCount = struct.unpack("B", data[0])[0]
|
||||
nbytes = 1 + 12 + trackCount * (1 + 8)
|
||||
|
||||
ret.append(AccurateRipResponse(data[:nbytes]))
|
||||
data = data[nbytes:]
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
class AccurateRipResponse(object):
|
||||
"""
|
||||
I represent the response of the AccurateRip online database.
|
||||
|
||||
@type checksums: list of str
|
||||
"""
|
||||
|
||||
trackCount = None
|
||||
discId1 = ""
|
||||
discId2 = ""
|
||||
cddbDiscId = ""
|
||||
confidences = None
|
||||
checksums = None
|
||||
|
||||
def __init__(self, data):
|
||||
self.trackCount = struct.unpack("B", data[0])[0]
|
||||
self.discId1 = "%08x" % struct.unpack("<L", data[1:5])[0]
|
||||
self.discId2 = "%08x" % struct.unpack("<L", data[5:9])[0]
|
||||
self.cddbDiscId = "%08x" % struct.unpack("<L", data[9:13])[0]
|
||||
|
||||
self.confidences = []
|
||||
self.checksums = []
|
||||
|
||||
pos = 13
|
||||
for _ in range(self.trackCount):
|
||||
confidence = struct.unpack("B", data[pos])[0]
|
||||
checksum = "%08x" % struct.unpack("<L", data[pos + 1:pos + 5])[0]
|
||||
pos += 9
|
||||
self.confidences.append(confidence)
|
||||
self.checksums.append(checksum)
|
||||
229
whipper/common/cache.py
Normal file
229
whipper/common/cache.py
Normal file
@@ -0,0 +1,229 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_cache -*-
|
||||
# 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 os.path
|
||||
import glob
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
from whipper.result import result
|
||||
from whipper.common import directory
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Persister:
|
||||
"""
|
||||
I wrap an optional pickle to persist an object to disk.
|
||||
|
||||
Instantiate me with a path to automatically unpickle the object.
|
||||
Call persist to store the object to disk; it will get stored if it
|
||||
changed from the on-disk object.
|
||||
|
||||
@ivar object: the persistent object
|
||||
"""
|
||||
|
||||
def __init__(self, path=None, default=None):
|
||||
"""
|
||||
If path is not given, the object will not be persisted.
|
||||
This allows code to transparently deal with both persisted and
|
||||
non-persisted objects, since the persist method will just end up
|
||||
doing nothing.
|
||||
"""
|
||||
self._path = path
|
||||
self.object = None
|
||||
|
||||
self._unpickle(default)
|
||||
|
||||
def persist(self, obj=None):
|
||||
"""
|
||||
Persist the given object, if we have a persistence path and the
|
||||
object changed.
|
||||
|
||||
If object is not given, re-persist our object, always.
|
||||
If object is given, only persist if it was changed.
|
||||
"""
|
||||
# don't pickle if it's already ok
|
||||
if obj and obj == self.object:
|
||||
return
|
||||
|
||||
# store the object on ourselves if not None
|
||||
if obj is not None:
|
||||
self.object = obj
|
||||
|
||||
# don't pickle if there is no path
|
||||
if not self._path:
|
||||
return
|
||||
|
||||
# default to pickling our object again
|
||||
if obj is None:
|
||||
obj = self.object
|
||||
|
||||
# pickle
|
||||
self.object = obj
|
||||
(fd, path) = tempfile.mkstemp(suffix='.morituri.pickle')
|
||||
handle = os.fdopen(fd, 'wb')
|
||||
import pickle
|
||||
pickle.dump(obj, handle, 2)
|
||||
handle.close()
|
||||
# do an atomic move
|
||||
shutil.move(path, self._path)
|
||||
logger.debug('saved persisted object to %r' % self._path)
|
||||
|
||||
def _unpickle(self, default=None):
|
||||
self.object = default
|
||||
|
||||
if not self._path:
|
||||
return None
|
||||
|
||||
if not os.path.exists(self._path):
|
||||
return None
|
||||
|
||||
handle = open(self._path)
|
||||
import pickle
|
||||
|
||||
try:
|
||||
self.object = pickle.load(handle)
|
||||
logger.debug('loaded persisted object from %r' % self._path)
|
||||
except:
|
||||
# can fail for various reasons; in that case, pretend we didn't
|
||||
# load it
|
||||
pass
|
||||
|
||||
def delete(self):
|
||||
self.object = None
|
||||
os.unlink(self._path)
|
||||
|
||||
|
||||
class PersistedCache:
|
||||
"""
|
||||
I wrap a directory of persisted objects.
|
||||
"""
|
||||
|
||||
path = None
|
||||
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
try:
|
||||
os.makedirs(self.path)
|
||||
except OSError, e:
|
||||
if e.errno != 17: # FIXME
|
||||
raise
|
||||
|
||||
def _getPath(self, key):
|
||||
return os.path.join(self.path, '%s.pickle' % key)
|
||||
|
||||
def get(self, key):
|
||||
"""
|
||||
Returns the persister for the given key.
|
||||
"""
|
||||
persister = Persister(self._getPath(key))
|
||||
if persister.object:
|
||||
if hasattr(persister.object, 'instanceVersion'):
|
||||
o = persister.object
|
||||
if o.instanceVersion < o.__class__.classVersion:
|
||||
logger.debug(
|
||||
'key %r persisted object version %d is outdated',
|
||||
key, o.instanceVersion)
|
||||
persister.object = None
|
||||
# FIXME: don't delete old objects atm
|
||||
# persister.delete()
|
||||
|
||||
return persister
|
||||
|
||||
|
||||
class ResultCache:
|
||||
|
||||
def __init__(self, path=None):
|
||||
self._path = path or directory.cache_path('result')
|
||||
self._pcache = PersistedCache(self._path)
|
||||
|
||||
def getRipResult(self, cddbdiscid, create=True):
|
||||
"""
|
||||
Retrieve the persistable RipResult either from our cache (from a
|
||||
previous, possibly aborted rip), or return a new one.
|
||||
|
||||
@rtype: L{Persistable} for L{result.RipResult}
|
||||
"""
|
||||
presult = self._pcache.get(cddbdiscid)
|
||||
|
||||
if not presult.object:
|
||||
logger.debug('result for cddbdiscid %r not in cache', cddbdiscid)
|
||||
if not create:
|
||||
logger.debug('returning None')
|
||||
return None
|
||||
|
||||
logger.debug('creating result')
|
||||
presult.object = result.RipResult()
|
||||
presult.persist(presult.object)
|
||||
else:
|
||||
logger.debug('result for cddbdiscid %r found in cache, reusing',
|
||||
cddbdiscid)
|
||||
|
||||
return presult
|
||||
|
||||
def getIds(self):
|
||||
paths = glob.glob(os.path.join(self._path, '*.pickle'))
|
||||
|
||||
return [os.path.splitext(os.path.basename(path))[0] for path in paths]
|
||||
|
||||
|
||||
class TableCache:
|
||||
|
||||
"""
|
||||
I read and write entries to and from the cache of tables.
|
||||
|
||||
If no path is specified, the cache will write to the current cache
|
||||
directory and read from all possible cache directories (to allow for
|
||||
pre-0.2.1 cddbdiscid-keyed entries).
|
||||
"""
|
||||
|
||||
def __init__(self, path=None):
|
||||
if not path:
|
||||
self._path = directory.cache_path('table')
|
||||
else:
|
||||
self._path = path
|
||||
|
||||
self._pcache = PersistedCache(self._path)
|
||||
|
||||
def get(self, cddbdiscid, mbdiscid):
|
||||
# Before 0.2.1, we only saved by cddbdiscid, and had collisions
|
||||
# mbdiscid collisions are a lot less likely
|
||||
ptable = self._pcache.get('mbdiscid.' + mbdiscid)
|
||||
|
||||
if not ptable.object:
|
||||
ptable = self._pcache.get(cddbdiscid)
|
||||
if ptable.object:
|
||||
if ptable.object.getMusicBrainzDiscId() != mbdiscid:
|
||||
logger.debug('cached table is for different mb id %r' % (
|
||||
ptable.object.getMusicBrainzDiscId()))
|
||||
ptable.object = None
|
||||
else:
|
||||
logger.debug('no valid cached table found for %r' %
|
||||
cddbdiscid)
|
||||
|
||||
if not ptable.object:
|
||||
# get an empty persistable from the writable location
|
||||
ptable = self._pcache.get('mbdiscid.' + mbdiscid)
|
||||
|
||||
return ptable
|
||||
76
whipper/common/checksum.py
Normal file
76
whipper/common/checksum.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_checksum -*-
|
||||
# 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 binascii
|
||||
import wave
|
||||
|
||||
|
||||
from whipper.extern.task import task as etask
|
||||
|
||||
from whipper.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 CRC32Task(etask.Task):
|
||||
# TODO: Support sampleStart, sampleLength later on (should be trivial, just
|
||||
# add change the read part in _crc32 to skip some samples and/or not
|
||||
# read too far)
|
||||
def __init__(self, path, sampleStart=0, sampleLength=-1):
|
||||
self.path = path
|
||||
|
||||
def start(self, runner):
|
||||
etask.Task.start(self, runner)
|
||||
self.schedule(0.0, self._crc32)
|
||||
|
||||
def _crc32(self):
|
||||
w = wave.open(self.path)
|
||||
d = w._data_chunk.read()
|
||||
|
||||
self.checksum = binascii.crc32(d) & 0xffffffff
|
||||
self.stop()
|
||||
|
||||
|
||||
class FastAccurateRipChecksumTask(etask.Task):
|
||||
description = 'Calculating (Fast) AccurateRip checksum'
|
||||
|
||||
def __init__(self, path, trackNumber, trackCount, wave, v2=False):
|
||||
self.path = path
|
||||
self.trackNumber = trackNumber
|
||||
self.trackCount = trackCount
|
||||
self._wave = wave
|
||||
self._v2 = v2
|
||||
self.checksum = None
|
||||
|
||||
def start(self, runner):
|
||||
etask.Task.start(self, runner)
|
||||
self.schedule(0.0, self._arc)
|
||||
|
||||
def _arc(self):
|
||||
arc = accuraterip_checksum(self.path, self.trackNumber, self.trackCount,
|
||||
self._wave, self._v2)
|
||||
self.checksum = arc
|
||||
|
||||
self.stop()
|
||||
307
whipper/common/common.py
Normal file
307
whipper/common/common.py
Normal file
@@ -0,0 +1,307 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_common -*-
|
||||
# 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 os.path
|
||||
import commands
|
||||
import math
|
||||
import subprocess
|
||||
|
||||
from whipper.extern import asyncsub
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
FRAMES_PER_SECOND = 75
|
||||
|
||||
SAMPLES_PER_FRAME = 588 # a sample is 2 16-bit values, left and right channel
|
||||
WORDS_PER_FRAME = SAMPLES_PER_FRAME * 2
|
||||
BYTES_PER_FRAME = SAMPLES_PER_FRAME * 4
|
||||
|
||||
|
||||
class EjectError(SystemError):
|
||||
"""
|
||||
Possibly ejects the drive in command.main.
|
||||
"""
|
||||
def __init__(self, device, *args):
|
||||
"""
|
||||
args is a tuple used by BaseException.__str__
|
||||
device is the device path to eject
|
||||
"""
|
||||
self.args = args
|
||||
self.device = device
|
||||
|
||||
|
||||
def msfToFrames(msf):
|
||||
"""
|
||||
Converts a string value in MM:SS:FF to frames.
|
||||
|
||||
@param msf: the MM:SS:FF value to convert
|
||||
@type msf: str
|
||||
|
||||
@rtype: int
|
||||
@returns: number of frames
|
||||
"""
|
||||
if not ':' in msf:
|
||||
return int(msf)
|
||||
|
||||
m, s, f = msf.split(':')
|
||||
|
||||
return 60 * FRAMES_PER_SECOND * int(m) \
|
||||
+ FRAMES_PER_SECOND * int(s) \
|
||||
+ int(f)
|
||||
|
||||
|
||||
def framesToMSF(frames, frameDelimiter=':'):
|
||||
f = frames % FRAMES_PER_SECOND
|
||||
frames -= f
|
||||
s = (frames / FRAMES_PER_SECOND) % 60
|
||||
frames -= s * 60
|
||||
m = frames / FRAMES_PER_SECOND / 60
|
||||
|
||||
return "%02d:%02d%s%02d" % (m, s, frameDelimiter, f)
|
||||
|
||||
|
||||
def framesToHMSF(frames):
|
||||
# cdparanoia style
|
||||
f = frames % FRAMES_PER_SECOND
|
||||
frames -= f
|
||||
s = (frames / FRAMES_PER_SECOND) % 60
|
||||
frames -= s * FRAMES_PER_SECOND
|
||||
m = (frames / FRAMES_PER_SECOND / 60) % 60
|
||||
frames -= m * FRAMES_PER_SECOND * 60
|
||||
h = frames / FRAMES_PER_SECOND / 60 / 60
|
||||
|
||||
return "%02d:%02d:%02d.%02d" % (h, m, s, f)
|
||||
|
||||
|
||||
def formatTime(seconds, fractional=3):
|
||||
"""
|
||||
Nicely format time in a human-readable format, like
|
||||
HH:MM:SS.mmm
|
||||
|
||||
If fractional is zero, no seconds will be shown.
|
||||
If it is greater than 0, we will show seconds and fractions of seconds.
|
||||
As a side consequence, there is no way to show seconds without fractions.
|
||||
|
||||
@param seconds: the time in seconds to format.
|
||||
@type seconds: int or float
|
||||
@param fractional: how many digits to show for the fractional part of
|
||||
seconds.
|
||||
@type fractional: int
|
||||
|
||||
@rtype: string
|
||||
@returns: a nicely formatted time string.
|
||||
"""
|
||||
chunks = []
|
||||
|
||||
if seconds < 0:
|
||||
chunks.append(('-'))
|
||||
seconds = -seconds
|
||||
|
||||
hour = 60 * 60
|
||||
hours = seconds / hour
|
||||
seconds %= hour
|
||||
|
||||
minute = 60
|
||||
minutes = seconds / minute
|
||||
seconds %= minute
|
||||
|
||||
chunk = '%02d:%02d' % (hours, minutes)
|
||||
if fractional > 0:
|
||||
chunk += ':%0*.*f' % (fractional + 3, fractional, seconds)
|
||||
|
||||
chunks.append(chunk)
|
||||
|
||||
return " ".join(chunks)
|
||||
|
||||
class MissingDependencyException(Exception):
|
||||
dependency = None
|
||||
|
||||
def __init__(self, *args):
|
||||
self.args = args
|
||||
self.dependency = args[0]
|
||||
|
||||
|
||||
class EmptyError(Exception):
|
||||
pass
|
||||
|
||||
class MissingFrames(Exception):
|
||||
"""
|
||||
Less frames decoded than expected.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def shrinkPath(path):
|
||||
"""
|
||||
Shrink a full path to a shorter version.
|
||||
Used to handle ENAMETOOLONG
|
||||
"""
|
||||
parts = list(os.path.split(path))
|
||||
length = len(parts[-1])
|
||||
target = 127
|
||||
if length <= target:
|
||||
target = pow(2, int(math.log(length, 2))) - 1
|
||||
|
||||
name, ext = os.path.splitext(parts[-1])
|
||||
target -= len(ext) + 1
|
||||
|
||||
# split on space, then reassemble
|
||||
words = name.split(' ')
|
||||
length = 0
|
||||
pieces = []
|
||||
for word in words:
|
||||
if length + 1 + len(word) <= target:
|
||||
pieces.append(word)
|
||||
length += 1 + len(word)
|
||||
else:
|
||||
break
|
||||
|
||||
name = " ".join(pieces)
|
||||
# ext includes period
|
||||
parts[-1] = u'%s%s' % (name, ext)
|
||||
path = os.path.join(*parts)
|
||||
return path
|
||||
|
||||
|
||||
def getRealPath(refPath, filePath):
|
||||
"""
|
||||
Translate a .cue or .toc's FILE argument to an existing path.
|
||||
Does Windows path translation.
|
||||
Will look for the given file name, but with .flac and .wav as extensions.
|
||||
|
||||
@param refPath: path to the file from which the track is referenced;
|
||||
for example, path to the .cue file in the same directory
|
||||
@type refPath: unicode
|
||||
|
||||
@type filePath: unicode
|
||||
"""
|
||||
assert type(filePath) is unicode, "%r is not unicode" % filePath
|
||||
|
||||
if os.path.exists(filePath):
|
||||
return filePath
|
||||
|
||||
candidatePaths = []
|
||||
|
||||
# .cue FILE statements can have Windows-style path separators, so convert
|
||||
# them as one possible candidate
|
||||
# on the other hand, the file may indeed contain a backslash in the name
|
||||
# on linux
|
||||
# FIXME: I guess we might do all possible combinations of splitting or
|
||||
# keeping the slash, but let's just assume it's either Windows
|
||||
# or linux
|
||||
# See https://thomas.apestaart.org/morituri/trac/ticket/107
|
||||
parts = filePath.split('\\')
|
||||
if parts[0] == '':
|
||||
parts[0] = os.path.sep
|
||||
tpath = os.path.join(*parts)
|
||||
|
||||
for path in [filePath, tpath]:
|
||||
if path == os.path.abspath(path):
|
||||
candidatePaths.append(path)
|
||||
else:
|
||||
# if the path is relative:
|
||||
# - check relatively to the cue file
|
||||
# - check only the filename part relative to the cue file
|
||||
candidatePaths.append(os.path.join(
|
||||
os.path.dirname(refPath), path))
|
||||
candidatePaths.append(os.path.join(
|
||||
os.path.dirname(refPath), os.path.basename(path)))
|
||||
|
||||
# Now look for .wav and .flac files, as .flac files are often named .wav
|
||||
for candidate in candidatePaths:
|
||||
noext, _ = os.path.splitext(candidate)
|
||||
for ext in ['wav', 'flac']:
|
||||
cpath = '%s.%s' % (noext, ext)
|
||||
if os.path.exists(cpath):
|
||||
return cpath
|
||||
|
||||
raise KeyError("Cannot find file for %r" % filePath)
|
||||
|
||||
|
||||
def getRelativePath(targetPath, collectionPath):
|
||||
"""
|
||||
Get a relative path from the directory of collectionPath to
|
||||
targetPath.
|
||||
|
||||
Used to determine the path to use in .cue/.m3u files
|
||||
"""
|
||||
logger.debug('getRelativePath: target %r, collection %r' % (
|
||||
targetPath, collectionPath))
|
||||
|
||||
targetDir = os.path.dirname(targetPath)
|
||||
collectionDir = os.path.dirname(collectionPath)
|
||||
if targetDir == collectionDir:
|
||||
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)
|
||||
logger.debug(
|
||||
'getRelativePath: target and collection in different dir, %r' % rel
|
||||
)
|
||||
return os.path.join(rel, os.path.basename(targetPath))
|
||||
|
||||
|
||||
class VersionGetter(object):
|
||||
"""
|
||||
I get the version of a program by looking for it in command output
|
||||
according to a regexp.
|
||||
"""
|
||||
|
||||
def __init__(self, dependency, args, regexp, expander):
|
||||
"""
|
||||
@param dependency: name of the dependency providing the program
|
||||
@param args: the arguments to invoke to show the version
|
||||
@type args: list of str
|
||||
@param regexp: the regular expression to get the version
|
||||
@param expander: the expansion string for the version using the
|
||||
regexp group dict
|
||||
"""
|
||||
|
||||
self._dep = dependency
|
||||
self._args = args
|
||||
self._regexp = regexp
|
||||
self._expander = expander
|
||||
|
||||
def get(self):
|
||||
version = "(Unknown)"
|
||||
|
||||
try:
|
||||
p = asyncsub.Popen(self._args,
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, close_fds=True)
|
||||
p.wait()
|
||||
output = asyncsub.recv_some(p, e=0, stderr=1)
|
||||
vre = self._regexp.search(output)
|
||||
if vre:
|
||||
version = self._expander % vre.groupdict()
|
||||
except OSError, e:
|
||||
import errno
|
||||
if e.errno == errno.ENOENT:
|
||||
raise MissingDependencyException(self._dep)
|
||||
raise
|
||||
|
||||
return version
|
||||
159
whipper/common/config.py
Normal file
159
whipper/common/config.py
Normal file
@@ -0,0 +1,159 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_config -*-
|
||||
# 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 ConfigParser
|
||||
import codecs
|
||||
import os.path
|
||||
import shutil
|
||||
import tempfile
|
||||
import urllib
|
||||
|
||||
from whipper.common import directory
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Config:
|
||||
|
||||
def __init__(self, path=None):
|
||||
self._path = path or directory.config_path()
|
||||
|
||||
self._parser = ConfigParser.SafeConfigParser()
|
||||
|
||||
self.open()
|
||||
|
||||
def open(self):
|
||||
# Open the file with the correct encoding
|
||||
if os.path.exists(self._path):
|
||||
with codecs.open(self._path, 'r', encoding='utf-8') as f:
|
||||
self._parser.readfp(f)
|
||||
|
||||
logger.info('Loaded %d sections from config file' %
|
||||
len(self._parser.sections()))
|
||||
|
||||
def write(self):
|
||||
fd, path = tempfile.mkstemp(suffix=u'.moriturirc')
|
||||
handle = os.fdopen(fd, 'w')
|
||||
self._parser.write(handle)
|
||||
handle.close()
|
||||
shutil.move(path, self._path)
|
||||
|
||||
|
||||
### any section
|
||||
|
||||
def _getter(self, suffix, section, option):
|
||||
methodName = 'get' + suffix
|
||||
method = getattr(self._parser, methodName)
|
||||
try:
|
||||
return method(section, option)
|
||||
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
|
||||
return None
|
||||
|
||||
def get(self, section, option):
|
||||
return self._getter('', section, option)
|
||||
|
||||
def getboolean(self, section, option):
|
||||
return self._getter('boolean', section, option)
|
||||
|
||||
### drive sections
|
||||
|
||||
def setReadOffset(self, vendor, model, release, offset):
|
||||
"""
|
||||
Set a read offset for the given drive.
|
||||
|
||||
Strips the given strings of leading and trailing whitespace.
|
||||
"""
|
||||
section = self._findOrCreateDriveSection(vendor, model, release)
|
||||
self._parser.set(section, 'read_offset', str(offset))
|
||||
self.write()
|
||||
|
||||
def getReadOffset(self, vendor, model, release):
|
||||
"""
|
||||
Get a read offset for the given drive.
|
||||
"""
|
||||
section = self._findDriveSection(vendor, model, release)
|
||||
|
||||
try:
|
||||
return int(self._parser.get(section, 'read_offset'))
|
||||
except ConfigParser.NoOptionError:
|
||||
raise KeyError("Could not find read_offset for %s/%s/%s" % (
|
||||
vendor, model, release))
|
||||
|
||||
|
||||
def setDefeatsCache(self, vendor, model, release, defeat):
|
||||
"""
|
||||
Set whether the drive defeats the cache.
|
||||
|
||||
Strips the given strings of leading and trailing whitespace.
|
||||
"""
|
||||
section = self._findOrCreateDriveSection(vendor, model, release)
|
||||
self._parser.set(section, 'defeats_cache', str(defeat))
|
||||
self.write()
|
||||
|
||||
def getDefeatsCache(self, vendor, model, release):
|
||||
section = self._findDriveSection(vendor, model, release)
|
||||
|
||||
try:
|
||||
return self._parser.get(section, 'defeats_cache') == 'True'
|
||||
except ConfigParser.NoOptionError:
|
||||
raise KeyError("Could not find defeats_cache for %s/%s/%s" % (
|
||||
vendor, model, release))
|
||||
|
||||
def _findDriveSection(self, vendor, model, release):
|
||||
for name in self._parser.sections():
|
||||
if not name.startswith('drive:'):
|
||||
continue
|
||||
|
||||
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)
|
||||
logger.debug("%s: '%s' versus '%s'" % (
|
||||
key, locals()[key], conf[key]
|
||||
))
|
||||
if vendor.strip() != conf['vendor']:
|
||||
continue
|
||||
if model.strip() != conf['model']:
|
||||
continue
|
||||
if release.strip() != conf['release']:
|
||||
continue
|
||||
|
||||
return name
|
||||
|
||||
raise KeyError("Could not find configuration section for %s/%s/%s" % (
|
||||
vendor, model, release))
|
||||
|
||||
def _findOrCreateDriveSection(self, vendor, model, release):
|
||||
try:
|
||||
section = self._findDriveSection(vendor, model, release)
|
||||
except KeyError:
|
||||
section = 'drive:' + urllib.quote('%s:%s:%s' % (
|
||||
vendor, model, release))
|
||||
self._parser.add_section(section)
|
||||
__pychecker__ = 'no-local'
|
||||
for key in ['vendor', 'model', 'release']:
|
||||
self._parser.set(section, key, locals()[key].strip())
|
||||
|
||||
self.write()
|
||||
|
||||
return self._findDriveSection(vendor, model, release)
|
||||
50
whipper/common/directory.py
Normal file
50
whipper/common/directory.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_directory -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
# Morituri - for those about to RIP
|
||||
|
||||
# Copyright (C) 2013 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/>.
|
||||
|
||||
from os import getenv, makedirs
|
||||
from os.path import join, expanduser, exists
|
||||
|
||||
def config_path():
|
||||
path = join(getenv('XDG_CONFIG_HOME') or join(expanduser('~'), u'.config'),
|
||||
u'whipper')
|
||||
if not exists(path):
|
||||
makedirs(path)
|
||||
return join(path, u'whipper.conf')
|
||||
|
||||
def cache_path(name=None):
|
||||
path = join(getenv('XDG_CACHE_HOME') or join(expanduser('~'), u'.cache'),
|
||||
u'whipper')
|
||||
if name:
|
||||
path = join(path, name)
|
||||
if not exists(path):
|
||||
makedirs(path)
|
||||
return path
|
||||
|
||||
def data_path(name=None):
|
||||
path = join(getenv('XDG_DATA_HOME')
|
||||
or join(expanduser('~'), u'.local/share'),
|
||||
u'whipper')
|
||||
if name:
|
||||
path = join(path, name)
|
||||
if not exists(path):
|
||||
makedirs(path)
|
||||
return path
|
||||
73
whipper/common/drive.py
Normal file
73
whipper/common/drive.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_drive -*-
|
||||
# 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 logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def _listify(listOrString):
|
||||
if type(listOrString) == str:
|
||||
return [listOrString, ]
|
||||
|
||||
return listOrString
|
||||
|
||||
|
||||
def getAllDevicePaths():
|
||||
try:
|
||||
# see https://savannah.gnu.org/bugs/index.php?38477
|
||||
return [str(dev) for dev in _getAllDevicePathsPyCdio()]
|
||||
except ImportError:
|
||||
logger.info('Cannot import pycdio')
|
||||
return _getAllDevicePathsStatic()
|
||||
|
||||
|
||||
def _getAllDevicePathsPyCdio():
|
||||
import pycdio
|
||||
import cdio
|
||||
|
||||
# using FS_AUDIO here only makes it list the drive when an audio cd
|
||||
# is inserted
|
||||
# ticket 102: this cdio call returns a list of str, or a single str
|
||||
return _listify(cdio.get_devices_with_cap(pycdio.FS_MATCH_ALL, False))
|
||||
|
||||
|
||||
def _getAllDevicePathsStatic():
|
||||
ret = []
|
||||
|
||||
for c in ['/dev/cdrom', '/dev/cdrecorder']:
|
||||
if os.path.exists(c):
|
||||
ret.append(c)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def getDeviceInfo(path):
|
||||
try:
|
||||
import cdio
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
device = cdio.Device(path)
|
||||
ok, vendor, model, release = device.get_hwinfo()
|
||||
|
||||
return (vendor, model, release)
|
||||
89
whipper/common/encode.py
Normal file
89
whipper/common/encode.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_encode -*-
|
||||
# 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/>.
|
||||
|
||||
|
||||
from mutagen.flac import FLAC
|
||||
|
||||
from whipper.extern.task import task
|
||||
|
||||
from whipper.program import sox
|
||||
from whipper.program import flac
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SoxPeakTask(task.Task):
|
||||
description = 'Calculating peak level'
|
||||
|
||||
def __init__(self, track_path):
|
||||
self.track_path = track_path
|
||||
self.peak = None
|
||||
|
||||
def start(self, runner):
|
||||
task.Task.start(self, runner)
|
||||
self.schedule(0.0, self._sox_peak)
|
||||
|
||||
def _sox_peak(self):
|
||||
self.peak = sox.peak_level(self.track_path)
|
||||
self.stop()
|
||||
|
||||
class FlacEncodeTask(task.Task):
|
||||
description = 'Encoding to FLAC'
|
||||
|
||||
def __init__(self, track_path, track_out_path, what="track"):
|
||||
self.track_path = track_path
|
||||
self.track_out_path = track_out_path
|
||||
self.new_path = None
|
||||
self.description = 'Encoding %s to FLAC' % what
|
||||
|
||||
def start(self, runner):
|
||||
task.Task.start(self, runner)
|
||||
self.schedule(0.0, self._flac_encode)
|
||||
|
||||
def _flac_encode(self):
|
||||
self.new_path = flac.encode(self.track_path, self.track_out_path)
|
||||
self.stop()
|
||||
|
||||
# TODO: Wizzup: Do we really want this as 'Task'...?
|
||||
# I only made it a task for now because that it's easier to integrate in
|
||||
# program/cdparanoia.py - where morituri currently does the tagging.
|
||||
# We should just move the tagging to a more sensible place.
|
||||
class TaggingTask(task.Task):
|
||||
description = 'Writing tags to FLAC'
|
||||
|
||||
def __init__(self, track_path, tags):
|
||||
self.track_path = track_path
|
||||
self.tags = tags
|
||||
|
||||
def start(self, runner):
|
||||
task.Task.start(self, runner)
|
||||
self.schedule(0.0, self._tag)
|
||||
|
||||
def _tag(self):
|
||||
w = FLAC(self.track_path)
|
||||
|
||||
for k, v in self.tags.items():
|
||||
w[k] = v
|
||||
|
||||
w.save()
|
||||
|
||||
self.stop()
|
||||
321
whipper/common/mbngs.py
Normal file
321
whipper/common/mbngs.py
Normal file
@@ -0,0 +1,321 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_mbngs -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
# Morituri - for those about to RIP
|
||||
|
||||
# Copyright (C) 2009, 2010, 2011 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/>.
|
||||
|
||||
"""
|
||||
Handles communication with the musicbrainz server using NGS.
|
||||
"""
|
||||
|
||||
import urllib2
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
VA_ID = "89ad4ac3-39f7-470e-963a-56509c546377" # Various Artists
|
||||
|
||||
|
||||
class MusicBrainzException(Exception):
|
||||
|
||||
def __init__(self, exc):
|
||||
self.args = (exc, )
|
||||
self.exception = exc
|
||||
|
||||
|
||||
class NotFoundException(MusicBrainzException):
|
||||
|
||||
def __str__(self):
|
||||
return "Disc not found in MusicBrainz"
|
||||
|
||||
|
||||
class TrackMetadata(object):
|
||||
artist = None
|
||||
title = None
|
||||
duration = None # in ms
|
||||
mbid = None
|
||||
sortName = None
|
||||
mbidArtist = None
|
||||
|
||||
|
||||
class DiscMetadata(object):
|
||||
"""
|
||||
@param artist: artist(s) name
|
||||
@param sortName: album artist sort name
|
||||
@param release: earliest release date, in YYYY-MM-DD
|
||||
@type release: unicode
|
||||
@param title: title of the disc (with disambiguation)
|
||||
@param releaseTitle: title of the release (without disambiguation)
|
||||
@type tracks: C{list} of L{TrackMetadata}
|
||||
"""
|
||||
artist = None
|
||||
sortName = None
|
||||
title = None
|
||||
various = False
|
||||
tracks = None
|
||||
release = None
|
||||
|
||||
releaseTitle = None
|
||||
releaseType = None
|
||||
|
||||
mbid = None
|
||||
mbidArtist = None
|
||||
url = None
|
||||
|
||||
catalogNumber = None
|
||||
barcode = None
|
||||
|
||||
def __init__(self):
|
||||
self.tracks = []
|
||||
|
||||
|
||||
def _record(record, which, name, what):
|
||||
# optionally record to disc as a JSON serialization
|
||||
if record:
|
||||
import json
|
||||
filename = 'morituri.%s.%s.json' % (which, name)
|
||||
handle = open(filename, 'w')
|
||||
handle.write(json.dumps(what))
|
||||
handle.close()
|
||||
logger.info('Wrote %s %s to %s', which, name, filename)
|
||||
|
||||
# credit is of the form [dict, str, dict, ... ]
|
||||
# e.g. [
|
||||
# {'artist': {
|
||||
# 'sort-name': 'Sukilove',
|
||||
# 'id': '5f4af6cf-a1b8-4e51-a811-befed399a1c6',
|
||||
# 'name': 'Sukilove'
|
||||
# }}, ' & ', {
|
||||
# 'artist': {
|
||||
# 'sort-name': 'Blackie and the Oohoos',
|
||||
# 'id': '028a9dc7-f5ef-43c2-866b-08d69ffff363',
|
||||
# 'name': 'Blackie & the Oohoos'}}]
|
||||
# or
|
||||
# [{'artist':
|
||||
# {'sort-name': 'Pixies',
|
||||
# 'id': 'b6b2bb8d-54a9-491f-9607-7b546023b433', 'name': 'Pixies'}}]
|
||||
|
||||
|
||||
class _Credit(list):
|
||||
"""
|
||||
I am a representation of an artist-credit in musicbrainz for a disc
|
||||
or track.
|
||||
"""
|
||||
|
||||
def joiner(self, attributeGetter, joinString=None):
|
||||
res = []
|
||||
|
||||
for item in self:
|
||||
if isinstance(item, dict):
|
||||
res.append(attributeGetter(item))
|
||||
else:
|
||||
if not joinString:
|
||||
res.append(item)
|
||||
else:
|
||||
res.append(joinString)
|
||||
|
||||
return "".join(res)
|
||||
|
||||
|
||||
def getSortName(self):
|
||||
return self.joiner(lambda i: i.get('artist').get('sort-name', None))
|
||||
|
||||
def getName(self):
|
||||
return self.joiner(lambda i: i.get('artist').get('name', None))
|
||||
|
||||
def getIds(self):
|
||||
return self.joiner(lambda i: i.get('artist').get('id', None),
|
||||
joinString=";")
|
||||
|
||||
|
||||
def _getMetadata(releaseShort, release, discid, country=None):
|
||||
"""
|
||||
@type release: C{dict}
|
||||
@param release: a release dict as returned in the value for key release
|
||||
from get_release_by_id
|
||||
|
||||
@rtype: L{DiscMetadata} or None
|
||||
"""
|
||||
logger.debug('getMetadata for release id %r',
|
||||
release['id'])
|
||||
if not release['id']:
|
||||
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:
|
||||
logger.warning('%r was not released in %r', release, country)
|
||||
return None
|
||||
|
||||
discMD = DiscMetadata()
|
||||
|
||||
discMD.releaseType = releaseShort.get('release-group', {}).get('type')
|
||||
discCredit = _Credit(release['artist-credit'])
|
||||
|
||||
# FIXME: is there a better way to check for VA ?
|
||||
discMD.various = False
|
||||
if discCredit[0]['artist']['id'] == VA_ID:
|
||||
discMD.various = True
|
||||
|
||||
|
||||
if len(discCredit) > 1:
|
||||
logger.debug('artist-credit more than 1: %r', discCredit)
|
||||
|
||||
albumArtistName = discCredit.getName()
|
||||
|
||||
# getUniqueName gets disambiguating names like Muse (UK rock band)
|
||||
discMD.artist = albumArtistName
|
||||
discMD.sortName = discCredit.getSortName()
|
||||
if 'date' not in release:
|
||||
logger.warning("Release with ID '%s' (%s - %s) does not have a date",
|
||||
release['id'], discMD.artist, release['title'])
|
||||
else:
|
||||
discMD.release = release['date']
|
||||
|
||||
discMD.mbid = release['id']
|
||||
discMD.mbidArtist = discCredit.getIds()
|
||||
discMD.url = 'https://musicbrainz.org/release/' + release['id']
|
||||
|
||||
discMD.barcode = release.get('barcode', None)
|
||||
lil = release.get('label-info-list', [{}])
|
||||
if lil:
|
||||
discMD.catalogNumber = lil[0].get('catalog-number')
|
||||
tainted = False
|
||||
duration = 0
|
||||
|
||||
# only show discs from medium-list->disc-list with matching discid
|
||||
for medium in release['medium-list']:
|
||||
for disc in medium['disc-list']:
|
||||
if disc['id'] == discid:
|
||||
title = release['title']
|
||||
discMD.releaseTitle = title
|
||||
if 'disambiguation' in release:
|
||||
title += " (%s)" % release['disambiguation']
|
||||
count = len(release['medium-list'])
|
||||
if count > 1:
|
||||
title += ' (Disc %d of %d)' % (
|
||||
int(medium['position']), count)
|
||||
if 'title' in medium:
|
||||
title += ": %s" % medium['title']
|
||||
discMD.title = title
|
||||
for t in medium['track-list']:
|
||||
track = TrackMetadata()
|
||||
trackCredit = _Credit(t['recording']['artist-credit'])
|
||||
if len(trackCredit) > 1:
|
||||
logger.debug('artist-credit more than 1: %r',
|
||||
trackCredit)
|
||||
|
||||
# FIXME: leftover comment, need an example
|
||||
# various artists discs can have tracks with no artist
|
||||
track.artist = trackCredit.getName()
|
||||
track.sortName = trackCredit.getSortName()
|
||||
track.mbidArtist = trackCredit.getIds()
|
||||
|
||||
track.title = t['recording']['title']
|
||||
track.mbid = t['recording']['id']
|
||||
|
||||
# FIXME: unit of duration ?
|
||||
track.duration = int(t['recording'].get('length', 0))
|
||||
if not track.duration:
|
||||
logger.warning('track %r (%r) does not have duration' % (
|
||||
track.title, track.mbid))
|
||||
tainted = True
|
||||
else:
|
||||
duration += track.duration
|
||||
|
||||
discMD.tracks.append(track)
|
||||
|
||||
if not tainted:
|
||||
discMD.duration = duration
|
||||
else:
|
||||
discMD.duration = 0
|
||||
|
||||
return discMD
|
||||
|
||||
|
||||
# see http://bugs.musicbrainz.org/browser/python-musicbrainz2/trunk/examples/
|
||||
# ripper.py
|
||||
|
||||
|
||||
def musicbrainz(discid, country=None, record=False):
|
||||
"""
|
||||
Based on a MusicBrainz disc id, get a list of DiscMetadata objects
|
||||
for the given disc id.
|
||||
|
||||
Example disc id: Mj48G109whzEmAbPBoGvd4KyCS4-
|
||||
|
||||
@type discid: str
|
||||
|
||||
@rtype: list of L{DiscMetadata}
|
||||
"""
|
||||
logger.debug('looking up results for discid %r', discid)
|
||||
import musicbrainzngs
|
||||
|
||||
ret = []
|
||||
|
||||
try:
|
||||
result = musicbrainzngs.get_releases_by_discid(discid,
|
||||
includes=["artists", "recordings", "release-groups"])
|
||||
except musicbrainzngs.ResponseError, e:
|
||||
if isinstance(e.cause, urllib2.HTTPError):
|
||||
if e.cause.code == 404:
|
||||
raise NotFoundException(e)
|
||||
else:
|
||||
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'):
|
||||
logger.debug('found %d releases for discid %r',
|
||||
len(result['disc']['release-list']), discid)
|
||||
_record(record, 'releases', discid, result)
|
||||
|
||||
# Display the returned results to the user.
|
||||
|
||||
import json
|
||||
for release in result['disc']['release-list']:
|
||||
formatted = json.dumps(release, sort_keys=False, indent=4)
|
||||
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
|
||||
# artist-credits
|
||||
|
||||
res = musicbrainzngs.get_release_by_id(
|
||||
release['id'], includes=["artists", "artist-credits",
|
||||
"recordings", "discids", "labels"])
|
||||
_record(record, 'release', release['id'], res)
|
||||
releaseDetail = res['release']
|
||||
formatted = json.dumps(releaseDetail, sort_keys=False, indent=4)
|
||||
logger.debug('release %s' % formatted)
|
||||
|
||||
md = _getMetadata(release, releaseDetail, discid, country)
|
||||
if md:
|
||||
logger.debug('duration %r', md.duration)
|
||||
ret.append(md)
|
||||
|
||||
return ret
|
||||
elif result.get('cdstub'):
|
||||
logger.debug('query returned cdstub: ignored')
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
68
whipper/common/path.py
Normal file
68
whipper/common/path.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_path -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
# Morituri - for those about to RIP
|
||||
|
||||
# Copyright (C) 2009 Thomas Vander Stichele
|
||||
|
||||
# This file is part of 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 re
|
||||
|
||||
|
||||
class PathFilter(object):
|
||||
"""
|
||||
I filter path components for safe storage on file systems.
|
||||
"""
|
||||
|
||||
def __init__(self, slashes=True, quotes=True, fat=True, special=False):
|
||||
"""
|
||||
@param slashes: whether to convert slashes to dashes
|
||||
@param quotes: whether to normalize quotes
|
||||
@param fat: whether to strip characters illegal on FAT filesystems
|
||||
@param special: whether to strip special characters
|
||||
"""
|
||||
self._slashes = slashes
|
||||
self._quotes = quotes
|
||||
self._fat = fat
|
||||
self._special = special
|
||||
|
||||
def filter(self, path):
|
||||
if self._slashes:
|
||||
path = re.sub(r'[/\\]', '-', path, re.UNICODE)
|
||||
|
||||
def separators(path):
|
||||
# replace separators with a space-hyphen or hyphen
|
||||
path = re.sub(r'[:]', ' -', path, re.UNICODE)
|
||||
path = re.sub(r'[\|]', '-', path, re.UNICODE)
|
||||
return path
|
||||
|
||||
# change all fancy single/double quotes to normal quotes
|
||||
if self._quotes:
|
||||
path = re.sub(ur'[\xc2\xb4\u2018\u2019\u201b]', "'", path,
|
||||
re.UNICODE)
|
||||
path = re.sub(ur'[\u201c\u201d\u201f]', '"', path, re.UNICODE)
|
||||
|
||||
if self._special:
|
||||
path = separators(path)
|
||||
path = re.sub(r'[\*\?&!\'\"\$\(\)`{}\[\]<>]', '_', path, re.UNICODE)
|
||||
|
||||
if self._fat:
|
||||
path = separators(path)
|
||||
# : and | already gone, but leave them here for reference
|
||||
path = re.sub(r'[:\*\?"<>|"]', '_', path, re.UNICODE)
|
||||
|
||||
return path
|
||||
703
whipper/common/program.py
Normal file
703
whipper/common/program.py
Normal file
@@ -0,0 +1,703 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_program -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
# Morituri - for those about to RIP
|
||||
|
||||
# Copyright (C) 2009, 2010, 2011 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/>.
|
||||
|
||||
"""
|
||||
Common functionality and class for all programs using morituri.
|
||||
"""
|
||||
|
||||
import musicbrainzngs
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from whipper.common import common, mbngs, cache, path
|
||||
from whipper.common import checksum
|
||||
from whipper.program import cdrdao, cdparanoia
|
||||
from whipper.image import image
|
||||
from whipper.extern.task import task
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# FIXME: should Program have a runner ?
|
||||
|
||||
|
||||
class Program:
|
||||
"""
|
||||
I maintain program state and functionality.
|
||||
|
||||
@ivar metadata:
|
||||
@type metadata: L{mbngs.DiscMetadata}
|
||||
@ivar result: the rip's result
|
||||
@type result: L{result.RipResult}
|
||||
@type outdir: unicode
|
||||
@type config: L{whipper.common.config.Config}
|
||||
"""
|
||||
|
||||
cuePath = None
|
||||
logPath = None
|
||||
metadata = None
|
||||
outdir = None
|
||||
result = None
|
||||
|
||||
_stdout = None
|
||||
|
||||
def __init__(self, config, record=False, stdout=sys.stdout):
|
||||
"""
|
||||
@param record: whether to record results of API calls for playback.
|
||||
"""
|
||||
self._record = record
|
||||
self._cache = cache.ResultCache()
|
||||
self._stdout = stdout
|
||||
self._config = config
|
||||
|
||||
d = {}
|
||||
|
||||
for key, default in {
|
||||
'fat': True,
|
||||
'special': False
|
||||
}.items():
|
||||
value = None
|
||||
value = self._config.getboolean('main', 'path_filter_'+ key)
|
||||
if value is None:
|
||||
value = default
|
||||
|
||||
d[key] = value
|
||||
|
||||
self._filter = path.PathFilter(**d)
|
||||
|
||||
def setWorkingDirectory(self, workingDirectory):
|
||||
if workingDirectory:
|
||||
logger.info('Changing to working directory %s' % workingDirectory)
|
||||
os.chdir(workingDirectory)
|
||||
|
||||
def getFastToc(self, runner, toc_pickle, device):
|
||||
"""
|
||||
Retrieve the normal TOC table from a toc pickle or the drive.
|
||||
Also retrieves the cdrdao version
|
||||
|
||||
@rtype: tuple of L{table.Table}, str
|
||||
"""
|
||||
def function(r, t):
|
||||
r.run(t)
|
||||
|
||||
ptoc = cache.Persister(toc_pickle or None)
|
||||
if not ptoc.object:
|
||||
from pkg_resources import parse_version as V
|
||||
version = cdrdao.getCDRDAOVersion()
|
||||
if V(version) < V('1.2.3rc2'):
|
||||
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')
|
||||
t = cdrdao.ReadTOCTask(device)
|
||||
ptoc.persist(t.table)
|
||||
toc = ptoc.object
|
||||
assert toc.hasTOC()
|
||||
return toc
|
||||
|
||||
def getTable(self, runner, cddbdiscid, mbdiscid, device, offset):
|
||||
"""
|
||||
Retrieve the Table either from the cache or the drive.
|
||||
|
||||
@rtype: L{table.Table}
|
||||
"""
|
||||
tcache = cache.TableCache()
|
||||
ptable = tcache.get(cddbdiscid, mbdiscid)
|
||||
itable = None
|
||||
tdict = {}
|
||||
|
||||
# Ingore old cache, since we do not know what offset it used.
|
||||
if type(ptable.object) is dict:
|
||||
tdict = ptable.object
|
||||
|
||||
if offset in tdict:
|
||||
itable = tdict[offset]
|
||||
|
||||
if not itable:
|
||||
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)
|
||||
logger.debug('getTable: read table %r' % itable)
|
||||
else:
|
||||
logger.debug('getTable: cddbdiscid %s, mbdiscid %s in cache for offset %s' % (
|
||||
cddbdiscid, mbdiscid, offset))
|
||||
logger.debug('getTable: loaded table %r' % itable)
|
||||
|
||||
assert itable.hasTOC()
|
||||
|
||||
self.result.table = itable
|
||||
|
||||
logger.debug('getTable: returning table with mb id %s' %
|
||||
itable.getMusicBrainzDiscId())
|
||||
return itable
|
||||
|
||||
def getRipResult(self, cddbdiscid):
|
||||
"""
|
||||
Retrieve the persistable RipResult either from our cache (from a
|
||||
previous, possibly aborted rip), or return a new one.
|
||||
|
||||
@rtype: L{result.RipResult}
|
||||
"""
|
||||
assert self.result is None
|
||||
|
||||
self._presult = self._cache.getRipResult(cddbdiscid)
|
||||
self.result = self._presult.object
|
||||
|
||||
return self.result
|
||||
|
||||
def saveRipResult(self):
|
||||
self._presult.persist()
|
||||
|
||||
def getPath(self, outdir, template, mbdiscid, i, disambiguate=False):
|
||||
"""
|
||||
Based on the template, get a complete path for the given track,
|
||||
minus extension.
|
||||
Also works for the disc name, using disc variables for the template.
|
||||
|
||||
@param outdir: the directory where to write the files
|
||||
@type outdir: unicode
|
||||
@param template: the template for writing the file
|
||||
@type template: unicode
|
||||
@param i: track number (0 for HTOA, or for disc)
|
||||
@type i: int
|
||||
|
||||
@rtype: unicode
|
||||
"""
|
||||
assert type(outdir) is unicode, "%r is not unicode" % outdir
|
||||
assert type(template) is unicode, "%r is not unicode" % template
|
||||
|
||||
# the template is similar to grip, except for %s/%S/%r/%R
|
||||
# see #gripswitches
|
||||
|
||||
# returns without extension
|
||||
|
||||
v = {}
|
||||
|
||||
v['t'] = '%02d' % i
|
||||
|
||||
# default values
|
||||
v['A'] = 'Unknown Artist'
|
||||
v['d'] = mbdiscid # fallback for title
|
||||
v['r'] = 'unknown'
|
||||
v['R'] = 'Unknown'
|
||||
v['B'] = '' # barcode
|
||||
v['C'] = '' # catalog number
|
||||
v['x'] = 'flac'
|
||||
v['X'] = v['x'].upper()
|
||||
v['y'] = '0000'
|
||||
|
||||
v['a'] = v['A']
|
||||
if i == 0:
|
||||
v['n'] = 'Hidden Track One Audio'
|
||||
else:
|
||||
v['n'] = 'Unknown Track %d' % i
|
||||
|
||||
|
||||
if self.metadata:
|
||||
release = self.metadata.release or '0000'
|
||||
v['y'] = release[:4]
|
||||
v['A'] = self._filter.filter(self.metadata.artist)
|
||||
v['S'] = self._filter.filter(self.metadata.sortName)
|
||||
v['d'] = self._filter.filter(self.metadata.title)
|
||||
v['B'] = self.metadata.barcode
|
||||
v['C'] = self.metadata.catalogNumber
|
||||
if self.metadata.releaseType:
|
||||
v['R'] = self.metadata.releaseType
|
||||
v['r'] = self.metadata.releaseType.lower()
|
||||
if i > 0:
|
||||
try:
|
||||
v['a'] = self._filter.filter(self.metadata.tracks[i - 1].artist)
|
||||
v['s'] = self._filter.filter(
|
||||
self.metadata.tracks[i - 1].sortName)
|
||||
v['n'] = self._filter.filter(self.metadata.tracks[i - 1].title)
|
||||
except IndexError, e:
|
||||
print 'ERROR: no track %d found, %r' % (i, e)
|
||||
raise
|
||||
else:
|
||||
# htoa defaults to disc's artist
|
||||
v['a'] = self._filter.filter(self.metadata.artist)
|
||||
|
||||
# when disambiguating, use catalogNumber then barcode
|
||||
if disambiguate:
|
||||
templateParts = list(os.path.split(template))
|
||||
if self.metadata.catalogNumber:
|
||||
templateParts[-2] += ' (%s)' % self.metadata.catalogNumber
|
||||
elif self.metadata.barcode:
|
||||
templateParts[-2] += ' (%s)' % self.metadata.barcode
|
||||
template = os.path.join(*templateParts)
|
||||
logger.debug('Disambiguated template to %r' % template)
|
||||
|
||||
import re
|
||||
template = re.sub(r'%(\w)', r'%(\1)s', template)
|
||||
|
||||
ret = os.path.join(outdir, template % v)
|
||||
|
||||
|
||||
|
||||
return ret
|
||||
|
||||
def getCDDB(self, cddbdiscid):
|
||||
"""
|
||||
@param cddbdiscid: list of id, tracks, offsets, seconds
|
||||
|
||||
@rtype: str
|
||||
"""
|
||||
# FIXME: convert to nonblocking?
|
||||
import CDDB
|
||||
try:
|
||||
code, md = CDDB.query(cddbdiscid)
|
||||
logger.debug('CDDB query result: %r, %r', code, md)
|
||||
if code == 200:
|
||||
return md['title']
|
||||
|
||||
except IOError, e:
|
||||
# FIXME: for some reason errno is a str ?
|
||||
if e.errno == 'socket error':
|
||||
self._stdout.write("Warning: network error: %r\n" % (e, ))
|
||||
else:
|
||||
raise
|
||||
|
||||
return None
|
||||
|
||||
def getMusicBrainz(self, ittoc, mbdiscid, release=None, country=None, prompt=False):
|
||||
"""
|
||||
@type ittoc: L{whipper.image.table.Table}
|
||||
"""
|
||||
# look up disc on musicbrainz
|
||||
self._stdout.write('Disc duration: %s, %d audio tracks\n' % (
|
||||
common.formatTime(ittoc.duration() / 1000.0),
|
||||
ittoc.getAudioTracks()))
|
||||
logger.debug('MusicBrainz submit url: %r',
|
||||
ittoc.getMusicBrainzSubmitURL())
|
||||
ret = None
|
||||
|
||||
metadatas = None
|
||||
e = None
|
||||
|
||||
for _ in range(0, 4):
|
||||
try:
|
||||
metadatas = mbngs.musicbrainz(mbdiscid,
|
||||
country=country,
|
||||
record=self._record)
|
||||
break
|
||||
except mbngs.NotFoundException, e:
|
||||
break
|
||||
except musicbrainzngs.NetworkError, e:
|
||||
self._stdout.write("Warning: network error: %r\n" % (e, ))
|
||||
break
|
||||
except mbngs.MusicBrainzException, e:
|
||||
self._stdout.write("Warning: %r\n" % (e, ))
|
||||
time.sleep(5)
|
||||
continue
|
||||
|
||||
if not metadatas:
|
||||
if e:
|
||||
self._stdout.write("Error: %r\n" % (e, ))
|
||||
self._stdout.write('Continuing without metadata\n')
|
||||
|
||||
if metadatas:
|
||||
deltas = {}
|
||||
|
||||
self._stdout.write('\nMatching releases:\n')
|
||||
|
||||
for metadata in metadatas:
|
||||
self._stdout.write('\n')
|
||||
self._stdout.write('Artist : %s\n' %
|
||||
metadata.artist.encode('utf-8'))
|
||||
self._stdout.write('Title : %s\n' %
|
||||
metadata.title.encode('utf-8'))
|
||||
self._stdout.write('Duration: %s\n' %
|
||||
common.formatTime(metadata.duration / 1000.0))
|
||||
self._stdout.write('URL : %s\n' % metadata.url)
|
||||
self._stdout.write('Release : %s\n' % metadata.mbid)
|
||||
self._stdout.write('Type : %s\n' % metadata.releaseType)
|
||||
if metadata.barcode:
|
||||
self._stdout.write("Barcode : %s\n" % metadata.barcode)
|
||||
if metadata.catalogNumber:
|
||||
self._stdout.write("Cat no : %s\n" % metadata.catalogNumber)
|
||||
|
||||
delta = abs(metadata.duration - ittoc.duration())
|
||||
if not delta in deltas:
|
||||
deltas[delta] = []
|
||||
deltas[delta].append(metadata)
|
||||
|
||||
lowest = None
|
||||
|
||||
if not release and len(metadatas) > 1:
|
||||
# Select the release that most closely matches the duration.
|
||||
lowest = min(deltas.keys())
|
||||
|
||||
if prompt:
|
||||
guess = (deltas[lowest])[0].mbid
|
||||
release = raw_input("\nPlease select a release [%s]: " % guess)
|
||||
|
||||
if not release:
|
||||
release = guess
|
||||
|
||||
if release:
|
||||
metadatas = [m for m in metadatas if m.url.endswith(release)]
|
||||
logger.debug('Asked for release %r, only kept %r',
|
||||
release, metadatas)
|
||||
if len(metadatas) == 1:
|
||||
self._stdout.write('\n')
|
||||
self._stdout.write('Picked requested release id %s\n' %
|
||||
release)
|
||||
self._stdout.write('Artist : %s\n' %
|
||||
metadatas[0].artist.encode('utf-8'))
|
||||
self._stdout.write('Title : %s\n' %
|
||||
metadatas[0].title.encode('utf-8'))
|
||||
elif not metadatas:
|
||||
self._stdout.write(
|
||||
"Requested release id '%s', "
|
||||
"but none of the found releases match\n" % release)
|
||||
return
|
||||
else:
|
||||
if lowest:
|
||||
metadatas = deltas[lowest]
|
||||
|
||||
# If we have multiple, make sure they match
|
||||
if len(metadatas) > 1:
|
||||
artist = metadatas[0].artist
|
||||
releaseTitle = metadatas[0].releaseTitle
|
||||
for i, metadata in enumerate(metadatas):
|
||||
if not artist == metadata.artist:
|
||||
logger.warning("artist 0: %r and artist %d: %r "
|
||||
"are not the same" % (
|
||||
artist, i, metadata.artist))
|
||||
if not releaseTitle == metadata.releaseTitle:
|
||||
logger.warning("title 0: %r and title %d: %r "
|
||||
"are not the same" % (
|
||||
releaseTitle, i, metadata.releaseTitle))
|
||||
|
||||
if (not release and len(deltas.keys()) > 1):
|
||||
self._stdout.write('\n')
|
||||
self._stdout.write('Picked closest match in duration.\n')
|
||||
self._stdout.write('Others may be wrong in musicbrainz, '
|
||||
'please correct.\n')
|
||||
self._stdout.write('Artist : %s\n' %
|
||||
artist.encode('utf-8'))
|
||||
self._stdout.write('Title : %s\n' %
|
||||
metadatas[0].title.encode('utf-8'))
|
||||
|
||||
# Select one of the returned releases. We just pick the first one.
|
||||
ret = metadatas[0]
|
||||
else:
|
||||
self._stdout.write(
|
||||
'Submit this disc to MusicBrainz at the above URL.\n')
|
||||
ret = None
|
||||
|
||||
self._stdout.write('\n')
|
||||
return ret
|
||||
|
||||
def getTagList(self, number):
|
||||
"""
|
||||
Based on the metadata, get a dict of tags for the given track.
|
||||
|
||||
@param number: track number (0 for HTOA)
|
||||
@type number: int
|
||||
|
||||
@rtype: dict
|
||||
"""
|
||||
trackArtist = u'Unknown Artist'
|
||||
albumArtist = u'Unknown Artist'
|
||||
disc = u'Unknown Disc'
|
||||
title = u'Unknown Track'
|
||||
|
||||
if self.metadata:
|
||||
trackArtist = self.metadata.artist
|
||||
albumArtist = self.metadata.artist
|
||||
disc = self.metadata.title
|
||||
mbidAlbum = self.metadata.mbid
|
||||
mbidTrackAlbum = self.metadata.mbidArtist
|
||||
mbDiscId = self.metadata.discid
|
||||
|
||||
if number > 0:
|
||||
try:
|
||||
track = self.metadata.tracks[number - 1]
|
||||
trackArtist = track.artist
|
||||
title = track.title
|
||||
mbidTrack = track.mbid
|
||||
mbidTrackArtist = track.mbidArtist
|
||||
except IndexError, e:
|
||||
print 'ERROR: no track %d found, %r' % (number, e)
|
||||
raise
|
||||
else:
|
||||
# htoa defaults to disc's artist
|
||||
title = 'Hidden Track One Audio'
|
||||
|
||||
tags = {}
|
||||
|
||||
if self.metadata and not self.metadata.various:
|
||||
tags['ALBUMARTIST'] = albumArtist
|
||||
tags['ARTIST'] = trackArtist
|
||||
tags['TITLE'] = title
|
||||
tags['ALBUM'] = disc
|
||||
|
||||
tags['TRACKNUMBER'] = u'%s' % number
|
||||
|
||||
if self.metadata:
|
||||
if self.metadata.release is not None:
|
||||
tags['DATE'] = self.metadata.release
|
||||
|
||||
if number > 0:
|
||||
tags['MUSICBRAINZ_TRACKID'] = mbidTrack
|
||||
tags['MUSICBRAINZ_ARTISTID'] = mbidTrackArtist
|
||||
tags['MUSICBRAINZ_ALBUMID'] = mbidAlbum
|
||||
tags['MUSICBRAINZ_ALBUMARTISTID'] = mbidTrackAlbum
|
||||
tags['MUSICBRAINZ_DISCID'] = mbDiscId
|
||||
|
||||
# TODO/FIXME: ISRC tag
|
||||
|
||||
return tags
|
||||
|
||||
def getHTOA(self):
|
||||
"""
|
||||
Check if we have hidden track one audio.
|
||||
|
||||
@returns: tuple of (start, stop), or None
|
||||
"""
|
||||
track = self.result.table.tracks[0]
|
||||
try:
|
||||
index = track.getIndex(0)
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
start = index.absolute
|
||||
stop = track.getIndex(1).absolute - 1
|
||||
return (start, stop)
|
||||
|
||||
def verifyTrack(self, runner, trackResult):
|
||||
|
||||
t = checksum.CRC32Task(trackResult.filename)
|
||||
|
||||
try:
|
||||
runner.run(t)
|
||||
except task.TaskException, e:
|
||||
if isinstance(e.exception, common.MissingFrames):
|
||||
logger.warning('missing frames for %r' % trackResult.filename)
|
||||
return False
|
||||
else:
|
||||
raise
|
||||
|
||||
ret = trackResult.testcrc == t.checksum
|
||||
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, taglist,
|
||||
overread, what=None):
|
||||
"""
|
||||
Ripping the track may change the track's filename as stored in
|
||||
trackResult.
|
||||
|
||||
@param trackResult: the object to store information in.
|
||||
@type trackResult: L{result.TrackResult}
|
||||
"""
|
||||
if trackResult.number == 0:
|
||||
start, stop = self.getHTOA()
|
||||
else:
|
||||
start = self.result.table.getTrackStart(trackResult.number)
|
||||
stop = self.result.table.getTrackEnd(trackResult.number)
|
||||
|
||||
dirname = os.path.dirname(trackResult.filename)
|
||||
if not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
|
||||
if not what:
|
||||
what='track %d' % (trackResult.number, )
|
||||
|
||||
t = cdparanoia.ReadVerifyTrackTask(trackResult.filename,
|
||||
self.result.table, start, stop, overread,
|
||||
offset=offset,
|
||||
device=device,
|
||||
taglist=taglist,
|
||||
what=what)
|
||||
|
||||
runner.run(t)
|
||||
|
||||
logger.debug('ripped track')
|
||||
logger.debug('test speed %.3f/%.3f seconds' % (
|
||||
t.testspeed, t.testduration))
|
||||
logger.debug('copy speed %.3f/%.3f seconds' % (
|
||||
t.copyspeed, t.copyduration))
|
||||
trackResult.testcrc = t.testchecksum
|
||||
trackResult.copycrc = t.copychecksum
|
||||
trackResult.peak = t.peak
|
||||
trackResult.quality = t.quality
|
||||
trackResult.testspeed = t.testspeed
|
||||
trackResult.copyspeed = t.copyspeed
|
||||
# we want rerips to add cumulatively to the time
|
||||
trackResult.testduration += t.testduration
|
||||
trackResult.copyduration += t.copyduration
|
||||
|
||||
if trackResult.filename != t.path:
|
||||
trackResult.filename = t.path
|
||||
logger.info('Filename changed to %r', trackResult.filename)
|
||||
|
||||
def retagImage(self, runner, taglists):
|
||||
cueImage = image.Image(self.cuePath)
|
||||
t = image.ImageRetagTask(cueImage, taglists)
|
||||
runner.run(t)
|
||||
|
||||
def verifyImage(self, runner, responses):
|
||||
"""
|
||||
Verify our image against the given AccurateRip responses.
|
||||
|
||||
Needs an initialized self.result.
|
||||
Will set accurip and friends on each TrackResult.
|
||||
"""
|
||||
|
||||
logger.debug('verifying Image against %d AccurateRip responses',
|
||||
len(responses or []))
|
||||
|
||||
cueImage = image.Image(self.cuePath)
|
||||
verifytask = image.ImageVerifyTask(cueImage)
|
||||
cuetask = image.AccurateRipChecksumTask(cueImage)
|
||||
runner.run(verifytask)
|
||||
runner.run(cuetask)
|
||||
|
||||
self._verifyImageWithChecksums(responses, cuetask.checksums)
|
||||
|
||||
def _verifyImageWithChecksums(self, responses, checksums):
|
||||
# loop over tracks to set our calculated AccurateRip CRC's
|
||||
for i, csum in enumerate(checksums):
|
||||
trackResult = self.result.getTrackResult(i + 1)
|
||||
trackResult.ARCRC = csum
|
||||
|
||||
|
||||
if not responses:
|
||||
logger.warning('No AccurateRip responses, cannot verify.')
|
||||
return
|
||||
|
||||
# now loop to match responses
|
||||
for i, csum in enumerate(checksums):
|
||||
trackResult = self.result.getTrackResult(i + 1)
|
||||
|
||||
confidence = None
|
||||
response = None
|
||||
|
||||
# match against each response's checksum for this track
|
||||
for j, r in enumerate(responses):
|
||||
if "%08x" % csum == r.checksums[i]:
|
||||
response = r
|
||||
logger.debug(
|
||||
"Track %02d matched response %d of %d in "
|
||||
"AccurateRip database",
|
||||
i + 1, j + 1, len(responses))
|
||||
trackResult.accurip = True
|
||||
# FIXME: maybe checksums should be ints
|
||||
trackResult.ARDBCRC = int(r.checksums[i], 16)
|
||||
# arsum = csum
|
||||
confidence = r.confidences[i]
|
||||
trackResult.ARDBConfidence = confidence
|
||||
|
||||
if not trackResult.accurip:
|
||||
logger.warning("Track %02d: not matched in AccurateRip database",
|
||||
i + 1)
|
||||
|
||||
# I have seen AccurateRip responses with 0 as confidence
|
||||
# for example, Best of Luke Haines, disc 1, track 1
|
||||
maxConfidence = -1
|
||||
maxResponse = None
|
||||
for r in responses:
|
||||
if r.confidences[i] > maxConfidence:
|
||||
maxConfidence = r.confidences[i]
|
||||
maxResponse = r
|
||||
|
||||
logger.debug('Track %02d: found max confidence %d' % (
|
||||
i + 1, maxConfidence))
|
||||
trackResult.ARDBMaxConfidence = maxConfidence
|
||||
if not response:
|
||||
logger.warning('Track %02d: none of the responses matched.',
|
||||
i + 1)
|
||||
trackResult.ARDBCRC = int(
|
||||
maxResponse.checksums[i], 16)
|
||||
else:
|
||||
trackResult.ARDBCRC = int(response.checksums[i], 16)
|
||||
|
||||
# TODO MW: Update this further for ARv2 code
|
||||
def getAccurateRipResults(self):
|
||||
"""
|
||||
@rtype: list of str
|
||||
"""
|
||||
res = []
|
||||
|
||||
# loop over tracks
|
||||
for i, trackResult in enumerate(self.result.tracks):
|
||||
status = 'rip NOT accurate'
|
||||
|
||||
if trackResult.accurip:
|
||||
status = 'rip accurate '
|
||||
|
||||
c = "(not found) "
|
||||
ar = ", DB [notfound]"
|
||||
if trackResult.ARDBMaxConfidence:
|
||||
c = "(max confidence %3d)" % trackResult.ARDBMaxConfidence
|
||||
if trackResult.ARDBConfidence is not None:
|
||||
if trackResult.ARDBConfidence \
|
||||
< trackResult.ARDBMaxConfidence:
|
||||
c = "(confidence %3d of %3d)" % (
|
||||
trackResult.ARDBConfidence,
|
||||
trackResult.ARDBMaxConfidence)
|
||||
|
||||
ar = ", DB [%08x]" % trackResult.ARDBCRC
|
||||
# htoa tracks (i == 0) do not have an ARCRC
|
||||
if trackResult.ARCRC is None:
|
||||
assert trackResult.number == 0, \
|
||||
'no trackResult.ARCRC on non-HTOA track %d' % \
|
||||
trackResult.number
|
||||
res.append("Track 0: unknown (not tracked)")
|
||||
else:
|
||||
res.append("Track %2d: %s %s [%08x]%s" % (
|
||||
trackResult.number, status, c, trackResult.ARCRC, ar))
|
||||
|
||||
return res
|
||||
|
||||
def writeCue(self, discName):
|
||||
assert self.result.table.canCue()
|
||||
cuePath = '%s.cue' % discName
|
||||
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'))
|
||||
handle.close()
|
||||
|
||||
self.cuePath = cuePath
|
||||
|
||||
return cuePath
|
||||
|
||||
def writeLog(self, discName, logger):
|
||||
logPath = '%s.log' % discName
|
||||
handle = open(logPath, 'w')
|
||||
log = logger.log(self.result)
|
||||
handle.write(log.encode('utf-8'))
|
||||
handle.close()
|
||||
|
||||
self.logPath = logPath
|
||||
|
||||
return logPath
|
||||
223
whipper/common/renamer.py
Normal file
223
whipper/common/renamer.py
Normal file
@@ -0,0 +1,223 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_renamer -*-
|
||||
# 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 tempfile
|
||||
|
||||
"""
|
||||
Rename files on file system and inside metafiles in a resumable way.
|
||||
"""
|
||||
|
||||
|
||||
class Operator(object):
|
||||
|
||||
def __init__(self, statePath, key):
|
||||
self._todo = []
|
||||
self._done = []
|
||||
self._statePath = statePath
|
||||
self._key = key
|
||||
self._resuming = False
|
||||
|
||||
def addOperation(self, operation):
|
||||
"""
|
||||
Add an operation.
|
||||
"""
|
||||
self._todo.append(operation)
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
Load state from the given state path using the given key.
|
||||
Verifies the state.
|
||||
"""
|
||||
todo = os.path.join(self._statePath, self._key + '.todo')
|
||||
lines = []
|
||||
with open(todo, 'r') as handle:
|
||||
for line in handle.readlines():
|
||||
lines.append(line)
|
||||
name, data = line.split(' ', 1)
|
||||
cls = globals()[name]
|
||||
operation = cls.deserialize(data)
|
||||
self._todo.append(operation)
|
||||
|
||||
|
||||
done = os.path.join(self._statePath, self._key + '.done')
|
||||
if os.path.exists(done):
|
||||
with open(done, 'r') as handle:
|
||||
for i, line in enumerate(handle.readlines()):
|
||||
assert line == lines[i], "line %s is different than %s" % (
|
||||
line, lines[i])
|
||||
self._done.append(self._todo[i])
|
||||
|
||||
# last task done is i; check if the next one might have gotten done.
|
||||
self._resuming = True
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Saves the state to the given state path using the given key.
|
||||
"""
|
||||
# only save todo first time
|
||||
todo = os.path.join(self._statePath, self._key + '.todo')
|
||||
if not os.path.exists(todo):
|
||||
with open(todo, 'w') as handle:
|
||||
for o in self._todo:
|
||||
name = o.__class__.__name__
|
||||
data = o.serialize()
|
||||
handle.write('%s %s\n' % (name, data))
|
||||
|
||||
# save done every time
|
||||
done = os.path.join(self._statePath, self._key + '.done')
|
||||
with open(done, 'w') as handle:
|
||||
for o in self._done:
|
||||
name = o.__class__.__name__
|
||||
data = o.serialize()
|
||||
handle.write('%s %s\n' % (name, data))
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Execute the operations
|
||||
"""
|
||||
|
||||
def next(self):
|
||||
operation = self._todo[len(self._done)]
|
||||
if self._resuming:
|
||||
operation.redo()
|
||||
self._resuming = False
|
||||
else:
|
||||
operation.do()
|
||||
|
||||
self._done.append(operation)
|
||||
self.save()
|
||||
|
||||
|
||||
class FileRenamer(Operator):
|
||||
|
||||
def addRename(self, source, destination):
|
||||
"""
|
||||
Add a rename operation.
|
||||
|
||||
@param source: source filename
|
||||
@type source: str
|
||||
@param destination: destination filename
|
||||
@type destination: str
|
||||
"""
|
||||
|
||||
|
||||
class Operation(object):
|
||||
|
||||
def verify(self):
|
||||
"""
|
||||
Check if the operation will succeed in the current conditions.
|
||||
Consider this a pre-flight check.
|
||||
|
||||
Does not eliminate the need to handle errors as they happen.
|
||||
"""
|
||||
|
||||
def do(self):
|
||||
"""
|
||||
Perform the operation.
|
||||
"""
|
||||
pass
|
||||
|
||||
def redo(self):
|
||||
"""
|
||||
Perform the operation, without knowing if it already has been
|
||||
(partly) performed.
|
||||
"""
|
||||
self.do()
|
||||
|
||||
def serialize(self):
|
||||
"""
|
||||
Serialize the operation.
|
||||
The return value should bu usable with L{deserialize}
|
||||
|
||||
@rtype: str
|
||||
"""
|
||||
|
||||
def deserialize(cls, data):
|
||||
"""
|
||||
Deserialize the operation with the given operation data.
|
||||
|
||||
@type data: str
|
||||
"""
|
||||
raise NotImplementedError
|
||||
deserialize = classmethod(deserialize)
|
||||
|
||||
|
||||
class RenameFile(Operation):
|
||||
|
||||
def __init__(self, source, destination):
|
||||
self._source = source
|
||||
self._destination = destination
|
||||
|
||||
def verify(self):
|
||||
assert os.path.exists(self._source)
|
||||
assert not os.path.exists(self._destination)
|
||||
|
||||
def do(self):
|
||||
os.rename(self._source, self._destination)
|
||||
|
||||
def serialize(self):
|
||||
return '"%s" "%s"' % (self._source, self._destination)
|
||||
|
||||
def deserialize(cls, data):
|
||||
_, source, __, destination, ___ = data.split('"')
|
||||
return RenameFile(source, destination)
|
||||
deserialize = classmethod(deserialize)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self._source == other._source \
|
||||
and self._destination == other._destination
|
||||
|
||||
|
||||
class RenameInFile(Operation):
|
||||
|
||||
def __init__(self, path, source, destination):
|
||||
self._path = path
|
||||
self._source = source
|
||||
self._destination = destination
|
||||
|
||||
def verify(self):
|
||||
assert os.path.exists(self._path)
|
||||
# check if the source exists in the given file
|
||||
|
||||
def do(self):
|
||||
with open(self._path) as handle:
|
||||
(fd, name) = tempfile.mkstemp(suffix='.morituri')
|
||||
|
||||
for s in handle:
|
||||
os.write(fd, s.replace(self._source, self._destination))
|
||||
|
||||
os.close(fd)
|
||||
os.rename(name, self._path)
|
||||
|
||||
def serialize(self):
|
||||
return '"%s" "%s" "%s"' % (self._path, self._source, self._destination)
|
||||
|
||||
def deserialize(cls, data):
|
||||
_, path, __, source, ___, destination, ____ = data.split('"')
|
||||
return RenameInFile(path, source, destination)
|
||||
deserialize = classmethod(deserialize)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self._source == other._source \
|
||||
and self._destination == other._destination \
|
||||
and self._path == other._path
|
||||
149
whipper/common/task.py
Normal file
149
whipper/common/task.py
Normal file
@@ -0,0 +1,149 @@
|
||||
# -*- Mode: Python -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
|
||||
from whipper.extern import asyncsub
|
||||
from whipper.extern.task import task
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SyncRunner(task.SyncRunner):
|
||||
pass
|
||||
|
||||
|
||||
class LoggableTask(task.Task):
|
||||
pass
|
||||
|
||||
|
||||
class LoggableMultiSeparateTask(task.MultiSeparateTask):
|
||||
pass
|
||||
|
||||
|
||||
class PopenTask(task.Task):
|
||||
"""
|
||||
I am a task that runs a command using Popen.
|
||||
"""
|
||||
|
||||
logCategory = 'PopenTask'
|
||||
bufsize = 1024
|
||||
command = None
|
||||
cwd = None
|
||||
|
||||
def start(self, runner):
|
||||
task.Task.start(self, runner)
|
||||
|
||||
try:
|
||||
self._popen = asyncsub.Popen(self.command,
|
||||
bufsize=self.bufsize,
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, close_fds=True, cwd=self.cwd)
|
||||
except OSError, e:
|
||||
import errno
|
||||
if e.errno == errno.ENOENT:
|
||||
self.commandMissing()
|
||||
|
||||
raise
|
||||
|
||||
logger.debug('Started %r with pid %d', self.command,
|
||||
self._popen.pid)
|
||||
|
||||
self.schedule(1.0, self._read, runner)
|
||||
|
||||
def _read(self, runner):
|
||||
try:
|
||||
read = False
|
||||
|
||||
ret = self._popen.recv()
|
||||
|
||||
if ret:
|
||||
logger.debug("read from stdout: %s", ret)
|
||||
self.readbytesout(ret)
|
||||
read = True
|
||||
|
||||
ret = self._popen.recv_err()
|
||||
|
||||
if ret:
|
||||
logger.debug("read from stderr: %s", ret)
|
||||
self.readbyteserr(ret)
|
||||
read = True
|
||||
|
||||
# if we read anything, we might have more to read, so
|
||||
# reschedule immediately
|
||||
if read and self.runner:
|
||||
self.schedule(0.0, self._read, runner)
|
||||
return
|
||||
|
||||
# if we didn't read anything, give the command more time to
|
||||
# produce output
|
||||
if self._popen.poll() is None and self.runner:
|
||||
# not finished yet
|
||||
self.schedule(1.0, self._read, runner)
|
||||
return
|
||||
|
||||
self._done()
|
||||
except Exception, e:
|
||||
logger.debug('exception during _read(): %r', str(e))
|
||||
self.setException(e)
|
||||
self.stop()
|
||||
|
||||
def _done(self):
|
||||
assert self._popen.returncode is not None, "No returncode"
|
||||
|
||||
if self._popen.returncode >= 0:
|
||||
logger.debug('Return code was %d', self._popen.returncode)
|
||||
else:
|
||||
logger.debug('Terminated with signal %d',
|
||||
-self._popen.returncode)
|
||||
|
||||
self.setProgress(1.0)
|
||||
|
||||
if self._popen.returncode != 0:
|
||||
self.failed()
|
||||
else:
|
||||
self.done()
|
||||
|
||||
self.stop()
|
||||
return
|
||||
|
||||
def abort(self):
|
||||
logger.debug('Aborting, sending SIGTERM to %d', self._popen.pid)
|
||||
os.kill(self._popen.pid, signal.SIGTERM)
|
||||
# self.stop()
|
||||
|
||||
def readbytesout(self, bytes):
|
||||
"""
|
||||
Called when bytes have been read from stdout.
|
||||
"""
|
||||
pass
|
||||
|
||||
def readbyteserr(self, bytes):
|
||||
"""
|
||||
Called when bytes have been read from stderr.
|
||||
"""
|
||||
pass
|
||||
|
||||
def done(self):
|
||||
"""
|
||||
Called when the command completed successfully.
|
||||
"""
|
||||
pass
|
||||
|
||||
def failed(self):
|
||||
"""
|
||||
Called when the command failed.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def commandMissing(self):
|
||||
"""
|
||||
Called when the command is missing.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
0
whipper/extern/__init__.py
vendored
Normal file
0
whipper/extern/__init__.py
vendored
Normal file
174
whipper/extern/asyncsub.py
vendored
Normal file
174
whipper/extern/asyncsub.py
vendored
Normal file
@@ -0,0 +1,174 @@
|
||||
# -*- Mode: Python -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
# from http://code.activestate.com/recipes/440554/
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import errno
|
||||
import time
|
||||
import sys
|
||||
|
||||
PIPE = subprocess.PIPE
|
||||
|
||||
if subprocess.mswindows:
|
||||
from win32file import ReadFile, WriteFile
|
||||
from win32pipe import PeekNamedPipe
|
||||
import msvcrt
|
||||
else:
|
||||
import select
|
||||
import fcntl
|
||||
|
||||
|
||||
class Popen(subprocess.Popen):
|
||||
|
||||
def recv(self, maxsize=None):
|
||||
return self._recv('stdout', maxsize)
|
||||
|
||||
def recv_err(self, maxsize=None):
|
||||
return self._recv('stderr', maxsize)
|
||||
|
||||
def send_recv(self, input='', maxsize=None):
|
||||
return self.send(input), self.recv(maxsize), self.recv_err(maxsize)
|
||||
|
||||
def get_conn_maxsize(self, which, maxsize):
|
||||
if maxsize is None:
|
||||
maxsize = 1024
|
||||
elif maxsize < 1:
|
||||
maxsize = 1
|
||||
return getattr(self, which), maxsize
|
||||
|
||||
def _close(self, which):
|
||||
getattr(self, which).close()
|
||||
setattr(self, which, None)
|
||||
|
||||
if subprocess.mswindows:
|
||||
|
||||
def send(self, input):
|
||||
if not self.stdin:
|
||||
return None
|
||||
|
||||
try:
|
||||
x = msvcrt.get_osfhandle(self.stdin.fileno())
|
||||
(errCode, written) = WriteFile(x, input)
|
||||
except ValueError:
|
||||
return self._close('stdin')
|
||||
except (subprocess.pywintypes.error, Exception), why:
|
||||
if why[0] in (109, errno.ESHUTDOWN):
|
||||
return self._close('stdin')
|
||||
raise
|
||||
|
||||
return written
|
||||
|
||||
def _recv(self, which, maxsize):
|
||||
conn, maxsize = self.get_conn_maxsize(which, maxsize)
|
||||
if conn is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
x = msvcrt.get_osfhandle(conn.fileno())
|
||||
(read, nAvail, nMessage) = PeekNamedPipe(x, 0)
|
||||
if maxsize < nAvail:
|
||||
nAvail = maxsize
|
||||
if nAvail > 0:
|
||||
(errCode, read) = ReadFile(x, nAvail, None)
|
||||
except ValueError:
|
||||
return self._close(which)
|
||||
except (subprocess.pywintypes.error, Exception), why:
|
||||
if why[0] in (109, errno.ESHUTDOWN):
|
||||
return self._close(which)
|
||||
raise
|
||||
|
||||
if self.universal_newlines:
|
||||
read = self._translate_newlines(read)
|
||||
return read
|
||||
|
||||
else:
|
||||
|
||||
def send(self, input):
|
||||
if not self.stdin:
|
||||
return None
|
||||
|
||||
if not select.select([], [self.stdin], [], 0)[1]:
|
||||
return 0
|
||||
|
||||
try:
|
||||
written = os.write(self.stdin.fileno(), input)
|
||||
except OSError, why:
|
||||
if why[0] == errno.EPIPE: #broken pipe
|
||||
return self._close('stdin')
|
||||
raise
|
||||
|
||||
return written
|
||||
|
||||
def _recv(self, which, maxsize):
|
||||
conn, maxsize = self.get_conn_maxsize(which, maxsize)
|
||||
if conn is None:
|
||||
return None
|
||||
|
||||
flags = fcntl.fcntl(conn, fcntl.F_GETFL)
|
||||
if not conn.closed:
|
||||
fcntl.fcntl(conn, fcntl.F_SETFL, flags| os.O_NONBLOCK)
|
||||
|
||||
try:
|
||||
if not select.select([conn], [], [], 0)[0]:
|
||||
return ''
|
||||
|
||||
r = conn.read(maxsize)
|
||||
if not r:
|
||||
return self._close(which)
|
||||
|
||||
if self.universal_newlines:
|
||||
r = self._translate_newlines(r)
|
||||
return r
|
||||
finally:
|
||||
if not conn.closed:
|
||||
fcntl.fcntl(conn, fcntl.F_SETFL, flags)
|
||||
|
||||
message = "Other end disconnected!"
|
||||
|
||||
|
||||
def recv_some(p, t=.1, e=1, tr=5, stderr=0):
|
||||
if tr < 1:
|
||||
tr = 1
|
||||
x = time.time()+t
|
||||
y = []
|
||||
r = ''
|
||||
pr = p.recv
|
||||
if stderr:
|
||||
pr = p.recv_err
|
||||
while time.time() < x or r:
|
||||
r = pr()
|
||||
if r is None:
|
||||
if e:
|
||||
raise Exception(message)
|
||||
else:
|
||||
break
|
||||
elif r:
|
||||
y.append(r)
|
||||
else:
|
||||
time.sleep(max((x-time.time())/tr, 0))
|
||||
return ''.join(y)
|
||||
|
||||
|
||||
def send_all(p, data):
|
||||
while len(data):
|
||||
sent = p.send(data)
|
||||
if sent is None:
|
||||
raise Exception(message)
|
||||
data = buffer(data, sent)
|
||||
|
||||
if __name__ == '__main__':
|
||||
if sys.platform == 'win32':
|
||||
shell, commands, tail = ('cmd', ('dir /w', 'echo HELLO WORLD'), '\r\n')
|
||||
else:
|
||||
shell, commands, tail = ('sh', ('ls', 'echo HELLO WORLD'), '\n')
|
||||
|
||||
a = Popen(shell, stdin=PIPE, stdout=PIPE)
|
||||
print recv_some(a),
|
||||
for cmd in commands:
|
||||
send_all(a, cmd + tail)
|
||||
print recv_some(a),
|
||||
send_all(a, 'exit' + tail)
|
||||
print recv_some(a, e=0)
|
||||
a.wait()
|
||||
55
whipper/extern/task/ChangeLog
vendored
Normal file
55
whipper/extern/task/ChangeLog
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
2012-11-18 Thomas Vander Stichele <thomas at apestaart dot org>
|
||||
|
||||
* gstreamer.py:
|
||||
Only set an exception once in bus_error_cb.
|
||||
Was triggered by morituri's checksum test, but only
|
||||
if multiple tests were run - got the same bus error
|
||||
twice.
|
||||
|
||||
2012-07-12 Thomas Vander Stichele <thomas at apestaart dot org>
|
||||
|
||||
* task.py:
|
||||
Add a debug statement.
|
||||
|
||||
2011-08-15 Thomas Vander Stichele <thomas at apestaart dot org>
|
||||
|
||||
* task.py:
|
||||
Better logging when scheduling.
|
||||
* gstreamer.py:
|
||||
If paused() returns True, don't go to playing.
|
||||
add a method for querying duration in the common case.
|
||||
|
||||
2011-08-08 Thomas Vander Stichele <thomas at apestaart dot org>
|
||||
|
||||
* task.py:
|
||||
Remove scrubFilename call.
|
||||
|
||||
2011-08-08 Thomas Vander Stichele <thomas at apestaart dot org>
|
||||
|
||||
* task.py:
|
||||
Pull in getExceptionMessage privately.
|
||||
|
||||
2011-08-05 Thomas Vander Stichele <thomas at apestaart dot org>
|
||||
|
||||
* gstreamer.py:
|
||||
* task.py:
|
||||
Don't rely on the log module; users that want to log
|
||||
should first subclass from a log class that implements
|
||||
warning/info/debug/log
|
||||
|
||||
2011-08-05 Thomas Vander Stichele <thomas at apestaart dot org>
|
||||
|
||||
* gstreamer.py:
|
||||
Document bus and pipeline. Make bus public.
|
||||
|
||||
2011-08-05 Thomas Vander Stichele <thomas at apestaart dot org>
|
||||
|
||||
* gstreamer.py:
|
||||
Add quoteParse() method.
|
||||
|
||||
2011-08-05 Thomas Vander Stichele <thomas at apestaart dot org>
|
||||
|
||||
* gstreamer.py:
|
||||
Add getPipeline() method.
|
||||
Base class implementation uses getPipelineDesc().
|
||||
|
||||
0
whipper/extern/task/__init__.py
vendored
Normal file
0
whipper/extern/task/__init__.py
vendored
Normal file
567
whipper/extern/task/task.py
vendored
Normal file
567
whipper/extern/task/task.py
vendored
Normal file
@@ -0,0 +1,567 @@
|
||||
# -*- 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
|
||||
|
||||
import gobject
|
||||
|
||||
class TaskException(Exception):
|
||||
"""
|
||||
I wrap an exception that happened during task execution.
|
||||
"""
|
||||
|
||||
exception = None # original exception
|
||||
|
||||
def __init__(self, exception, message=None):
|
||||
self.exception = exception
|
||||
self.exceptionMessage = message
|
||||
self.args = (exception, message, )
|
||||
|
||||
# lifted from flumotion log module
|
||||
def _getExceptionMessage(exception, frame=-1, filename=None):
|
||||
"""
|
||||
Return a short message based on an exception, useful for debugging.
|
||||
Tries to find where the exception was triggered.
|
||||
"""
|
||||
import traceback
|
||||
|
||||
stack = traceback.extract_tb(sys.exc_info()[2])
|
||||
if filename:
|
||||
stack = [f for f in stack if f[0].find(filename) > -1]
|
||||
|
||||
# badly raised exceptions can come without a stack
|
||||
if stack:
|
||||
(filename, line, func, text) = stack[frame]
|
||||
else:
|
||||
(filename, line, func, text) = ('no stack', 0, 'none', '')
|
||||
|
||||
exc = exception.__class__.__name__
|
||||
msg = ""
|
||||
# a shortcut to extract a useful message out of most exceptions
|
||||
# for now
|
||||
if str(exception):
|
||||
msg = ": %s" % str(exception)
|
||||
return "exception %(exc)s at %(filename)s:%(line)s: %(func)s()%(msg)s" \
|
||||
% locals()
|
||||
|
||||
|
||||
class LogStub(object):
|
||||
"""
|
||||
I am a stub for a log interface.
|
||||
"""
|
||||
|
||||
### log stubs
|
||||
def log(self, message, *args):
|
||||
pass
|
||||
|
||||
def debug(self, message, *args):
|
||||
pass
|
||||
|
||||
def info(self, message, *args):
|
||||
pass
|
||||
|
||||
def warning(self, message, *args):
|
||||
pass
|
||||
|
||||
def error(self, message, *args):
|
||||
pass
|
||||
|
||||
|
||||
class Task(LogStub):
|
||||
"""
|
||||
I wrap a task in an asynchronous interface.
|
||||
I can be listened to for starting, stopping, description changes
|
||||
and progress updates.
|
||||
|
||||
I communicate an error by setting self.exception to an exception and
|
||||
stopping myself from running.
|
||||
The listener can then handle the Task.exception.
|
||||
|
||||
@ivar description: what am I doing
|
||||
@ivar exception: set if an exception happened during the task
|
||||
execution. Will be raised through run() at the end.
|
||||
"""
|
||||
logCategory = 'Task'
|
||||
|
||||
description = 'I am doing something.'
|
||||
|
||||
progress = 0.0
|
||||
increment = 0.01
|
||||
running = False
|
||||
runner = None
|
||||
exception = None
|
||||
exceptionMessage = None
|
||||
exceptionTraceback = None
|
||||
|
||||
_listeners = None
|
||||
|
||||
|
||||
### subclass methods
|
||||
def start(self, runner):
|
||||
"""
|
||||
Start the task.
|
||||
|
||||
Subclasses should chain up to me at the beginning.
|
||||
|
||||
Subclass implementations should raise exceptions immediately in
|
||||
case of failure (using set(AndRaise)Exception) first, or do it later
|
||||
using those methods.
|
||||
|
||||
If start doesn't raise an exception, the task should run until
|
||||
complete, or setException and stop().
|
||||
"""
|
||||
self.debug('starting')
|
||||
self.setProgress(self.progress)
|
||||
self.running = True
|
||||
self.runner = runner
|
||||
self._notifyListeners('started')
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stop the task.
|
||||
Also resets the runner on the task.
|
||||
|
||||
Subclasses should chain up to me at the end.
|
||||
It is important that they do so in all cases, even when
|
||||
they ran into an exception of their own.
|
||||
|
||||
Listeners will get notified that the task is stopped,
|
||||
whether successfully or with an exception.
|
||||
"""
|
||||
self.debug('stopping')
|
||||
self.running = False
|
||||
if not self.runner:
|
||||
print 'ERROR: stopping task which is already stopped'
|
||||
import traceback; traceback.print_stack()
|
||||
self.runner = None
|
||||
self.debug('reset runner to None')
|
||||
self._notifyListeners('stopped')
|
||||
|
||||
### base class methods
|
||||
def setProgress(self, value):
|
||||
"""
|
||||
Notify about progress changes bigger than the increment.
|
||||
Called by subclass implementations as the task progresses.
|
||||
"""
|
||||
if value - self.progress > self.increment or value >= 1.0 or value == 0.0:
|
||||
self.progress = value
|
||||
self._notifyListeners('progressed', value)
|
||||
self.log('notifying progress: %r on %r', value, self.description)
|
||||
|
||||
def setDescription(self, description):
|
||||
if description != self.description:
|
||||
self._notifyListeners('described', description)
|
||||
self.description = description
|
||||
|
||||
# FIXME: unify?
|
||||
def setExceptionAndTraceback(self, exception):
|
||||
"""
|
||||
Call this to set a synthetically created exception (and not one
|
||||
that was actually raised and caught)
|
||||
"""
|
||||
import traceback
|
||||
|
||||
stack = traceback.extract_stack()[:-1]
|
||||
(filename, line, func, text) = stack[-1]
|
||||
exc = exception.__class__.__name__
|
||||
msg = ""
|
||||
# a shortcut to extract a useful message out of most exceptions
|
||||
# for now
|
||||
if str(exception):
|
||||
msg = ": %s" % str(exception)
|
||||
line = "exception %(exc)s at %(filename)s:%(line)s: %(func)s()%(msg)s" \
|
||||
% locals()
|
||||
|
||||
self.exception = exception
|
||||
self.exceptionMessage = line
|
||||
self.exceptionTraceback = traceback.format_exc()
|
||||
self.debug('set exception, %r' % self.exceptionMessage)
|
||||
# FIXME: remove
|
||||
setAndRaiseException = setExceptionAndTraceback
|
||||
|
||||
def setException(self, exception):
|
||||
"""
|
||||
Call this to set a caught exception on the task.
|
||||
"""
|
||||
import traceback
|
||||
|
||||
self.exception = exception
|
||||
self.exceptionMessage = _getExceptionMessage(exception)
|
||||
self.exceptionTraceback = traceback.format_exc()
|
||||
self.debug('set exception, %r, %r' % (
|
||||
exception, self.exceptionMessage))
|
||||
|
||||
def schedule(self, delta, callable, *args, **kwargs):
|
||||
if not self.runner:
|
||||
print "ERROR: scheduling on a task that's altready stopped"
|
||||
import traceback; traceback.print_stack()
|
||||
return
|
||||
self.runner.schedule(self, delta, callable, *args, **kwargs)
|
||||
|
||||
|
||||
def addListener(self, listener):
|
||||
"""
|
||||
Add a listener for task status changes.
|
||||
|
||||
Listeners should implement started, stopped, and progressed.
|
||||
"""
|
||||
self.debug('Adding listener %r', listener)
|
||||
if not self._listeners:
|
||||
self._listeners = []
|
||||
self._listeners.append(listener)
|
||||
|
||||
def _notifyListeners(self, methodName, *args, **kwargs):
|
||||
if self._listeners:
|
||||
for l in self._listeners:
|
||||
method = getattr(l, methodName)
|
||||
try:
|
||||
method(self, *args, **kwargs)
|
||||
except Exception, e:
|
||||
self.setException(e)
|
||||
|
||||
# FIXME: should this become a real interface, like in zope ?
|
||||
class ITaskListener(object):
|
||||
"""
|
||||
I am an interface for objects listening to tasks.
|
||||
"""
|
||||
### listener callbacks
|
||||
def progressed(self, task, value):
|
||||
"""
|
||||
Implement me to be informed about progress.
|
||||
|
||||
@type value: float
|
||||
@param value: progress, from 0.0 to 1.0
|
||||
"""
|
||||
|
||||
def described(self, task, description):
|
||||
"""
|
||||
Implement me to be informed about description changes.
|
||||
|
||||
@type description: str
|
||||
@param description: description
|
||||
"""
|
||||
|
||||
def started(self, task):
|
||||
"""
|
||||
Implement me to be informed about the task starting.
|
||||
"""
|
||||
|
||||
def stopped(self, task):
|
||||
"""
|
||||
Implement me to be informed about the task stopping.
|
||||
If the task had an error, task.exception will be set.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
# this is a Dummy task that can be used to test if this works at all
|
||||
class DummyTask(Task):
|
||||
def start(self, runner):
|
||||
Task.start(self, runner)
|
||||
self.schedule(1.0, self._wind)
|
||||
|
||||
def _wind(self):
|
||||
self.setProgress(min(self.progress + 0.1, 1.0))
|
||||
|
||||
if self.progress >= 1.0:
|
||||
self.stop()
|
||||
return
|
||||
|
||||
self.schedule(1.0, self._wind)
|
||||
|
||||
class BaseMultiTask(Task, ITaskListener):
|
||||
"""
|
||||
I perform multiple tasks.
|
||||
|
||||
@ivar tasks: the tasks to run
|
||||
@type tasks: list of L{Task}
|
||||
"""
|
||||
|
||||
description = 'Doing various tasks'
|
||||
tasks = None
|
||||
|
||||
def __init__(self):
|
||||
self.tasks = []
|
||||
self._task = 0
|
||||
|
||||
def addTask(self, task):
|
||||
"""
|
||||
Add a task.
|
||||
|
||||
@type task: L{Task}
|
||||
"""
|
||||
if self.tasks is None:
|
||||
self.tasks = []
|
||||
self.tasks.append(task)
|
||||
|
||||
def start(self, runner):
|
||||
"""
|
||||
Start tasks.
|
||||
|
||||
Tasks can still be added while running. For example,
|
||||
a first task can determine how many additional tasks to run.
|
||||
"""
|
||||
Task.start(self, runner)
|
||||
|
||||
# initialize task tracking
|
||||
if not self.tasks:
|
||||
self.warning('no tasks')
|
||||
self._generic = self.description
|
||||
|
||||
self.next()
|
||||
|
||||
def next(self):
|
||||
"""
|
||||
Start the next task.
|
||||
"""
|
||||
try:
|
||||
# start next task
|
||||
task = self.tasks[self._task]
|
||||
self._task += 1
|
||||
self.debug('BaseMultiTask.next(): starting task %d of %d: %r',
|
||||
self._task, len(self.tasks), task)
|
||||
self.setDescription("%s (%d of %d) ..." % (
|
||||
task.description, self._task, len(self.tasks)))
|
||||
task.addListener(self)
|
||||
task.start(self.runner)
|
||||
self.debug('BaseMultiTask.next(): started task %d of %d: %r',
|
||||
self._task, len(self.tasks), task)
|
||||
except Exception, e:
|
||||
self.setException(e)
|
||||
self.debug('Got exception during next: %r', self.exceptionMessage)
|
||||
self.stop()
|
||||
return
|
||||
|
||||
### ITaskListener methods
|
||||
def started(self, task):
|
||||
pass
|
||||
|
||||
def progressed(self, task, value):
|
||||
pass
|
||||
|
||||
def stopped(self, task):
|
||||
"""
|
||||
Subclasses should chain up to me at the end of their implementation.
|
||||
They should fall through to chaining up if there is an exception.
|
||||
"""
|
||||
self.log('BaseMultiTask.stopped: task %r (%d of %d)',
|
||||
task, self.tasks.index(task) + 1, len(self.tasks))
|
||||
if task.exception:
|
||||
self.log('BaseMultiTask.stopped: exception %r',
|
||||
task.exceptionMessage)
|
||||
self.exception = task.exception
|
||||
self.exceptionMessage = task.exceptionMessage
|
||||
self.stop()
|
||||
return
|
||||
|
||||
if self._task == len(self.tasks):
|
||||
self.log('BaseMultiTask.stopped: all tasks done')
|
||||
self.stop()
|
||||
return
|
||||
|
||||
# pick another
|
||||
self.log('BaseMultiTask.stopped: pick next task')
|
||||
self.schedule(0, self.next)
|
||||
|
||||
|
||||
class MultiSeparateTask(BaseMultiTask):
|
||||
"""
|
||||
I perform multiple tasks.
|
||||
I track progress of each individual task, going back to 0 for each task.
|
||||
"""
|
||||
description = 'Doing various tasks separately'
|
||||
|
||||
def start(self, runner):
|
||||
self.debug('MultiSeparateTask.start()')
|
||||
BaseMultiTask.start(self, runner)
|
||||
|
||||
def next(self):
|
||||
self.debug('MultiSeparateTask.next()')
|
||||
# start next task
|
||||
self.progress = 0.0 # reset progress for each task
|
||||
BaseMultiTask.next(self)
|
||||
|
||||
### ITaskListener methods
|
||||
def progressed(self, task, value):
|
||||
self.setProgress(value)
|
||||
|
||||
def described(self, description):
|
||||
self.setDescription("%s (%d of %d) ..." % (
|
||||
description, self._task, len(self.tasks)))
|
||||
|
||||
class MultiCombinedTask(BaseMultiTask):
|
||||
"""
|
||||
I perform multiple tasks.
|
||||
I track progress as a combined progress on all tasks on task granularity.
|
||||
"""
|
||||
|
||||
description = 'Doing various tasks combined'
|
||||
_stopped = 0
|
||||
|
||||
### ITaskListener methods
|
||||
def progressed(self, task, value):
|
||||
self.setProgress(float(self._stopped + value) / len(self.tasks))
|
||||
|
||||
def stopped(self, task):
|
||||
self._stopped += 1
|
||||
self.setProgress(float(self._stopped) / len(self.tasks))
|
||||
BaseMultiTask.stopped(self, task)
|
||||
|
||||
class TaskRunner(LogStub):
|
||||
"""
|
||||
I am a base class for task runners.
|
||||
Task runners should be reusable.
|
||||
"""
|
||||
logCategory = 'TaskRunner'
|
||||
|
||||
def run(self, task):
|
||||
"""
|
||||
Run the given task.
|
||||
|
||||
@type task: Task
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
### methods for tasks to call
|
||||
def schedule(self, delta, callable, *args, **kwargs):
|
||||
"""
|
||||
Schedule a single future call.
|
||||
|
||||
Subclasses should implement this.
|
||||
|
||||
@type delta: float
|
||||
@param delta: time in the future to schedule call for, in seconds.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SyncRunner(TaskRunner, ITaskListener):
|
||||
"""
|
||||
I run the task synchronously in a gobject MainLoop.
|
||||
"""
|
||||
def __init__(self, verbose=True):
|
||||
self._verbose = verbose
|
||||
self._longest = 0 # longest string shown; for clearing
|
||||
|
||||
def run(self, task, verbose=None, skip=False):
|
||||
self.debug('run task %r', task)
|
||||
self._task = task
|
||||
self._verboseRun = self._verbose
|
||||
if verbose is not None:
|
||||
self._verboseRun = verbose
|
||||
self._skip = skip
|
||||
|
||||
self._loop = gobject.MainLoop()
|
||||
self._task.addListener(self)
|
||||
# only start the task after going into the mainloop,
|
||||
# otherwise the task might complete before we are in it
|
||||
gobject.timeout_add(0L, self._startWrap, self._task)
|
||||
self.debug('run loop')
|
||||
self._loop.run()
|
||||
|
||||
self.debug('done running task %r', task)
|
||||
if task.exception:
|
||||
# catch the exception message
|
||||
# FIXME: this gave a traceback in the logging module
|
||||
self.debug('raising TaskException for %r, %r' % (
|
||||
task.exceptionMessage, task.exceptionTraceback))
|
||||
msg = task.exceptionMessage
|
||||
if task.exceptionTraceback:
|
||||
msg += "\n" + task.exceptionTraceback
|
||||
raise TaskException(task.exception, message=msg)
|
||||
|
||||
def _startWrap(self, task):
|
||||
# wrap task start such that we can report any exceptions and
|
||||
# never hang
|
||||
try:
|
||||
self.debug('start task %r' % task)
|
||||
task.start(self)
|
||||
except Exception, e:
|
||||
# getExceptionMessage uses global exception state that doesn't
|
||||
# hang around, so store the message
|
||||
task.setException(e)
|
||||
self.debug('exception during start: %r', task.exceptionMessage)
|
||||
self.stopped(task)
|
||||
|
||||
|
||||
def schedule(self, task, delta, callable, *args, **kwargs):
|
||||
def c():
|
||||
try:
|
||||
self.log('schedule: calling %r(*args=%r, **kwargs=%r)',
|
||||
callable, args, kwargs)
|
||||
callable(*args, **kwargs)
|
||||
return False
|
||||
except Exception, e:
|
||||
self.debug('exception when calling scheduled callable %r',
|
||||
callable)
|
||||
task.setException(e)
|
||||
self.stopped(task)
|
||||
raise
|
||||
self.log('schedule: scheduling %r(*args=%r, **kwargs=%r)',
|
||||
callable, args, kwargs)
|
||||
|
||||
gobject.timeout_add(int(delta * 1000L), c)
|
||||
|
||||
### ITaskListener methods
|
||||
def progressed(self, task, value):
|
||||
if not self._verboseRun:
|
||||
return
|
||||
|
||||
self._report()
|
||||
|
||||
if value >= 1.0:
|
||||
if self._skip:
|
||||
self._output('%s %3d %%' % (
|
||||
self._task.description, 100.0))
|
||||
else:
|
||||
# clear with whitespace
|
||||
sys.stdout.write("%s\r" % (' ' * self._longest, ))
|
||||
|
||||
def _output(self, what, newline=False, ret=True):
|
||||
sys.stdout.write(what)
|
||||
sys.stdout.write(' ' * (self._longest - len(what)))
|
||||
if ret:
|
||||
sys.stdout.write('\r')
|
||||
if newline:
|
||||
sys.stdout.write('\n')
|
||||
sys.stdout.flush()
|
||||
if len(what) > self._longest:
|
||||
#print; print 'setting longest', self._longest; print
|
||||
self._longest = len(what)
|
||||
|
||||
def described(self, task, description):
|
||||
if self._verboseRun:
|
||||
self._report()
|
||||
|
||||
def stopped(self, task):
|
||||
self.debug('stopped task %r', task)
|
||||
self.progressed(task, 1.0)
|
||||
self._loop.quit()
|
||||
|
||||
def _report(self):
|
||||
self._output('%s %3d %%' % (
|
||||
self._task.description, self._task.progress * 100.0))
|
||||
|
||||
if __name__ == '__main__':
|
||||
task = DummyTask()
|
||||
runner = SyncRunner()
|
||||
runner.run(task)
|
||||
0
whipper/image/__init__.py
Normal file
0
whipper/image/__init__.py
Normal file
207
whipper/image/cue.py
Normal file
207
whipper/image/cue.py
Normal file
@@ -0,0 +1,207 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_image_cue -*-
|
||||
# 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/>.
|
||||
|
||||
"""
|
||||
Reading .cue files
|
||||
|
||||
See http://digitalx.org/cuesheetsyntax.php
|
||||
"""
|
||||
|
||||
import re
|
||||
import codecs
|
||||
|
||||
from whipper.common import common
|
||||
from whipper.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(.*)$")
|
||||
|
||||
_FILE_RE = re.compile(r"""
|
||||
^FILE # FILE
|
||||
\s+"(?P<name>.*)" # 'file name' in quotes
|
||||
\s+(?P<format>\w+)$ # format (WAVE/MP3/AIFF/...)
|
||||
""", re.VERBOSE)
|
||||
|
||||
_TRACK_RE = re.compile(r"""
|
||||
^\s+TRACK # TRACK
|
||||
\s+(?P<track>\d\d) # two-digit track number
|
||||
\s+(?P<mode>.+)$ # mode (AUDIO, MODEx/2xxx, ...)
|
||||
""", re.VERBOSE)
|
||||
|
||||
_INDEX_RE = re.compile(r"""
|
||||
^\s+INDEX # INDEX
|
||||
\s+(\d\d) # two-digit index number
|
||||
\s+(\d\d) # minutes
|
||||
:(\d\d) # seconds
|
||||
:(\d\d)$ # frames
|
||||
""", re.VERBOSE)
|
||||
|
||||
|
||||
class CueFile(object):
|
||||
"""
|
||||
I represent a .cue file as an object.
|
||||
|
||||
@type table: L{table.Table}
|
||||
@ivar table: the index table.
|
||||
"""
|
||||
logCategory = 'CueFile'
|
||||
|
||||
def __init__(self, path):
|
||||
"""
|
||||
@type path: unicode
|
||||
"""
|
||||
assert type(path) is unicode, "%r is not unicode" % path
|
||||
|
||||
self._path = path
|
||||
self._rems = {}
|
||||
self._messages = []
|
||||
self.leadout = None
|
||||
self.table = table.Table()
|
||||
|
||||
def parse(self):
|
||||
state = 'HEADER'
|
||||
currentFile = None
|
||||
currentTrack = None
|
||||
counter = 0
|
||||
|
||||
logger.info('Parsing .cue file %r', self._path)
|
||||
handle = codecs.open(self._path, 'r', 'utf-8')
|
||||
|
||||
for number, line in enumerate(handle.readlines()):
|
||||
line = line.rstrip()
|
||||
|
||||
m = _REM_RE.search(line)
|
||||
if m:
|
||||
tag = m.expand('\\1')
|
||||
value = m.expand('\\2')
|
||||
if state != 'HEADER':
|
||||
self.message(number, 'REM %s outside of header' % tag)
|
||||
else:
|
||||
self._rems[tag] = value
|
||||
continue
|
||||
|
||||
# look for FILE lines
|
||||
m = _FILE_RE.search(line)
|
||||
if m:
|
||||
counter += 1
|
||||
filePath = m.group('name')
|
||||
fileFormat = m.group('format')
|
||||
currentFile = File(filePath, fileFormat)
|
||||
|
||||
# look for TRACK lines
|
||||
m = _TRACK_RE.search(line)
|
||||
if m:
|
||||
if not currentFile:
|
||||
self.message(number, 'TRACK without preceding FILE')
|
||||
continue
|
||||
|
||||
state = 'TRACK'
|
||||
|
||||
trackNumber = int(m.group('track'))
|
||||
#trackMode = m.group('mode')
|
||||
|
||||
logger.debug('found track %d', trackNumber)
|
||||
currentTrack = table.Track(trackNumber)
|
||||
self.table.tracks.append(currentTrack)
|
||||
continue
|
||||
|
||||
# look for INDEX lines
|
||||
m = _INDEX_RE.search(line)
|
||||
if m:
|
||||
if not currentTrack:
|
||||
self.message(number, 'INDEX without preceding TRACK')
|
||||
print 'ouch'
|
||||
continue
|
||||
|
||||
indexNumber = int(m.expand('\\1'))
|
||||
minutes = int(m.expand('\\2'))
|
||||
seconds = int(m.expand('\\3'))
|
||||
frames = int(m.expand('\\4'))
|
||||
frameOffset = frames \
|
||||
+ seconds * common.FRAMES_PER_SECOND \
|
||||
+ minutes * common.FRAMES_PER_SECOND * 60
|
||||
|
||||
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,
|
||||
path=currentFile.path, relative=frameOffset,
|
||||
counter=counter)
|
||||
continue
|
||||
|
||||
def message(self, number, message):
|
||||
"""
|
||||
Add a message about a given line in the cue file.
|
||||
|
||||
@param number: line number, counting from 0.
|
||||
"""
|
||||
self._messages.append((number + 1, message))
|
||||
|
||||
def getTrackLength(self, track):
|
||||
# returns track length in frames, or -1 if can't be determined and
|
||||
# complete file should be assumed
|
||||
# FIXME: this assumes a track can only be in one file; is this true ?
|
||||
i = self.table.tracks.index(track)
|
||||
if i == len(self.table.tracks) - 1:
|
||||
# last track, so no length known
|
||||
return -1
|
||||
|
||||
thisIndex = track.indexes[1] # FIXME: could be more
|
||||
nextIndex = self.table.tracks[i + 1].indexes[1] # FIXME: could be 0
|
||||
|
||||
c = thisIndex.counter
|
||||
if c is not None and c == nextIndex.counter:
|
||||
# they belong to the same source, so their relative delta is length
|
||||
return nextIndex.relative - thisIndex.relative
|
||||
|
||||
# FIXME: more logic
|
||||
return -1
|
||||
|
||||
def getRealPath(self, path):
|
||||
"""
|
||||
Translate the .cue's FILE to an existing path.
|
||||
|
||||
@type path: unicode
|
||||
"""
|
||||
return common.getRealPath(self._path, path)
|
||||
|
||||
|
||||
class File:
|
||||
"""
|
||||
I represent a FILE line in a cue file.
|
||||
"""
|
||||
|
||||
def __init__(self, path, format):
|
||||
"""
|
||||
@type path: unicode
|
||||
"""
|
||||
assert type(path) is unicode, "%r is not unicode" % path
|
||||
|
||||
self.path = path
|
||||
self.format = format
|
||||
|
||||
def __repr__(self):
|
||||
return '<File %r of format %s>' % (self.path, self.format)
|
||||
255
whipper/image/image.py
Normal file
255
whipper/image/image.py
Normal file
@@ -0,0 +1,255 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_image_image -*-
|
||||
# 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/>.
|
||||
|
||||
"""
|
||||
Wrap on-disk CD images based on the .cue file.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from whipper.common import encode
|
||||
from whipper.common import common
|
||||
from whipper.common import checksum
|
||||
from whipper.image import cue, table
|
||||
from whipper.extern.task import task
|
||||
from whipper.program.soxi import AudioLengthTask
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Image(object):
|
||||
"""
|
||||
@ivar table: The Table of Contents for this image.
|
||||
@type table: L{table.Table}
|
||||
"""
|
||||
logCategory = 'Image'
|
||||
|
||||
def __init__(self, path):
|
||||
"""
|
||||
@type path: unicode
|
||||
@param path: .cue path
|
||||
"""
|
||||
assert type(path) is unicode, "%r is not unicode" % path
|
||||
|
||||
self._path = path
|
||||
self.cue = cue.CueFile(path)
|
||||
self.cue.parse()
|
||||
self._offsets = [] # 0 .. trackCount - 1
|
||||
self._lengths = [] # 0 .. trackCount - 1
|
||||
|
||||
self.table = None
|
||||
|
||||
def getRealPath(self, path):
|
||||
"""
|
||||
Translate the .cue's FILE to an existing path.
|
||||
|
||||
@param path: .cue path
|
||||
"""
|
||||
assert type(path) is unicode, "%r is not unicode" % path
|
||||
|
||||
return self.cue.getRealPath(path)
|
||||
|
||||
def setup(self, runner):
|
||||
"""
|
||||
Do initial setup, like figuring out track lengths, and
|
||||
constructing the Table of Contents.
|
||||
"""
|
||||
logger.debug('setup image start')
|
||||
verify = ImageVerifyTask(self)
|
||||
logger.debug('verifying image')
|
||||
runner.run(verify)
|
||||
logger.debug('verified image')
|
||||
|
||||
# calculate offset and length for each track
|
||||
|
||||
# CD's have a standard lead-in time of 2 seconds;
|
||||
# checksums that use it should add it there
|
||||
if verify.lengths.has_key(0):
|
||||
offset = verify.lengths[0]
|
||||
else:
|
||||
offset = self.cue.table.tracks[0].getIndex(1).relative
|
||||
|
||||
tracks = []
|
||||
|
||||
for i in range(len(self.cue.table.tracks)):
|
||||
length = self.cue.getTrackLength(self.cue.table.tracks[i])
|
||||
if length == -1:
|
||||
length = verify.lengths[i + 1]
|
||||
t = table.Track(i + 1, audio=True)
|
||||
tracks.append(t)
|
||||
# FIXME: this probably only works for non-compliant .CUE files
|
||||
# where pregap is put at end of previous file
|
||||
t.index(1, absolute=offset,
|
||||
path=self.cue.table.tracks[i].getIndex(1).path,
|
||||
relative=0)
|
||||
|
||||
offset += length
|
||||
|
||||
self.table = table.Table(tracks)
|
||||
self.table.leadout = offset
|
||||
logger.debug('setup image done')
|
||||
|
||||
|
||||
class AccurateRipChecksumTask(task.MultiSeparateTask):
|
||||
"""
|
||||
I calculate the AccurateRip checksums of all tracks.
|
||||
"""
|
||||
|
||||
description = "Checksumming tracks"
|
||||
|
||||
# TODO MW: Update this further for V2 code
|
||||
def __init__(self, image):
|
||||
task.MultiSeparateTask.__init__(self)
|
||||
|
||||
self._image = image
|
||||
cue = image.cue
|
||||
self.checksums = []
|
||||
|
||||
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:
|
||||
logger.debug('track %d has unknown length' % (trackIndex + 1, ))
|
||||
else:
|
||||
logger.debug('track %d is %d samples long' % (
|
||||
trackIndex + 1, length))
|
||||
|
||||
path = image.getRealPath(index.path)
|
||||
|
||||
|
||||
checksumTask = checksum.FastAccurateRipChecksumTask(path,
|
||||
trackNumber=trackIndex + 1, trackCount=len(cue.table.tracks),
|
||||
wave=True, v2=False)
|
||||
|
||||
self.addTask(checksumTask)
|
||||
|
||||
def stop(self):
|
||||
self.checksums = [t.checksum for t in self.tasks]
|
||||
task.MultiSeparateTask.stop(self)
|
||||
|
||||
|
||||
class ImageVerifyTask(task.MultiSeparateTask):
|
||||
"""
|
||||
I verify a disk image and get the necessary track lengths.
|
||||
"""
|
||||
|
||||
logCategory = 'ImageVerifyTask'
|
||||
|
||||
description = "Checking tracks"
|
||||
lengths = None
|
||||
|
||||
def __init__(self, image):
|
||||
task.MultiSeparateTask.__init__(self)
|
||||
|
||||
self._image = image
|
||||
cue = image.cue
|
||||
self._tasks = []
|
||||
self.lengths = {}
|
||||
|
||||
try:
|
||||
htoa = cue.table.tracks[0].indexes[0]
|
||||
track = cue.table.tracks[0]
|
||||
path = image.getRealPath(htoa.path)
|
||||
assert type(path) is unicode, "%r is not unicode" % 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):
|
||||
logger.debug('no htoa track')
|
||||
|
||||
for trackIndex, track in enumerate(cue.table.tracks):
|
||||
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
|
||||
logger.debug('schedule scan of audio length of %r', path)
|
||||
taskk = AudioLengthTask(path)
|
||||
self.addTask(taskk)
|
||||
self._tasks.append((trackIndex + 1, track, taskk))
|
||||
else:
|
||||
logger.debug('track %d has length %d', trackIndex + 1, length)
|
||||
|
||||
def stop(self):
|
||||
for trackIndex, track, taskk in self._tasks:
|
||||
if taskk.exception:
|
||||
logger.debug('subtask %r had exception %r, shutting down' % (
|
||||
taskk, taskk.exception))
|
||||
self.setException(taskk.exception)
|
||||
break
|
||||
|
||||
if taskk.length is None:
|
||||
raise ValueError("Track length was not found; look for "
|
||||
"earlier errors in debug log (set RIP_DEBUG=4)")
|
||||
# print '%d has length %d' % (trackIndex, taskk.length)
|
||||
index = track.indexes[1]
|
||||
assert taskk.length % common.SAMPLES_PER_FRAME == 0
|
||||
end = taskk.length / common.SAMPLES_PER_FRAME
|
||||
self.lengths[trackIndex] = end - index.relative
|
||||
|
||||
task.MultiSeparateTask.stop(self)
|
||||
|
||||
|
||||
class ImageEncodeTask(task.MultiSeparateTask):
|
||||
"""
|
||||
I encode a disk image to a different format.
|
||||
"""
|
||||
|
||||
description = "Encoding tracks"
|
||||
|
||||
def __init__(self, image, outdir):
|
||||
task.MultiSeparateTask.__init__(self)
|
||||
|
||||
self._image = image
|
||||
cue = image.cue
|
||||
self._tasks = []
|
||||
self.lengths = {}
|
||||
|
||||
def add(index):
|
||||
|
||||
path = image.getRealPath(index.path)
|
||||
assert type(path) is unicode, "%r is not unicode" % path
|
||||
logger.debug('schedule encode of %r', path)
|
||||
root, ext = os.path.splitext(os.path.basename(path))
|
||||
outpath = os.path.join(outdir, root + '.' + 'flac')
|
||||
logger.debug('schedule encode to %r', outpath)
|
||||
taskk = encode.FlacEncodeTask(path, os.path.join(outdir,
|
||||
root + '.' + 'flac'))
|
||||
self.addTask(taskk)
|
||||
|
||||
try:
|
||||
htoa = cue.table.tracks[0].indexes[0]
|
||||
logger.debug('encoding htoa track')
|
||||
add(htoa)
|
||||
except (KeyError, IndexError):
|
||||
logger.debug('no htoa track')
|
||||
pass
|
||||
|
||||
for trackIndex, track in enumerate(cue.table.tracks):
|
||||
logger.debug('encoding track %d', trackIndex + 1)
|
||||
index = track.indexes[1]
|
||||
add(index)
|
||||
871
whipper/image/table.py
Normal file
871
whipper/image/table.py
Normal file
@@ -0,0 +1,871 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_image_table -*-
|
||||
# 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/>.
|
||||
|
||||
"""
|
||||
Wrap Table of Contents.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import urllib
|
||||
import urlparse
|
||||
|
||||
import whipper
|
||||
|
||||
from whipper.common import common
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# FIXME: taken from libcdio, but no reference found for these
|
||||
|
||||
CDTEXT_FIELDS = [
|
||||
'ARRANGER',
|
||||
'COMPOSER',
|
||||
'DISCID',
|
||||
'GENRE',
|
||||
'MESSAGE',
|
||||
'ISRC',
|
||||
'PERFORMER',
|
||||
'SIZE_INFO',
|
||||
'SONGWRITER',
|
||||
'TITLE',
|
||||
'TOC_INFO',
|
||||
'TOC_INFO2',
|
||||
'UPC_EAN',
|
||||
]
|
||||
|
||||
|
||||
class Track:
|
||||
"""
|
||||
I represent a track entry in an Table.
|
||||
|
||||
@ivar number: track number (1-based)
|
||||
@type number: int
|
||||
@ivar audio: whether the track is audio
|
||||
@type audio: bool
|
||||
@type indexes: dict of number -> L{Index}
|
||||
@ivar isrc: ISRC code (12 alphanumeric characters)
|
||||
@type isrc: str
|
||||
@ivar cdtext: dictionary of CD Text information; see L{CDTEXT_KEYS}.
|
||||
@type cdtext: str -> unicode
|
||||
@ivar pre_emphasis: whether track is pre-emphasised
|
||||
@type pre_emphasis: bool
|
||||
"""
|
||||
|
||||
number = None
|
||||
audio = None
|
||||
indexes = None
|
||||
isrc = None
|
||||
cdtext = None
|
||||
session = None
|
||||
pre_emphasis = None
|
||||
|
||||
def __repr__(self):
|
||||
return '<Track %02d>' % self.number
|
||||
|
||||
def __init__(self, number, audio=True, session=None):
|
||||
self.number = number
|
||||
self.audio = audio
|
||||
self.indexes = {}
|
||||
self.cdtext = {}
|
||||
|
||||
def index(self, number, absolute=None, path=None, relative=None,
|
||||
counter=None):
|
||||
"""
|
||||
@type path: unicode or None
|
||||
"""
|
||||
if path is not None:
|
||||
assert type(path) is unicode, "%r is not unicode" % path
|
||||
|
||||
i = Index(number, absolute, path, relative, counter)
|
||||
self.indexes[number] = i
|
||||
|
||||
def getIndex(self, number):
|
||||
return self.indexes[number]
|
||||
|
||||
def getFirstIndex(self):
|
||||
"""
|
||||
Get the first chronological index for this track.
|
||||
|
||||
Typically this is INDEX 01; but it could be INDEX 00 if there's
|
||||
a pre-gap.
|
||||
"""
|
||||
indexes = self.indexes.keys()
|
||||
indexes.sort()
|
||||
return self.indexes[indexes[0]]
|
||||
|
||||
def getLastIndex(self):
|
||||
indexes = self.indexes.keys()
|
||||
indexes.sort()
|
||||
return self.indexes[indexes[-1]]
|
||||
|
||||
def getPregap(self):
|
||||
"""
|
||||
Returns the length of the pregap for this track.
|
||||
|
||||
The pregap is 0 if there is no index 0, and the difference between
|
||||
index 1 and index 0 if there is.
|
||||
"""
|
||||
if 0 not in self.indexes:
|
||||
return 0
|
||||
|
||||
return self.indexes[1].absolute - self.indexes[0].absolute
|
||||
|
||||
|
||||
class Index:
|
||||
"""
|
||||
@ivar counter: counter for the index source; distinguishes between
|
||||
the matching FILE lines in .cue files for example
|
||||
@type path: unicode or None
|
||||
"""
|
||||
number = None
|
||||
absolute = None
|
||||
path = None
|
||||
relative = None
|
||||
counter = None
|
||||
|
||||
def __init__(self, number, absolute=None, path=None, relative=None,
|
||||
counter=None):
|
||||
|
||||
if path is not None:
|
||||
assert type(path) is unicode, "%r is not unicode" % path
|
||||
|
||||
self.number = number
|
||||
self.absolute = absolute
|
||||
self.path = path
|
||||
self.relative = relative
|
||||
self.counter = counter
|
||||
|
||||
def __repr__(self):
|
||||
return '<Index %02d absolute %r path %r relative %r counter %r>' % (
|
||||
self.number, self.absolute, self.path, self.relative, self.counter)
|
||||
|
||||
|
||||
class Table(object):
|
||||
"""
|
||||
I represent a table of indexes on a CD.
|
||||
|
||||
@ivar tracks: tracks on this CD
|
||||
@type tracks: list of L{Track}
|
||||
@ivar catalog: catalog number
|
||||
@type catalog: str
|
||||
@type cdtext: dict of str -> str
|
||||
"""
|
||||
|
||||
tracks = None # list of Track
|
||||
leadout = None # offset where the leadout starts
|
||||
catalog = None # catalog number; FIXME: is this UPC ?
|
||||
cdtext = None
|
||||
mbdiscid = None
|
||||
|
||||
classVersion = 4
|
||||
|
||||
def __init__(self, tracks=None):
|
||||
if not tracks:
|
||||
tracks = []
|
||||
|
||||
self.tracks = tracks
|
||||
self.cdtext = {}
|
||||
# done this way because just having a class-defined instance var
|
||||
# gets overridden when unpickling
|
||||
self.instanceVersion = self.classVersion
|
||||
self.unpickled()
|
||||
|
||||
def unpickled(self):
|
||||
self.logName = "Table 0x%08x v%d" % (id(self), self.instanceVersion)
|
||||
logger.debug('set logName')
|
||||
|
||||
def getTrackStart(self, number):
|
||||
"""
|
||||
@param number: the track number, 1-based
|
||||
@type number: int
|
||||
|
||||
@returns: the start of the given track number's index 1, in CD frames
|
||||
@rtype: int
|
||||
"""
|
||||
track = self.tracks[number - 1]
|
||||
return track.getIndex(1).absolute
|
||||
|
||||
def getTrackEnd(self, number):
|
||||
"""
|
||||
@param number: the track number, 1-based
|
||||
@type number: int
|
||||
|
||||
@returns: the end of the given track number (ie index 1 of next track)
|
||||
@rtype: int
|
||||
"""
|
||||
# default to end of disc
|
||||
end = self.leadout - 1
|
||||
|
||||
# if not last track, calculate it from the next track
|
||||
if number < len(self.tracks):
|
||||
end = self.tracks[number].getIndex(1).absolute - 1
|
||||
|
||||
# if on a session border, subtract the session leadin
|
||||
thisTrack = self.tracks[number - 1]
|
||||
nextTrack = self.tracks[number]
|
||||
if nextTrack.session > thisTrack.session:
|
||||
gap = self._getSessionGap(nextTrack.session)
|
||||
end -= gap
|
||||
|
||||
return end
|
||||
|
||||
def getTrackLength(self, number):
|
||||
"""
|
||||
@param number: the track number, 1-based
|
||||
@type number: int
|
||||
|
||||
@returns: the length of the given track number, in CD frames
|
||||
@rtype: int
|
||||
"""
|
||||
return self.getTrackEnd(number) - self.getTrackStart(number) + 1
|
||||
|
||||
def getAudioTracks(self):
|
||||
"""
|
||||
@returns: the number of audio tracks on the CD
|
||||
@rtype: int
|
||||
"""
|
||||
return len([t for t in self.tracks if t.audio])
|
||||
|
||||
def hasDataTracks(self):
|
||||
"""
|
||||
@returns: whether this disc contains data tracks
|
||||
"""
|
||||
return len([t for t in self.tracks if not t.audio]) > 0
|
||||
|
||||
def _cddbSum(self, i):
|
||||
ret = 0
|
||||
while i > 0:
|
||||
ret += (i % 10)
|
||||
i /= 10
|
||||
|
||||
return ret
|
||||
|
||||
def getCDDBValues(self):
|
||||
"""
|
||||
Get all CDDB values needed to calculate disc id and lookup URL.
|
||||
|
||||
This includes:
|
||||
- CDDB disc id
|
||||
- number of audio tracks
|
||||
- offset of index 1 of each track
|
||||
- length of disc in seconds (including data track)
|
||||
|
||||
@rtype: list of int
|
||||
"""
|
||||
result = []
|
||||
|
||||
result.append(self.getAudioTracks())
|
||||
|
||||
# cddb disc id takes into account data tracks
|
||||
# last byte is the number of tracks on the CD
|
||||
n = 0
|
||||
|
||||
# CD's have a standard lead-in time of 2 seconds
|
||||
# which gets added for CDDB disc id's
|
||||
delta = 2 * common.FRAMES_PER_SECOND
|
||||
#if self.getTrackStart(1) > 0:
|
||||
# delta = 0
|
||||
|
||||
debug = [str(len(self.tracks))]
|
||||
for track in self.tracks:
|
||||
offset = self.getTrackStart(track.number) + delta
|
||||
result.append(offset)
|
||||
debug.append(str(offset))
|
||||
seconds = offset / common.FRAMES_PER_SECOND
|
||||
n += self._cddbSum(seconds)
|
||||
|
||||
# the 'real' leadout, not offset by 150 frames
|
||||
# print 'THOMAS: disc leadout', self.leadout
|
||||
last = self.tracks[-1]
|
||||
leadout = self.getTrackEnd(last.number) + 1
|
||||
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
|
||||
# before making the difference
|
||||
startSeconds = self.getTrackStart(1) / common.FRAMES_PER_SECOND
|
||||
leadoutSeconds = leadout / common.FRAMES_PER_SECOND
|
||||
t = leadoutSeconds - startSeconds
|
||||
# durationFrames = self.getFrameLength(data=True)
|
||||
# duration = durationFrames / common.FRAMES_PER_SECOND
|
||||
# assert t == duration, "%r != %r" % (t, duration)
|
||||
|
||||
debug.append(str(leadoutSeconds + 2)) # 2 is the 150 frame cddb offset
|
||||
result.append(leadoutSeconds)
|
||||
|
||||
value = (n % 0xff) << 24 | t << 8 | len(self.tracks)
|
||||
result.insert(0, value)
|
||||
|
||||
# compare this debug line to cd-discid output
|
||||
logger.debug('cddb values: %r', result)
|
||||
|
||||
logger.debug('cddb disc id debug: %s',
|
||||
" ".join(["%08x" % value, ] + debug))
|
||||
|
||||
return result
|
||||
|
||||
def getCDDBDiscId(self):
|
||||
"""
|
||||
Calculate the CDDB disc ID.
|
||||
|
||||
@rtype: str
|
||||
@returns: the 8-character hexadecimal disc ID
|
||||
"""
|
||||
values = self.getCDDBValues()
|
||||
return "%08x" % values[0]
|
||||
|
||||
def getMusicBrainzDiscId(self):
|
||||
"""
|
||||
Calculate the MusicBrainz disc ID.
|
||||
|
||||
@rtype: str
|
||||
@returns: the 28-character base64-encoded disc ID
|
||||
"""
|
||||
if self.mbdiscid:
|
||||
logger.debug('getMusicBrainzDiscId: returning cached %r'
|
||||
% self.mbdiscid)
|
||||
return self.mbdiscid
|
||||
values = self._getMusicBrainzValues()
|
||||
|
||||
# MusicBrainz disc id does not take into account data tracks
|
||||
# P2.3
|
||||
try:
|
||||
import hashlib
|
||||
sha1 = hashlib.sha1
|
||||
except ImportError:
|
||||
from sha import sha as sha1
|
||||
import base64
|
||||
|
||||
sha = sha1()
|
||||
|
||||
# number of first track
|
||||
sha.update("%02X" % values[0])
|
||||
|
||||
# number of last track
|
||||
sha.update("%02X" % values[1])
|
||||
|
||||
sha.update("%08X" % values[2])
|
||||
|
||||
# offsets of tracks
|
||||
for i in range(1, 100):
|
||||
try:
|
||||
offset = values[2 + i]
|
||||
except IndexError:
|
||||
#print 'track', i - 1, '0 offset'
|
||||
offset = 0
|
||||
sha.update("%08X" % offset)
|
||||
|
||||
digest = sha.digest()
|
||||
assert len(digest) == 20, \
|
||||
"digest should be 20 chars, not %d" % len(digest)
|
||||
|
||||
# The RFC822 spec uses +, /, and = characters, all of which are special
|
||||
# HTTP/URL characters. To avoid the problems with dealing with that, I
|
||||
# (Rob) used ., _, and -
|
||||
|
||||
# base64 altchars specify replacements for + and /
|
||||
result = base64.b64encode(digest, '._')
|
||||
|
||||
# now replace =
|
||||
result = "-".join(result.split("="))
|
||||
assert len(result) == 28, \
|
||||
"Result should be 28 characters, not %d" % len(result)
|
||||
|
||||
logger.debug('getMusicBrainzDiscId: returning %r' % result)
|
||||
self.mbdiscid = result
|
||||
return result
|
||||
|
||||
def getMusicBrainzSubmitURL(self):
|
||||
host = 'musicbrainz.org'
|
||||
|
||||
discid = self.getMusicBrainzDiscId()
|
||||
values = self._getMusicBrainzValues()
|
||||
|
||||
query = urllib.urlencode({
|
||||
'id': discid,
|
||||
'toc': ' '.join([str(v) for v in values]),
|
||||
'tracks': self.getAudioTracks(),
|
||||
})
|
||||
|
||||
return urlparse.urlunparse((
|
||||
'https', host, '/cdtoc/attach', '', query, ''))
|
||||
|
||||
def getFrameLength(self, data=False):
|
||||
"""
|
||||
Get the length in frames (excluding HTOA)
|
||||
|
||||
@param data: whether to include the data tracks in the length
|
||||
"""
|
||||
# the 'real' leadout, not offset by 150 frames
|
||||
if data:
|
||||
last = self.tracks[-1]
|
||||
else:
|
||||
last = self.tracks[self.getAudioTracks() - 1]
|
||||
|
||||
leadout = self.getTrackEnd(last.number) + 1
|
||||
logger.debug('leadout LBA: %d', leadout)
|
||||
durationFrames = leadout - self.getTrackStart(1)
|
||||
|
||||
return durationFrames
|
||||
|
||||
def duration(self):
|
||||
"""
|
||||
Get the duration in ms for all audio tracks (excluding HTOA).
|
||||
"""
|
||||
return int(self.getFrameLength() * 1000.0 / common.FRAMES_PER_SECOND)
|
||||
|
||||
def _getMusicBrainzValues(self):
|
||||
"""
|
||||
Get all MusicBrainz values needed to calculate disc id and submit URL.
|
||||
|
||||
This includes:
|
||||
- track number of first track
|
||||
- number of audio tracks
|
||||
- leadout of disc
|
||||
- offset of index 1 of each track
|
||||
|
||||
@rtype: list of int
|
||||
"""
|
||||
# MusicBrainz disc id does not take into account data tracks
|
||||
|
||||
result = []
|
||||
|
||||
# number of first track
|
||||
result.append(1)
|
||||
|
||||
# number of last audio track
|
||||
result.append(self.getAudioTracks())
|
||||
|
||||
leadout = self.leadout
|
||||
# if the disc is multi-session, last track is the data track,
|
||||
# and we should subtract 11250 + 150 from the last track's offset
|
||||
# for the leadout
|
||||
if self.hasDataTracks():
|
||||
assert not self.tracks[-1].audio
|
||||
leadout = self.tracks[-1].getIndex(1).absolute - 11250 - 150
|
||||
|
||||
# treat leadout offset as track 0 offset
|
||||
result.append(150 + leadout)
|
||||
|
||||
# offsets of tracks
|
||||
for i in range(1, 100):
|
||||
try:
|
||||
track = self.tracks[i - 1]
|
||||
if not track.audio:
|
||||
continue
|
||||
offset = track.getIndex(1).absolute + 150
|
||||
result.append(offset)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
|
||||
logger.debug('Musicbrainz values: %r', result)
|
||||
return result
|
||||
|
||||
def getAccurateRipIds(self):
|
||||
"""
|
||||
Calculate the two AccurateRip ID's.
|
||||
|
||||
@returns: the two 8-character hexadecimal disc ID's
|
||||
@rtype: tuple of (str, str)
|
||||
"""
|
||||
# AccurateRip does not take into account data tracks,
|
||||
# but does count the data track to determine the leadout offset
|
||||
discId1 = 0
|
||||
discId2 = 0
|
||||
|
||||
for track in self.tracks:
|
||||
if not track.audio:
|
||||
continue
|
||||
offset = self.getTrackStart(track.number)
|
||||
discId1 += offset
|
||||
discId2 += (offset or 1) * track.number
|
||||
|
||||
# also add end values, where leadout offset is one past the end
|
||||
# of the last track
|
||||
last = self.tracks[-1]
|
||||
offset = self.getTrackEnd(last.number) + 1
|
||||
discId1 += offset
|
||||
discId2 += offset * (self.getAudioTracks() + 1)
|
||||
|
||||
discId1 &= 0xffffffff
|
||||
discId2 &= 0xffffffff
|
||||
|
||||
return ("%08x" % discId1, "%08x" % discId2)
|
||||
|
||||
def getAccurateRipURL(self):
|
||||
"""
|
||||
Return the full AccurateRip URL.
|
||||
|
||||
@returns: the AccurateRip URL
|
||||
@rtype: str
|
||||
"""
|
||||
discId1, discId2 = self.getAccurateRipIds()
|
||||
|
||||
return "http://www.accuraterip.com/accuraterip/" \
|
||||
"%s/%s/%s/dBAR-%.3d-%s-%s-%s.bin" % (
|
||||
discId1[-1], discId1[-2], discId1[-3],
|
||||
self.getAudioTracks(), discId1, discId2, self.getCDDBDiscId())
|
||||
|
||||
def cue(self, cuePath='', program='whipper'):
|
||||
"""
|
||||
@param cuePath: path to the cue file to be written. If empty,
|
||||
will treat paths as if in current directory.
|
||||
|
||||
|
||||
Dump our internal representation to a .cue file content.
|
||||
|
||||
@rtype: C{unicode}
|
||||
"""
|
||||
logger.debug('generating .cue for cuePath %r', cuePath)
|
||||
|
||||
lines = []
|
||||
|
||||
def writeFile(path):
|
||||
targetPath = common.getRelativePath(path, cuePath)
|
||||
line = 'FILE "%s" WAVE' % targetPath
|
||||
lines.append(line)
|
||||
logger.debug('writeFile: %r' % line)
|
||||
|
||||
# header
|
||||
main = ['PERFORMER', 'TITLE']
|
||||
|
||||
for key in CDTEXT_FIELDS:
|
||||
if key not in main and key in self.cdtext:
|
||||
lines.append(" %s %s" % (key, self.cdtext[key]))
|
||||
|
||||
assert self.hasTOC(), "Table does not represent a full CD TOC"
|
||||
lines.append('REM DISCID %s' % self.getCDDBDiscId().upper())
|
||||
lines.append('REM COMMENT "%s %s"' % (program, whipper.__version__))
|
||||
|
||||
if self.catalog:
|
||||
lines.append("CATALOG %s" % self.catalog)
|
||||
|
||||
for key in main:
|
||||
if key in self.cdtext:
|
||||
lines.append('%s "%s"' % (key, self.cdtext[key]))
|
||||
|
||||
# FIXME:
|
||||
# - the first FILE statement goes before the first TRACK, even if
|
||||
# there is a non-file-using PREGAP
|
||||
# - the following FILE statements come after the last INDEX that
|
||||
# use that FILE; so before a next TRACK, PREGAP silence, ...
|
||||
|
||||
# add the first FILE line; EAC always puts the first FILE
|
||||
# statement before TRACK 01 and any possible PRE-GAP
|
||||
firstTrack = self.tracks[0]
|
||||
index = firstTrack.getFirstIndex()
|
||||
indexOne = firstTrack.getIndex(1)
|
||||
counter = index.counter
|
||||
track = firstTrack
|
||||
|
||||
while not index.path:
|
||||
t, i = self.getNextTrackIndex(track.number, index.number)
|
||||
track = self.tracks[t - 1]
|
||||
index = track.getIndex(i)
|
||||
counter = index.counter
|
||||
|
||||
if index.path:
|
||||
logger.debug('counter %d, writeFile' % counter)
|
||||
writeFile(index.path)
|
||||
|
||||
for i, track in enumerate(self.tracks):
|
||||
logger.debug('track i %r, track %r' % (i, track))
|
||||
# FIXME: skip data tracks for now
|
||||
if not track.audio:
|
||||
continue
|
||||
|
||||
indexes = track.indexes.keys()
|
||||
indexes.sort()
|
||||
|
||||
wroteTrack = False
|
||||
|
||||
for number in indexes:
|
||||
index = track.indexes[number]
|
||||
logger.debug('index %r, %r' % (number, index))
|
||||
|
||||
# any time the source counter changes to a higher value,
|
||||
# write a FILE statement
|
||||
# it has to be higher, because we can run into the HTOA
|
||||
# at counter 0 here
|
||||
if index.counter > counter:
|
||||
if index.path:
|
||||
logger.debug('counter %d, writeFile' % counter)
|
||||
writeFile(index.path)
|
||||
logger.debug('setting counter to index.counter %r' %
|
||||
index.counter)
|
||||
counter = index.counter
|
||||
|
||||
# any time we hit the first index, write a TRACK statement
|
||||
if not wroteTrack:
|
||||
wroteTrack = True
|
||||
line = " TRACK %02d %s" % (i + 1, 'AUDIO')
|
||||
lines.append(line)
|
||||
logger.debug('%r' % line)
|
||||
|
||||
for key in CDTEXT_FIELDS:
|
||||
if key in track.cdtext:
|
||||
lines.append(' %s "%s"' % (
|
||||
key, track.cdtext[key]))
|
||||
|
||||
if track.isrc is not None:
|
||||
lines.append(" ISRC %s" % track.isrc)
|
||||
|
||||
if track.pre_emphasis is not None:
|
||||
lines.append(" FLAGS PRE")
|
||||
|
||||
# handle TRACK 01 INDEX 00 specially
|
||||
if 0 in indexes:
|
||||
index00 = track.indexes[0]
|
||||
if i == 0:
|
||||
# if we have a silent pre-gap, output it
|
||||
if not index00.path:
|
||||
length = indexOne.absolute - index00.absolute
|
||||
lines.append(" PREGAP %s" %
|
||||
common.framesToMSF(length))
|
||||
continue
|
||||
|
||||
# handle any other INDEX 00 after its TRACK
|
||||
lines.append(" INDEX %02d %s" % (0,
|
||||
common.framesToMSF(index00.relative)))
|
||||
|
||||
if number > 0:
|
||||
# index 00 is output after TRACK up above
|
||||
lines.append(" INDEX %02d %s" % (number,
|
||||
common.framesToMSF(index.relative)))
|
||||
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
### methods that modify the table
|
||||
|
||||
def clearFiles(self):
|
||||
"""
|
||||
Clear all file backings.
|
||||
Resets indexes paths and relative offsets.
|
||||
"""
|
||||
# FIXME: do a loop over track indexes better, with a pythonic
|
||||
# construct that allows you to do for t, i in ...
|
||||
t = self.tracks[0].number
|
||||
index = self.tracks[0].getFirstIndex()
|
||||
i = index.number
|
||||
|
||||
logger.debug('clearing path')
|
||||
while True:
|
||||
track = self.tracks[t - 1]
|
||||
index = track.getIndex(i)
|
||||
logger.debug('Clearing path on track %d, index %d', t, i)
|
||||
index.path = None
|
||||
index.relative = None
|
||||
try:
|
||||
t, i = self.getNextTrackIndex(t, i)
|
||||
except IndexError:
|
||||
break
|
||||
|
||||
def setFile(self, track, index, path, length, counter=None):
|
||||
"""
|
||||
Sets the given file as the source from the given index on.
|
||||
Will loop over all indexes that fall within the given length,
|
||||
to adjust the path.
|
||||
|
||||
Assumes all indexes have an absolute offset and will raise if not.
|
||||
|
||||
@type track: C{int}
|
||||
@type index: C{int}
|
||||
"""
|
||||
logger.debug('setFile: track %d, index %d, path %r, '
|
||||
'length %r, counter %r', track, index, path, length, counter)
|
||||
|
||||
t = self.tracks[track - 1]
|
||||
i = t.indexes[index]
|
||||
start = i.absolute
|
||||
assert start is not None, "index %r is missing absolute offset" % i
|
||||
end = start + length - 1 # last sector that should come from this file
|
||||
|
||||
# FIXME: check border conditions here, esp. wrt. toc's off-by-one bug
|
||||
while i.absolute <= end:
|
||||
i.path = path
|
||||
i.relative = i.absolute - start
|
||||
i.counter = counter
|
||||
logger.debug('Setting path %r, relative %r on '
|
||||
'track %d, index %d, counter %r',
|
||||
path, i.relative, track, index, counter)
|
||||
try:
|
||||
track, index = self.getNextTrackIndex(track, index)
|
||||
t = self.tracks[track - 1]
|
||||
i = t.indexes[index]
|
||||
except IndexError:
|
||||
break
|
||||
|
||||
def absolutize(self):
|
||||
"""
|
||||
Calculate absolute offsets on indexes as much as possible.
|
||||
Only possible for as long as tracks draw from the same file.
|
||||
"""
|
||||
t = self.tracks[0].number
|
||||
index = self.tracks[0].getFirstIndex()
|
||||
i = index.number
|
||||
# the first cut is the deepest
|
||||
counter = index.counter
|
||||
|
||||
#for t in self.tracks: print t, t.indexes
|
||||
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:
|
||||
logger.debug('Track %d, index %d has no counter', t, i)
|
||||
break
|
||||
if index.counter != counter:
|
||||
logger.debug('Track %d, index %d has a different counter', t, i)
|
||||
break
|
||||
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:
|
||||
msg = 'Track %d, index %d had absolute %d,' \
|
||||
' overriding with %d' % (
|
||||
t, i, index.absolute, index.relative)
|
||||
raise ValueError(msg)
|
||||
index.absolute = index.relative
|
||||
try:
|
||||
t, i = self.getNextTrackIndex(t, i)
|
||||
except IndexError:
|
||||
break
|
||||
|
||||
def merge(self, other, session=2):
|
||||
"""
|
||||
Merges the given table at the end.
|
||||
The other table is assumed to be from an additional session,
|
||||
|
||||
|
||||
@type other: L{Table}
|
||||
"""
|
||||
gap = self._getSessionGap(session)
|
||||
|
||||
trackCount = len(self.tracks)
|
||||
sourceCounter = self.tracks[-1].getLastIndex().counter
|
||||
|
||||
for track in other.tracks:
|
||||
t = copy.deepcopy(track)
|
||||
t.number = track.number + trackCount
|
||||
t.session = session
|
||||
for i in t.indexes.values():
|
||||
if i.absolute is not None:
|
||||
i.absolute += self.leadout + gap
|
||||
logger.debug('Fixing track %02d, index %02d, absolute %d' % (
|
||||
t.number, i.number, i.absolute))
|
||||
if i.counter is not None:
|
||||
i.counter += sourceCounter
|
||||
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
|
||||
logger.debug('Fixing leadout, now %d', self.leadout)
|
||||
|
||||
def _getSessionGap(self, session):
|
||||
# From cdrecord multi-session info:
|
||||
# For the first additional session this is 11250 sectors
|
||||
# lead-out/lead-in overhead + 150 sectors for the pre-gap of the first
|
||||
# track after the lead-in = 11400 sectos.
|
||||
|
||||
# For all further session this is 6750 sectors lead-out/lead-in
|
||||
# overhead + 150 sectors for the pre-gap of the first track after the
|
||||
# lead-in = 6900 sectors.
|
||||
|
||||
gap = 11400
|
||||
if session > 2:
|
||||
gap = 6900
|
||||
return gap
|
||||
|
||||
### lookups
|
||||
|
||||
def getNextTrackIndex(self, track, index):
|
||||
"""
|
||||
Return the next track and index.
|
||||
|
||||
@param track: track number, 1-based
|
||||
|
||||
@raises IndexError: on last index
|
||||
|
||||
@rtype: tuple of (int, int)
|
||||
"""
|
||||
t = self.tracks[track - 1]
|
||||
indexes = t.indexes.keys()
|
||||
position = indexes.index(index)
|
||||
|
||||
if position + 1 < len(indexes):
|
||||
return track, indexes[position + 1]
|
||||
|
||||
track += 1
|
||||
if track > len(self.tracks):
|
||||
raise IndexError("No index beyond track %d, index %d" % (
|
||||
track - 1, index))
|
||||
|
||||
t = self.tracks[track - 1]
|
||||
indexes = t.indexes.keys()
|
||||
|
||||
return track, indexes[0]
|
||||
|
||||
# various tests for types of Table
|
||||
|
||||
def hasTOC(self):
|
||||
"""
|
||||
Check if the Table has a complete TOC.
|
||||
a TOC is a list of all tracks and their Index 01, with absolute
|
||||
offsets, as well as the leadout.
|
||||
"""
|
||||
if not self.leadout:
|
||||
logger.debug('no leadout, no TOC')
|
||||
return False
|
||||
|
||||
for t in self.tracks:
|
||||
if 1 not in t.indexes.keys():
|
||||
logger.debug('no index 1, no TOC')
|
||||
return False
|
||||
if t.indexes[1].absolute is None:
|
||||
logger.debug('no absolute index 1, no TOC')
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def canCue(self):
|
||||
"""
|
||||
Check if this table can be used to generate a .cue file
|
||||
"""
|
||||
if not self.hasTOC():
|
||||
logger.debug('No TOC, cannot cue')
|
||||
return False
|
||||
|
||||
for t in self.tracks:
|
||||
for i in t.indexes.values():
|
||||
if i.relative is None:
|
||||
logger.debug('Track %02d, Index %02d does not have relative',
|
||||
t.number, i.number)
|
||||
return False
|
||||
|
||||
return True
|
||||
445
whipper/image/toc.py
Normal file
445
whipper/image/toc.py
Normal file
@@ -0,0 +1,445 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_image_toc -*-
|
||||
# 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/>.
|
||||
|
||||
"""
|
||||
Reading .toc files
|
||||
|
||||
The .toc file format is described in the man page of cdrdao
|
||||
"""
|
||||
|
||||
import re
|
||||
import codecs
|
||||
|
||||
from whipper.common import common
|
||||
from whipper.image import table
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# shared
|
||||
_CDTEXT_CANDIDATE_RE = re.compile(r'(?P<key>\w+) "(?P<value>.+)"')
|
||||
|
||||
# header
|
||||
_CATALOG_RE = re.compile(r'^CATALOG "(?P<catalog>\d+)"$')
|
||||
|
||||
# pre emphasis
|
||||
_PRE_EMPHASIS_RE = re.compile(r'^PRE_EMPHASIS$')
|
||||
|
||||
# records
|
||||
_TRACK_RE = re.compile(r"""
|
||||
^TRACK # TRACK
|
||||
\s(?P<mode>.+)$ # mode (AUDIO, MODE2_FORM_MIX, MODEx/2xxx, ...)
|
||||
""", re.VERBOSE)
|
||||
|
||||
_ISRC_RE = re.compile(r'^ISRC "(?P<isrc>\w+)"$')
|
||||
|
||||
# a HTOA is marked in the cdrdao's TOC as SILENCE
|
||||
_SILENCE_RE = re.compile(r"""
|
||||
^SILENCE # SILENCE
|
||||
\s(?P<length>.*)$ # pre-gap length
|
||||
""", re.VERBOSE)
|
||||
|
||||
# ZERO is used as pre-gap source when switching mode
|
||||
_ZERO_RE = re.compile(r"""
|
||||
^ZERO # ZERO
|
||||
\s(?P<mode>.+) # mode (AUDIO, MODEx/2xxx, ...)
|
||||
\s(?P<length>.*)$ # zero length
|
||||
""", re.VERBOSE)
|
||||
|
||||
|
||||
_FILE_RE = re.compile(r"""
|
||||
^FILE # FILE
|
||||
\s+"(?P<name>.*)" # 'file name' in quotes
|
||||
\s+(?P<start>.+) # start offset
|
||||
\s(?P<length>.+)$ # length in frames of section
|
||||
""", re.VERBOSE)
|
||||
|
||||
_DATAFILE_RE = re.compile(r"""
|
||||
^DATAFILE # DATA FILE
|
||||
\s+"(?P<name>.*)" # 'file name' in quotes
|
||||
\s+(?P<length>\S+) # start offset
|
||||
\s*.* # possible // comment
|
||||
""", re.VERBOSE)
|
||||
|
||||
|
||||
# FIXME: start can be 0
|
||||
_START_RE = re.compile(r"""
|
||||
^START # START
|
||||
\s(?P<length>.*)$ # pre-gap length
|
||||
""", re.VERBOSE)
|
||||
|
||||
|
||||
_INDEX_RE = re.compile(r"""
|
||||
^INDEX # INDEX
|
||||
\s(?P<offset>.+)$ # start offset
|
||||
""", re.VERBOSE)
|
||||
|
||||
|
||||
class Sources:
|
||||
"""
|
||||
I represent the list of sources used in the .toc file.
|
||||
Each SILENCE and each FILE is a source.
|
||||
If the filename for FILE doesn't change, the counter is not increased.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._sources = []
|
||||
|
||||
def append(self, counter, offset, source):
|
||||
"""
|
||||
@param counter: the source counter; updates for each different
|
||||
data source (silence or different file path)
|
||||
@type counter: int
|
||||
@param offset: the absolute disc offset where this source starts
|
||||
"""
|
||||
logger.debug('Appending source, counter %d, abs offset %d, source %r' % (
|
||||
counter, offset, source))
|
||||
self._sources.append((counter, offset, source))
|
||||
|
||||
def get(self, offset):
|
||||
"""
|
||||
Retrieve the source used at the given offset.
|
||||
"""
|
||||
for i, (c, o, s) in enumerate(self._sources):
|
||||
if offset < o:
|
||||
return self._sources[i - 1]
|
||||
|
||||
return self._sources[-1]
|
||||
|
||||
def getCounterStart(self, counter):
|
||||
"""
|
||||
Retrieve the absolute offset of the first source for this counter
|
||||
"""
|
||||
for i, (c, o, s) in enumerate(self._sources):
|
||||
if c == counter:
|
||||
return self._sources[i][1]
|
||||
|
||||
return self._sources[-1][1]
|
||||
|
||||
|
||||
class TocFile(object):
|
||||
|
||||
def __init__(self, path):
|
||||
"""
|
||||
@type path: unicode
|
||||
"""
|
||||
assert type(path) is unicode, "%r is not unicode" % path
|
||||
self._path = path
|
||||
self._messages = []
|
||||
self.table = table.Table()
|
||||
self.logName = '<TocFile %08x>' % id(self)
|
||||
|
||||
self._sources = Sources()
|
||||
|
||||
def _index(self, currentTrack, i, absoluteOffset, trackOffset):
|
||||
absolute = absoluteOffset + trackOffset
|
||||
# this may be in a new source, so calculate relative
|
||||
c, o, s = self._sources.get(absolute)
|
||||
logger.debug('at abs offset %d, we are in source %r' % (
|
||||
absolute, s))
|
||||
counterStart = self._sources.getCounterStart(c)
|
||||
relative = absolute - counterStart
|
||||
|
||||
currentTrack.index(i, path=s.path,
|
||||
absolute=absolute,
|
||||
relative=relative,
|
||||
counter=c)
|
||||
logger.debug(
|
||||
'[track %02d index %02d] trackOffset %r, added %r',
|
||||
currentTrack.number, i, trackOffset,
|
||||
currentTrack.getIndex(i))
|
||||
|
||||
|
||||
def parse(self):
|
||||
# these two objects start as None then get set as real objects,
|
||||
# so no need to complain about them here
|
||||
__pychecker__ = 'no-objattrs'
|
||||
currentFile = None
|
||||
currentTrack = None
|
||||
|
||||
state = 'HEADER'
|
||||
counter = 0 # counts sources for audio data; SILENCE/ZERO/FILE
|
||||
trackNumber = 0
|
||||
indexNumber = 0
|
||||
absoluteOffset = 0 # running absolute offset of where each track starts
|
||||
relativeOffset = 0 # running relative offset, relative to counter src
|
||||
currentLength = 0 # accrued during TRACK record parsing;
|
||||
# length of current track as parsed so far;
|
||||
# reset on each TRACK statement
|
||||
totalLength = 0 # accrued during TRACK record parsing, total disc
|
||||
pregapLength = 0 # length of the pre-gap, current track in for loop
|
||||
|
||||
# the first track's INDEX 1 can only be gotten from the .toc
|
||||
# file once the first pregap is calculated; so we add INDEX 1
|
||||
# at the end of each parsed TRACK record
|
||||
handle = codecs.open(self._path, "r", "utf-8")
|
||||
|
||||
for number, line in enumerate(handle.readlines()):
|
||||
line = line.rstrip()
|
||||
|
||||
# look for CDTEXT stuff in either header or tracks
|
||||
m = _CDTEXT_CANDIDATE_RE.search(line)
|
||||
if m:
|
||||
key = m.group('key')
|
||||
value = m.group('value')
|
||||
# usually, value is encoded with octal escapes and in latin-1
|
||||
# FIXME: other encodings are possible, does cdrdao handle
|
||||
# them ?
|
||||
value = value.decode('string-escape').decode('latin-1')
|
||||
if key in table.CDTEXT_FIELDS:
|
||||
# FIXME: consider ISRC separate for now, but this
|
||||
# is a limitation of our parser approach
|
||||
if state == 'HEADER':
|
||||
self.table.cdtext[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:
|
||||
logger.debug('Found track CD-Text %s: %r',
|
||||
key, value)
|
||||
currentTrack.cdtext[key] = value
|
||||
|
||||
# look for header elements
|
||||
m = _CATALOG_RE.search(line)
|
||||
if m:
|
||||
self.table.catalog = m.group('catalog')
|
||||
logger.debug("Found catalog number %s", self.table.catalog)
|
||||
|
||||
# look for TRACK lines
|
||||
m = _TRACK_RE.search(line)
|
||||
if m:
|
||||
state = 'TRACK'
|
||||
|
||||
# set index 1 of previous track if there was one, using
|
||||
# pregapLength if applicable
|
||||
if currentTrack:
|
||||
self._index(currentTrack, 1, absoluteOffset, pregapLength)
|
||||
|
||||
# create a new track to be filled by later lines
|
||||
trackNumber += 1
|
||||
trackMode = m.group('mode')
|
||||
audio = trackMode == 'AUDIO'
|
||||
currentTrack = table.Track(trackNumber, audio=audio)
|
||||
self.table.tracks.append(currentTrack)
|
||||
|
||||
# update running totals
|
||||
absoluteOffset += currentLength
|
||||
relativeOffset += currentLength
|
||||
totalLength += currentLength
|
||||
|
||||
# FIXME: track mode
|
||||
logger.debug('found track %d, mode %s, at absoluteOffset %d',
|
||||
trackNumber, trackMode, absoluteOffset)
|
||||
|
||||
# reset counters relative to a track
|
||||
currentLength = 0
|
||||
indexNumber = 1
|
||||
pregapLength = 0
|
||||
|
||||
continue
|
||||
|
||||
# look for PRE_EMPHASIS lines
|
||||
m = _PRE_EMPHASIS_RE.search(line)
|
||||
if m:
|
||||
currentTrack.pre_emphasis = True
|
||||
logger.debug('Track has PRE_EMPHASIS')
|
||||
|
||||
# look for ISRC lines
|
||||
m = _ISRC_RE.search(line)
|
||||
if m:
|
||||
isrc = m.group('isrc')
|
||||
currentTrack.isrc = isrc
|
||||
logger.debug('Found ISRC code %s', isrc)
|
||||
|
||||
# look for SILENCE lines
|
||||
m = _SILENCE_RE.search(line)
|
||||
if m:
|
||||
length = m.group('length')
|
||||
logger.debug('SILENCE of %r', length)
|
||||
self._sources.append(counter, absoluteOffset, None)
|
||||
if currentFile is not None:
|
||||
logger.debug('SILENCE after FILE, increasing counter')
|
||||
counter += 1
|
||||
relativeOffset = 0
|
||||
currentFile = None
|
||||
currentLength += common.msfToFrames(length)
|
||||
|
||||
# look for ZERO lines
|
||||
m = _ZERO_RE.search(line)
|
||||
if m:
|
||||
if currentFile is not None:
|
||||
logger.debug('ZERO after FILE, increasing counter')
|
||||
counter += 1
|
||||
relativeOffset = 0
|
||||
currentFile = None
|
||||
length = m.group('length')
|
||||
currentLength += common.msfToFrames(length)
|
||||
|
||||
# look for FILE lines
|
||||
m = _FILE_RE.search(line)
|
||||
if m:
|
||||
filePath = m.group('name')
|
||||
start = m.group('start')
|
||||
length = m.group('length')
|
||||
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
|
||||
logger.debug('track %d, switched to new FILE, '
|
||||
'increased counter to %d',
|
||||
trackNumber, counter)
|
||||
currentFile = File(filePath, common.msfToFrames(start),
|
||||
common.msfToFrames(length))
|
||||
self._sources.append(counter, absoluteOffset + currentLength,
|
||||
currentFile)
|
||||
#absoluteOffset += common.msfToFrames(start)
|
||||
currentLength += common.msfToFrames(length)
|
||||
|
||||
# look for DATAFILE lines
|
||||
m = _DATAFILE_RE.search(line)
|
||||
if m:
|
||||
filePath = m.group('name')
|
||||
length = m.group('length')
|
||||
# print 'THOMAS', length
|
||||
logger.debug('FILE %s, length %r',
|
||||
filePath, common.msfToFrames(length))
|
||||
if not currentFile or filePath != currentFile.path:
|
||||
counter += 1
|
||||
relativeOffset = 0
|
||||
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
|
||||
currentFile = File(filePath, 0, common.msfToFrames(length))
|
||||
self._sources.append(counter, absoluteOffset + currentLength,
|
||||
currentFile)
|
||||
#absoluteOffset += common.msfToFrames(start)
|
||||
currentLength += common.msfToFrames(length)
|
||||
|
||||
|
||||
# look for START lines
|
||||
m = _START_RE.search(line)
|
||||
if m:
|
||||
if not currentTrack:
|
||||
self.message(number, 'START without preceding TRACK')
|
||||
print 'ouch'
|
||||
continue
|
||||
|
||||
length = common.msfToFrames(m.group('length'))
|
||||
c, o, s = self._sources.get(absoluteOffset)
|
||||
logger.debug('at abs offset %d, we are in source %r' % (
|
||||
absoluteOffset, s))
|
||||
counterStart = self._sources.getCounterStart(c)
|
||||
relativeOffset = absoluteOffset - counterStart
|
||||
|
||||
currentTrack.index(0, path=s and s.path or None,
|
||||
absolute=absoluteOffset,
|
||||
relative=relativeOffset, counter=c)
|
||||
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
|
||||
pregapLength = length
|
||||
|
||||
# look for INDEX lines
|
||||
m = _INDEX_RE.search(line)
|
||||
if m:
|
||||
if not currentTrack:
|
||||
self.message(number, 'INDEX without preceding TRACK')
|
||||
print 'ouch'
|
||||
continue
|
||||
|
||||
indexNumber += 1
|
||||
offset = common.msfToFrames(m.group('offset'))
|
||||
self._index(currentTrack, indexNumber, absoluteOffset, offset)
|
||||
|
||||
# handle index 1 of final track, if any
|
||||
if currentTrack:
|
||||
self._index(currentTrack, 1, absoluteOffset, pregapLength)
|
||||
|
||||
# totalLength was added up to the penultimate track
|
||||
self.table.leadout = totalLength + currentLength
|
||||
logger.debug('parse: leadout: %r', self.table.leadout)
|
||||
|
||||
def message(self, number, message):
|
||||
"""
|
||||
Add a message about a given line in the cue file.
|
||||
|
||||
@param number: line number, counting from 0.
|
||||
"""
|
||||
self._messages.append((number + 1, message))
|
||||
|
||||
def getTrackLength(self, track):
|
||||
"""
|
||||
Returns the length of the given track, from its INDEX 01 to the next
|
||||
track's INDEX 01
|
||||
"""
|
||||
# returns track length in frames, or -1 if can't be determined and
|
||||
# complete file should be assumed
|
||||
# FIXME: this assumes a track can only be in one file; is this true ?
|
||||
i = self.table.tracks.index(track)
|
||||
if i == len(self.table.tracks) - 1:
|
||||
# last track, so no length known
|
||||
return -1
|
||||
|
||||
thisIndex = track.indexes[1] # FIXME: could be more
|
||||
nextIndex = self.table.tracks[i + 1].indexes[1] # FIXME: could be 0
|
||||
|
||||
c = thisIndex.counter
|
||||
if c is not None and c == nextIndex.counter:
|
||||
# they belong to the same source, so their relative delta is length
|
||||
return nextIndex.relative - thisIndex.relative
|
||||
|
||||
# FIXME: more logic
|
||||
return -1
|
||||
|
||||
def getRealPath(self, path):
|
||||
"""
|
||||
Translate the .toc's FILE to an existing path.
|
||||
|
||||
@type path: unicode
|
||||
"""
|
||||
return common.getRealPath(self._path, path)
|
||||
|
||||
|
||||
class File:
|
||||
"""
|
||||
I represent a FILE line in a .toc file.
|
||||
"""
|
||||
|
||||
def __init__(self, path, start, length):
|
||||
"""
|
||||
@type path: C{unicode}
|
||||
@type start: C{int}
|
||||
@param start: starting point for the track in this file, in frames
|
||||
@param length: length for the track in this file, in frames
|
||||
"""
|
||||
assert type(path) is unicode, "%r is not unicode" % path
|
||||
|
||||
self.path = path
|
||||
self.start = start
|
||||
self.length = length
|
||||
|
||||
def __repr__(self):
|
||||
return '<File %r>' % (self.path, )
|
||||
0
whipper/program/__init__.py
Normal file
0
whipper/program/__init__.py
Normal file
52
whipper/program/arc.py
Normal file
52
whipper/program/arc.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from os.path import exists
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ARB = 'accuraterip-checksum'
|
||||
FLAC = 'flac'
|
||||
|
||||
def accuraterip_checksum(f, track, tracks, wave=False, v2=False):
|
||||
v = '--accuraterip-v1'
|
||||
if v2:
|
||||
v = '--accuraterip-v2'
|
||||
|
||||
track, tracks = str(track), str(tracks)
|
||||
|
||||
if not wave:
|
||||
flac = Popen([FLAC, '-cds', f], stdout=PIPE)
|
||||
|
||||
arc = Popen([ARB, v, '/dev/stdin', track, tracks],
|
||||
stdin=flac.stdout, stdout=PIPE, stderr=PIPE)
|
||||
else:
|
||||
arc = Popen([ARB, v, f, track, tracks],
|
||||
stdout=PIPE, stderr=PIPE)
|
||||
|
||||
if not wave:
|
||||
flac.stdout.close()
|
||||
|
||||
out, err = arc.communicate()
|
||||
|
||||
if not wave:
|
||||
flac.wait()
|
||||
flac_rc = flac.returncode
|
||||
|
||||
arc_rc = arc.returncode
|
||||
|
||||
if not wave and flac_rc != 0:
|
||||
logger.warning('ARC calculation failed: flac return code is non zero')
|
||||
return None
|
||||
|
||||
if arc_rc != 0:
|
||||
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:
|
||||
logger.warning('ARC output is not usable')
|
||||
return None
|
||||
|
||||
return outh
|
||||
613
whipper/program/cdparanoia.py
Normal file
613
whipper/program/cdparanoia.py
Normal file
@@ -0,0 +1,613 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_program_cdparanoia -*-
|
||||
# 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 errno
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from whipper.common import common
|
||||
from whipper.common import task as ctask
|
||||
from whipper.extern import asyncsub
|
||||
from whipper.extern.task import task
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FileSizeError(Exception):
|
||||
|
||||
message = None
|
||||
|
||||
"""
|
||||
The given path does not have the expected size.
|
||||
"""
|
||||
|
||||
def __init__(self, path, message):
|
||||
self.args = (path, message)
|
||||
self.path = path
|
||||
self.message = message
|
||||
|
||||
|
||||
class ReturnCodeError(Exception):
|
||||
"""
|
||||
The program had a non-zero return code.
|
||||
"""
|
||||
|
||||
def __init__(self, returncode):
|
||||
self.args = (returncode, )
|
||||
self.returncode = returncode
|
||||
|
||||
|
||||
class ChecksumException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# example:
|
||||
# ##: 0 [read] @ 24696
|
||||
_PROGRESS_RE = re.compile(r"""
|
||||
^\#\#: (?P<code>.+)\s # function code
|
||||
\[(?P<function>.*)\]\s@\s # [function name] @
|
||||
(?P<offset>\d+) # offset in words (2-byte one channel value)
|
||||
""", re.VERBOSE)
|
||||
|
||||
_ERROR_RE = re.compile("^scsi_read error:")
|
||||
|
||||
# from reading cdparanoia source code, it looks like offset is reported in
|
||||
# number of single-channel samples, ie. 2 bytes (word) per unit, and absolute
|
||||
|
||||
|
||||
class ProgressParser:
|
||||
read = 0 # last [read] frame
|
||||
wrote = 0 # last [wrote] frame
|
||||
errors = 0 # count of number of scsi errors
|
||||
_nframes = None # number of frames read on each [read]
|
||||
_firstFrames = None # number of frames read on first [read]
|
||||
reads = 0 # total number of reads
|
||||
|
||||
def __init__(self, start, stop):
|
||||
"""
|
||||
@param start: first frame to rip
|
||||
@type start: int
|
||||
@param stop: last frame to rip (inclusive)
|
||||
@type stop: int
|
||||
"""
|
||||
self.start = start
|
||||
self.stop = stop
|
||||
|
||||
# FIXME: privatize
|
||||
self.read = start
|
||||
|
||||
self._reads = {} # read count for each sector
|
||||
|
||||
def parse(self, line):
|
||||
"""
|
||||
Parse a line.
|
||||
"""
|
||||
m = _PROGRESS_RE.search(line)
|
||||
if m:
|
||||
# code = int(m.group('code'))
|
||||
function = m.group('function')
|
||||
wordOffset = int(m.group('offset'))
|
||||
if function == 'read':
|
||||
self._parse_read(wordOffset)
|
||||
elif function == 'wrote':
|
||||
self._parse_wrote(wordOffset)
|
||||
|
||||
m = _ERROR_RE.search(line)
|
||||
if m:
|
||||
self.errors += 1
|
||||
|
||||
def _parse_read(self, wordOffset):
|
||||
if wordOffset % common.WORDS_PER_FRAME != 0:
|
||||
print 'THOMAS: not a multiple of %d: %d' % (
|
||||
common.WORDS_PER_FRAME, wordOffset)
|
||||
return
|
||||
|
||||
frameOffset = wordOffset / common.WORDS_PER_FRAME
|
||||
|
||||
# set nframes if not yet set
|
||||
if self._nframes is None and self.read != 0:
|
||||
self._nframes = frameOffset - self.read
|
||||
logger.debug('set nframes to %r', self._nframes)
|
||||
|
||||
# set firstFrames if not yet set
|
||||
if self._firstFrames is None:
|
||||
self._firstFrames = frameOffset - self.start
|
||||
logger.debug('set firstFrames to %r', self._firstFrames)
|
||||
|
||||
markStart = None
|
||||
markEnd = None # the next unread frame (half-inclusive)
|
||||
|
||||
# verify it either read nframes more or went back for verify
|
||||
if frameOffset > self.read:
|
||||
delta = frameOffset - self.read
|
||||
if self._nframes and delta != self._nframes:
|
||||
# print 'THOMAS: Read %d frames more, not %d' % (
|
||||
# delta, self._nframes)
|
||||
# my drive either reads 7 or 13 frames
|
||||
pass
|
||||
|
||||
# update our read sectors hash
|
||||
markStart = self.read
|
||||
markEnd = frameOffset
|
||||
else:
|
||||
# went back to verify
|
||||
# we could use firstFrames as an estimate on how many frames this
|
||||
# read, but this lowers our track quality needlessly where
|
||||
# EAC still reports 100% track quality
|
||||
markStart = frameOffset # - self._firstFrames
|
||||
markEnd = frameOffset
|
||||
|
||||
# FIXME: doing this is way too slow even for a testcase, so disable
|
||||
if False:
|
||||
for frame in range(markStart, markEnd):
|
||||
if not frame in self._reads.keys():
|
||||
self._reads[frame] = 0
|
||||
self._reads[frame] += 1
|
||||
|
||||
# cdparanoia reads quite a bit beyond the current track before it
|
||||
# goes back to verify; don't count those
|
||||
# markStart, markEnd of 0, 21 with stop 0 should give 1 read
|
||||
if markEnd > self.stop + 1:
|
||||
markEnd = self.stop + 1
|
||||
if markStart > self.stop + 1:
|
||||
markStart = self.stop + 1
|
||||
|
||||
self.reads += markEnd - markStart
|
||||
|
||||
# update our read pointer
|
||||
self.read = frameOffset
|
||||
|
||||
def _parse_wrote(self, wordOffset):
|
||||
# cdparanoia outputs most [wrote] calls with one word less than a frame
|
||||
frameOffset = (wordOffset + 1) / common.WORDS_PER_FRAME
|
||||
self.wrote = frameOffset
|
||||
|
||||
def getTrackQuality(self):
|
||||
"""
|
||||
Each frame gets read twice.
|
||||
More than two reads for a frame reduce track quality.
|
||||
"""
|
||||
frames = self.stop - self.start + 1 # + 1 since stop is inclusive
|
||||
reads = self.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
|
||||
return min(frames * 2.0 / reads, 1.0)
|
||||
|
||||
|
||||
# FIXME: handle errors
|
||||
|
||||
|
||||
class ReadTrackTask(task.Task):
|
||||
"""
|
||||
I am a task that reads a track using cdparanoia.
|
||||
|
||||
@ivar reads: how many reads were done to rip the track
|
||||
"""
|
||||
|
||||
description = "Reading track"
|
||||
quality = None # set at end of reading
|
||||
speed = None
|
||||
duration = None # in seconds
|
||||
|
||||
_MAXERROR = 100 # number of errors detected by parser
|
||||
|
||||
def __init__(self, path, table, start, stop, overread, offset=0,
|
||||
device=None, action="Reading", what="track"):
|
||||
"""
|
||||
Read the given track.
|
||||
|
||||
@param path: where to store the ripped track
|
||||
@type path: unicode
|
||||
@param table: table of contents of CD
|
||||
@type table: L{table.Table}
|
||||
@param start: first frame to rip
|
||||
@type start: int
|
||||
@param stop: last frame to rip (inclusive); >= start
|
||||
@type stop: int
|
||||
@param offset: read offset, in samples
|
||||
@type offset: int
|
||||
@param device: the device to rip from
|
||||
@type device: str
|
||||
@param action: a string representing the action; e.g. Read/Verify
|
||||
@type action: str
|
||||
@param what: a string representing what's being read; e.g. Track
|
||||
@type what: str
|
||||
"""
|
||||
assert type(path) is unicode, "%r is not unicode" % path
|
||||
|
||||
self.path = path
|
||||
self._table = table
|
||||
self._start = start
|
||||
self._stop = stop
|
||||
self._offset = offset
|
||||
self._parser = ProgressParser(start, stop)
|
||||
self._device = device
|
||||
self._start_time = None
|
||||
self._overread = overread
|
||||
|
||||
self._buffer = "" # accumulate characters
|
||||
self._errors = []
|
||||
self.description = "%s %s" % (action, what)
|
||||
|
||||
def start(self, runner):
|
||||
task.Task.start(self, runner)
|
||||
|
||||
# find on which track the range starts and stops
|
||||
startTrack = 0
|
||||
startOffset = 0
|
||||
stopTrack = 0
|
||||
stopOffset = self._stop
|
||||
|
||||
for i, t in enumerate(self._table.tracks):
|
||||
if self._table.getTrackStart(i + 1) <= self._start:
|
||||
startTrack = i + 1
|
||||
startOffset = self._start - self._table.getTrackStart(i + 1)
|
||||
if self._table.getTrackEnd(i + 1) <= self._stop:
|
||||
stopTrack = i + 1
|
||||
stopOffset = self._stop - self._table.getTrackStart(i + 1)
|
||||
|
||||
logger.debug('Ripping from %d to %d (inclusive)',
|
||||
self._start, self._stop)
|
||||
logger.debug('Starting at track %d, offset %d',
|
||||
startTrack, startOffset)
|
||||
logger.debug('Stopping at track %d, offset %d',
|
||||
stopTrack, stopOffset)
|
||||
|
||||
bufsize = 1024
|
||||
if self._overread:
|
||||
argv = ["cdparanoia", "--stderr-progress",
|
||||
"--sample-offset=%d" % self._offset, "--force-overread", ]
|
||||
else:
|
||||
argv = ["cdparanoia", "--stderr-progress",
|
||||
"--sample-offset=%d" % self._offset, ]
|
||||
if self._device:
|
||||
argv.extend(["--force-cdrom-device", self._device, ])
|
||||
argv.extend(["%d[%s]-%d[%s]" % (
|
||||
startTrack, common.framesToHMSF(startOffset),
|
||||
stopTrack, common.framesToHMSF(stopOffset)),
|
||||
self.path])
|
||||
logger.debug('Running %s' % (" ".join(argv), ))
|
||||
try:
|
||||
self._popen = asyncsub.Popen(argv,
|
||||
bufsize=bufsize,
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, close_fds=True)
|
||||
except OSError, e:
|
||||
import errno
|
||||
if e.errno == errno.ENOENT:
|
||||
raise common.MissingDependencyException('cdparanoia')
|
||||
|
||||
raise
|
||||
|
||||
self._start_time = time.time()
|
||||
self.schedule(1.0, self._read, runner)
|
||||
|
||||
def _read(self, runner):
|
||||
ret = self._popen.recv_err()
|
||||
if not ret:
|
||||
if self._popen.poll() is not None:
|
||||
self._done()
|
||||
return
|
||||
self.schedule(0.01, self._read, runner)
|
||||
return
|
||||
|
||||
self._buffer += ret
|
||||
|
||||
# parse buffer into lines if possible, and parse them
|
||||
if "\n" in self._buffer:
|
||||
lines = self._buffer.split('\n')
|
||||
if lines[-1] != "\n":
|
||||
# last line didn't end yet
|
||||
self._buffer = lines[-1]
|
||||
del lines[-1]
|
||||
else:
|
||||
self._buffer = ""
|
||||
|
||||
for line in lines:
|
||||
self._parser.parse(line)
|
||||
|
||||
# fail if too many errors
|
||||
if self._parser.errors > self._MAXERROR:
|
||||
logger.debug('%d errors, terminating', self._parser.errors)
|
||||
self._popen.terminate()
|
||||
|
||||
num = self._parser.wrote - self._start + 1
|
||||
den = self._stop - self._start + 1
|
||||
assert den != 0, "stop %d should be >= start %d" % (
|
||||
self._stop, self._start)
|
||||
progress = float(num) / float(den)
|
||||
if progress < 1.0:
|
||||
self.setProgress(progress)
|
||||
|
||||
# 0 does not give us output before we complete, 1.0 gives us output
|
||||
# too late
|
||||
self.schedule(0.01, self._read, runner)
|
||||
|
||||
def _poll(self, runner):
|
||||
if self._popen.poll() is None:
|
||||
self.schedule(1.0, self._poll, runner)
|
||||
return
|
||||
|
||||
self._done()
|
||||
|
||||
def _done(self):
|
||||
end_time = time.time()
|
||||
self.setProgress(1.0)
|
||||
|
||||
# check if the length matches
|
||||
size = os.stat(self.path)[stat.ST_SIZE]
|
||||
# wav header is 44 bytes
|
||||
offsetLength = self._stop - self._start + 1
|
||||
expected = offsetLength * common.BYTES_PER_FRAME + 44
|
||||
if size != expected:
|
||||
# FIXME: handle errors better
|
||||
logger.warning('file size %d did not match expected size %d',
|
||||
size, expected)
|
||||
if (size - expected) % common.BYTES_PER_FRAME == 0:
|
||||
logger.warning('%d frames difference' % (
|
||||
(size - expected) / common.BYTES_PER_FRAME))
|
||||
else:
|
||||
logger.warning('non-integral amount of frames difference')
|
||||
|
||||
self.setAndRaiseException(FileSizeError(self.path,
|
||||
"File size %d did not match expected size %d" % (
|
||||
size, expected)))
|
||||
|
||||
if not self.exception and self._popen.returncode != 0:
|
||||
if self._errors:
|
||||
print "\n".join(self._errors)
|
||||
else:
|
||||
logger.warning('exit code %r', self._popen.returncode)
|
||||
self.exception = ReturnCodeError(self._popen.returncode)
|
||||
|
||||
self.quality = self._parser.getTrackQuality()
|
||||
self.duration = end_time - self._start_time
|
||||
self.speed = (offsetLength / 75.0) / self.duration
|
||||
|
||||
self.stop()
|
||||
return
|
||||
|
||||
|
||||
class ReadVerifyTrackTask(task.MultiSeparateTask):
|
||||
"""
|
||||
I am a task that reads and verifies a track using cdparanoia.
|
||||
I also encode the track.
|
||||
|
||||
The path where the file is stored can be changed if necessary, for
|
||||
example if the file name is too long.
|
||||
|
||||
@ivar path: the path where the file is to be stored.
|
||||
@ivar checksum: the checksum of the track; set if they match.
|
||||
@ivar testchecksum: the test checksum of the track.
|
||||
@ivar copychecksum: the copy checksum of the track.
|
||||
@ivar testspeed: the test speed of the track, as a multiple of
|
||||
track duration.
|
||||
@ivar copyspeed: the copy speed of the track, as a multiple of
|
||||
track duration.
|
||||
@ivar testduration: the test duration of the track, in seconds.
|
||||
@ivar copyduration: the copy duration of the track, in seconds.
|
||||
@ivar peak: the peak level of the track
|
||||
"""
|
||||
|
||||
checksum = None
|
||||
testchecksum = None
|
||||
copychecksum = None
|
||||
peak = None
|
||||
quality = None
|
||||
testspeed = None
|
||||
copyspeed = None
|
||||
testduration = None
|
||||
copyduration = None
|
||||
|
||||
_tmpwavpath = None
|
||||
_tmppath = None
|
||||
|
||||
def __init__(self, path, table, start, stop, overread, offset=0,
|
||||
device=None, taglist=None, what="track"):
|
||||
"""
|
||||
@param path: where to store the ripped track
|
||||
@type path: str
|
||||
@param table: table of contents of CD
|
||||
@type table: L{table.Table}
|
||||
@param start: first frame to rip
|
||||
@type start: int
|
||||
@param stop: last frame to rip (inclusive)
|
||||
@type stop: int
|
||||
@param offset: read offset, in samples
|
||||
@type offset: int
|
||||
@param device: the device to rip from
|
||||
@type device: str
|
||||
@param taglist: a dict of tags
|
||||
@type taglist: dict
|
||||
"""
|
||||
task.MultiSeparateTask.__init__(self)
|
||||
|
||||
logger.debug('Creating read and verify task on %r', path)
|
||||
|
||||
if 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)
|
||||
os.close(fd)
|
||||
self._tmpwavpath = tmppath
|
||||
|
||||
from whipper.common import checksum
|
||||
|
||||
self.tasks = []
|
||||
self.tasks.append(
|
||||
ReadTrackTask(tmppath, table, start, stop, overread,
|
||||
offset=offset, device=device, what=what))
|
||||
self.tasks.append(checksum.CRC32Task(tmppath))
|
||||
t = ReadTrackTask(tmppath, table, start, stop, overread,
|
||||
offset=offset, device=device, action="Verifying", what=what)
|
||||
self.tasks.append(t)
|
||||
self.tasks.append(checksum.CRC32Task(tmppath))
|
||||
|
||||
# encode to the final path + '.part'
|
||||
try:
|
||||
tmpoutpath = path + u'.part'
|
||||
open(tmpoutpath, 'wb').close()
|
||||
except IOError, e:
|
||||
if errno.ENAMETOOLONG != e.errno:
|
||||
raise
|
||||
path = common.shrinkPath(path)
|
||||
tmpoutpath = path + u'.part'
|
||||
open(tmpoutpath, 'wb').close()
|
||||
self._tmppath = tmpoutpath
|
||||
self.path = path
|
||||
|
||||
from whipper.common import encode
|
||||
|
||||
self.tasks.append(encode.FlacEncodeTask(tmppath, tmpoutpath))
|
||||
|
||||
# MerlijnWajer: XXX: We run the CRC32Task on the wav file, because it's
|
||||
# in general stupid to run the CRC32 on the flac file since it already
|
||||
# has --verify. We should just get rid of this CRC32 step.
|
||||
# make sure our encoding is accurate
|
||||
self.tasks.append(checksum.CRC32Task(tmppath))
|
||||
self.tasks.append(encode.SoxPeakTask(tmppath))
|
||||
|
||||
# TODO: Move tagging outside of cdparanoia
|
||||
self.tasks.append(encode.TaggingTask(tmpoutpath, taglist))
|
||||
|
||||
self.checksum = None
|
||||
|
||||
def stop(self):
|
||||
# FIXME: maybe this kind of try-wrapping to make sure
|
||||
# we chain up should be handled by a parent class function ?
|
||||
try:
|
||||
if not self.exception:
|
||||
self.quality = max(self.tasks[0].quality,
|
||||
self.tasks[2].quality)
|
||||
self.peak = self.tasks[6].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
|
||||
self.copyduration = self.tasks[2].duration
|
||||
|
||||
self.testchecksum = c1 = self.tasks[1].checksum
|
||||
self.copychecksum = c2 = self.tasks[3].checksum
|
||||
if c1 == c2:
|
||||
logger.info('Checksums match, %08x' % c1)
|
||||
self.checksum = self.testchecksum
|
||||
else:
|
||||
# FIXME: detect this before encoding
|
||||
logger.info('Checksums do not match, %08x %08x' % (
|
||||
c1, c2))
|
||||
self.exception = ChecksumException(
|
||||
'read and verify failed: test checksum')
|
||||
|
||||
if self.tasks[5].checksum != self.checksum:
|
||||
self.exception = ChecksumException(
|
||||
'Encoding failed, checksum does not match')
|
||||
|
||||
# delete the unencoded file
|
||||
os.unlink(self._tmpwavpath)
|
||||
|
||||
if not self.exception:
|
||||
try:
|
||||
logger.debug('Moving to final path %r', self.path)
|
||||
os.rename(self._tmppath, self.path)
|
||||
except Exception, e:
|
||||
logger.debug('Exception while moving to final path %r: '
|
||||
'%r',
|
||||
self.path, str(e))
|
||||
self.exception = e
|
||||
else:
|
||||
os.unlink(self._tmppath)
|
||||
else:
|
||||
logger.debug('stop: exception %r', self.exception)
|
||||
except Exception, e:
|
||||
print 'WARNING: unhandled exception %r' % (e, )
|
||||
|
||||
task.MultiSeparateTask.stop(self)
|
||||
|
||||
_VERSION_RE = re.compile(
|
||||
"^cdparanoia (?P<version>.+) release (?P<release>.+) \(.*\)")
|
||||
|
||||
|
||||
def getCdParanoiaVersion():
|
||||
getter = common.VersionGetter('cdparanoia',
|
||||
["cdparanoia", "-V"],
|
||||
_VERSION_RE,
|
||||
"%(version)s %(release)s")
|
||||
|
||||
return getter.get()
|
||||
|
||||
|
||||
_OK_RE = re.compile(r'Drive tests OK with Paranoia.')
|
||||
_WARNING_RE = re.compile(r'WARNING! PARANOIA MAY NOT BE')
|
||||
|
||||
|
||||
class AnalyzeTask(ctask.PopenTask):
|
||||
|
||||
logCategory = 'AnalyzeTask'
|
||||
description = 'Analyzing drive caching behaviour'
|
||||
|
||||
defeatsCache = None
|
||||
|
||||
cwd = None
|
||||
|
||||
_output = []
|
||||
|
||||
def __init__(self, device=None):
|
||||
# cdparanoia -A *always* writes cdparanoia.log
|
||||
self.cwd = tempfile.mkdtemp(suffix='.morituri.cache')
|
||||
self.command = ['cdparanoia', '-A']
|
||||
if device:
|
||||
self.command += ['-d', device]
|
||||
|
||||
def commandMissing(self):
|
||||
raise common.MissingDependencyException('cdparanoia')
|
||||
|
||||
def readbyteserr(self, bytes):
|
||||
self._output.append(bytes)
|
||||
|
||||
def done(self):
|
||||
if self.cwd:
|
||||
shutil.rmtree(self.cwd)
|
||||
output = "".join(self._output)
|
||||
m = _OK_RE.search(output)
|
||||
if m:
|
||||
self.defeatsCache = True
|
||||
else:
|
||||
self.defeatsCache = False
|
||||
|
||||
def failed(self):
|
||||
# cdparanoia exits with return code 1 if it can't determine
|
||||
# whether it can defeat the audio cache
|
||||
output = "".join(self._output)
|
||||
m = _WARNING_RE.search(output)
|
||||
if m:
|
||||
self.defeatsCache = False
|
||||
if self.cwd:
|
||||
shutil.rmtree(self.cwd)
|
||||
82
whipper/program/cdrdao.py
Normal file
82
whipper/program/cdrdao.py
Normal file
@@ -0,0 +1,82 @@
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
from whipper.common.common import EjectError
|
||||
from whipper.image.toc import TocFile
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CDRDAO = 'cdrdao'
|
||||
|
||||
def read_toc(device, fast_toc=False):
|
||||
"""
|
||||
Return cdrdao-generated table of contents for 'device'.
|
||||
"""
|
||||
# cdrdao MUST be passed a non-existing filename as its last argument
|
||||
# to write the TOC to; it does not support writing to stdout or
|
||||
# overwriting an existing file, nor does linux seem to support
|
||||
# locking a non-existant file. Thus, this race-condition introducing
|
||||
# hack is carried from morituri to whipper and will be removed when
|
||||
# cdrdao is fixed.
|
||||
fd, tocfile = tempfile.mkstemp(suffix=u'.cdrdao.read-toc.whipper')
|
||||
os.close(fd)
|
||||
os.unlink(tocfile)
|
||||
|
||||
cmd = [CDRDAO, 'read-toc'] + (['--fast-toc'] if fast_toc else []) + [
|
||||
'--device', device, tocfile]
|
||||
# PIPE is the closest to >/dev/null we can get
|
||||
logger.debug("executing %r", cmd)
|
||||
p = Popen(cmd, stdout=PIPE, stderr=PIPE)
|
||||
_, stderr = p.communicate()
|
||||
if p.returncode != 0:
|
||||
msg = 'cdrdao read-toc failed: return code is non-zero: ' + \
|
||||
str(p.returncode)
|
||||
logger.critical(msg)
|
||||
# Gracefully handle missing disc
|
||||
if "ERROR: Unit not ready, giving up." in stderr:
|
||||
raise EjectError(device, "no disc detected")
|
||||
raise IOError(msg)
|
||||
|
||||
toc = TocFile(tocfile)
|
||||
toc.parse()
|
||||
os.unlink(tocfile)
|
||||
return toc
|
||||
|
||||
def version():
|
||||
"""
|
||||
Return cdrdao version as a string.
|
||||
"""
|
||||
cdrdao = Popen(CDRDAO, stderr=PIPE)
|
||||
out, err = cdrdao.communicate()
|
||||
if cdrdao.returncode != 1:
|
||||
logger.warning("cdrdao version detection failed: "
|
||||
"return code is " + str(cdrdao.returncode))
|
||||
return None
|
||||
m = re.compile(r'^Cdrdao version (?P<version>.*) - \(C\)').search(
|
||||
err.decode('utf-8'))
|
||||
if not m:
|
||||
logger.warning("cdrdao version detection failed: "
|
||||
"could not find version")
|
||||
return None
|
||||
return m.group('version')
|
||||
|
||||
def ReadTOCTask(device):
|
||||
"""
|
||||
stopgap morituri-insanity compatibility layer
|
||||
"""
|
||||
return read_toc(device, fast_toc=True)
|
||||
|
||||
def ReadTableTask(device):
|
||||
"""
|
||||
stopgap morituri-insanity compatibility layer
|
||||
"""
|
||||
return read_toc(device)
|
||||
|
||||
def getCDRDAOVersion():
|
||||
"""
|
||||
stopgap morituri-insanity compatibility layer
|
||||
"""
|
||||
return version()
|
||||
18
whipper/program/flac.py
Normal file
18
whipper/program/flac.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from subprocess import check_call, CalledProcessError
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def encode(infile, outfile):
|
||||
"""
|
||||
Encodes infile to outfile, with flac.
|
||||
Uses '-f' because morituri already creates the file.
|
||||
"""
|
||||
try:
|
||||
# TODO: Replace with Popen so that we can catch stderr and write it to
|
||||
# logging
|
||||
check_call(['flac', '--silent', '--verify', '-o', outfile,
|
||||
'-f', infile])
|
||||
except CalledProcessError:
|
||||
logger.exception('flac failed')
|
||||
raise
|
||||
26
whipper/program/sox.py
Normal file
26
whipper/program/sox.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import os
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SOX = 'sox'
|
||||
|
||||
def peak_level(track_path):
|
||||
"""
|
||||
Accepts a path to a sox-decodable audio file.
|
||||
|
||||
Returns track peak level from sox ('maximum amplitude') as a float.
|
||||
Returns None on error.
|
||||
"""
|
||||
if not os.path.exists(track_path):
|
||||
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:
|
||||
logger.warning("SoX peak detection failed: " + str(sox.returncode))
|
||||
return None
|
||||
# relevant captured line looks like:
|
||||
# Maximum amplitude: 0.123456
|
||||
return float(err.splitlines()[3].split()[2])
|
||||
49
whipper/program/soxi.py
Normal file
49
whipper/program/soxi.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import os
|
||||
|
||||
from whipper.common import common
|
||||
from whipper.common import task as ctask
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SOXI = 'soxi'
|
||||
|
||||
class AudioLengthTask(ctask.PopenTask):
|
||||
"""
|
||||
I calculate the length of a track in audio samples.
|
||||
|
||||
@ivar length: length of the decoded audio file, in audio samples.
|
||||
"""
|
||||
logCategory = 'AudioLengthTask'
|
||||
description = 'Getting length of audio track'
|
||||
length = None
|
||||
|
||||
def __init__(self, path):
|
||||
"""
|
||||
@type path: unicode
|
||||
"""
|
||||
assert type(path) is unicode, "%r is not unicode" % path
|
||||
|
||||
self.logName = os.path.basename(path).encode('utf-8')
|
||||
|
||||
self.command = [SOXI, '-s', path]
|
||||
|
||||
self._error = []
|
||||
self._output = []
|
||||
|
||||
def commandMissing(self):
|
||||
raise common.MissingDependencyException('soxi')
|
||||
|
||||
def readbytesout(self, bytes):
|
||||
self._output.append(bytes)
|
||||
|
||||
def readbyteserr(self, bytes):
|
||||
self._error.append(bytes)
|
||||
|
||||
def failed(self):
|
||||
self.setException(Exception("soxi failed: %s"%"".join(self._error)))
|
||||
|
||||
def done(self):
|
||||
if self._error:
|
||||
logger.warning("soxi reported on stderr: %s", "".join(self._error))
|
||||
self.length = int("".join(self._output))
|
||||
35
whipper/program/utils.py
Normal file
35
whipper/program/utils.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import os
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def eject_device(device):
|
||||
"""
|
||||
Eject the given device.
|
||||
"""
|
||||
logger.debug("ejecting device %s", device)
|
||||
os.system('eject %s' % device)
|
||||
|
||||
|
||||
def load_device(device):
|
||||
"""
|
||||
Load the given device.
|
||||
"""
|
||||
logger.debug("loading (eject -t) device %s", device)
|
||||
os.system('eject -t %s' % device)
|
||||
|
||||
|
||||
def unmount_device(device):
|
||||
"""
|
||||
Unmount the given device if it is mounted, as happens with automounted
|
||||
data tracks.
|
||||
|
||||
If the given device is a symlink, the target will be checked.
|
||||
"""
|
||||
device = os.path.realpath(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
|
||||
os.system('umount %s' % device)
|
||||
0
whipper/result/__init__.py
Normal file
0
whipper/result/__init__.py
Normal file
224
whipper/result/logger.py
Normal file
224
whipper/result/logger.py
Normal file
@@ -0,0 +1,224 @@
|
||||
import time
|
||||
import hashlib
|
||||
|
||||
import whipper
|
||||
|
||||
from whipper.common import common
|
||||
from whipper.result import result
|
||||
|
||||
|
||||
class MorituriLogger(result.Logger):
|
||||
|
||||
_accuratelyRipped = 0
|
||||
_inARDatabase = 0
|
||||
_errors = False
|
||||
|
||||
def log(self, ripResult, epoch=time.time()):
|
||||
"""Returns big str: logfile joined text lines"""
|
||||
|
||||
lines = self.logRip(ripResult, epoch=epoch)
|
||||
return "\n".join(lines)
|
||||
|
||||
def logRip(self, ripResult, epoch):
|
||||
"""Returns logfile lines list"""
|
||||
|
||||
lines = []
|
||||
|
||||
# Ripper version
|
||||
lines.append("Log created by: whipper %s (internal logger)" %
|
||||
whipper.__version__)
|
||||
|
||||
# Rip date
|
||||
date = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(epoch)).strip()
|
||||
lines.append("Log creation date: %s" % date)
|
||||
lines.append("")
|
||||
|
||||
# Rip technical settings
|
||||
lines.append("Ripping phase information:")
|
||||
lines.append(" Drive: %s%s (revision %s)" % (
|
||||
ripResult.vendor, ripResult.model, ripResult.release))
|
||||
if ripResult.cdparanoiaDefeatsCache is None:
|
||||
defeat = "Unknown"
|
||||
elif ripResult.cdparanoiaDefeatsCache:
|
||||
defeat = "Yes"
|
||||
else:
|
||||
defeat = "No"
|
||||
lines.append(" Defeat audio cache: %s" % defeat)
|
||||
lines.append(" Read offset correction: %+d" % ripResult.offset)
|
||||
# Currently unsupported by the official cdparanoia package
|
||||
over = "No"
|
||||
# Only implemented in whipper (ripResult.overread)
|
||||
if ripResult.overread:
|
||||
over = "Yes"
|
||||
lines.append(" Overread into lead-out: %s" % over)
|
||||
# Next one fully works only using the patched cdparanoia package
|
||||
# lines.append("Fill up missing offset samples with silence: Yes")
|
||||
lines.append(" Gap detection: cdrdao %s" % ripResult.cdrdaoVersion)
|
||||
lines.append("")
|
||||
|
||||
# CD metadata
|
||||
lines.append("CD metadata:")
|
||||
lines.append(" Album: %s - %s" % (ripResult.artist, ripResult.title))
|
||||
lines.append(" CDDB Disc ID: %s" % ripResult. table.getCDDBDiscId())
|
||||
lines.append(" MusicBrainz Disc ID: %s" %
|
||||
ripResult. table.getMusicBrainzDiscId())
|
||||
lines.append(" MusicBrainz lookup url: %s" %
|
||||
ripResult. table.getMusicBrainzSubmitURL())
|
||||
lines.append("")
|
||||
|
||||
# TOC section
|
||||
lines.append("TOC:")
|
||||
table = ripResult.table
|
||||
|
||||
# Test for HTOA presence
|
||||
htoa = None
|
||||
try:
|
||||
htoa = table.tracks[0].getIndex(0)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# If True, include HTOA line into log's TOC
|
||||
if htoa and htoa.path:
|
||||
htoastart = htoa.absolute
|
||||
htoaend = table.getTrackEnd(0)
|
||||
htoalength = table.tracks[0].getIndex(1).absolute - htoastart
|
||||
lines.append(" 00:")
|
||||
lines.append(" Start: %s" % common.framesToMSF(htoastart))
|
||||
lines.append(" Length: %s" % common.framesToMSF(htoalength))
|
||||
lines.append(" Start sector: %d" % htoastart)
|
||||
lines.append(" End sector: %d" % htoaend)
|
||||
lines.append("")
|
||||
|
||||
# For every track include information in the TOC
|
||||
for t in table.tracks:
|
||||
# FIXME: what happens to a track start over 60 minutes ?
|
||||
# Answer: tested empirically, everything seems OK
|
||||
start = t.getIndex(1).absolute
|
||||
length = table.getTrackLength(t.number)
|
||||
end = table.getTrackEnd(t.number)
|
||||
lines.append(" %02d:" % t.number)
|
||||
lines.append(" Start: %s" % common.framesToMSF(start))
|
||||
lines.append(" Length: %s" % common.framesToMSF(length))
|
||||
lines.append(" Start sector: %d" % start)
|
||||
lines.append(" End sector: %d" % end)
|
||||
lines.append("")
|
||||
|
||||
# Tracks section
|
||||
lines.append("Tracks:")
|
||||
duration = 0.0
|
||||
for t in ripResult.tracks:
|
||||
if not t.filename:
|
||||
continue
|
||||
lines.extend(self.trackLog(t))
|
||||
lines.append("")
|
||||
duration += t.testduration + t.copyduration
|
||||
|
||||
# Status report
|
||||
lines.append("Conclusive status report:")
|
||||
arHeading = " AccurateRip summary:"
|
||||
if self._inARDatabase == 0:
|
||||
lines.append("%s None of the tracks are present in the "
|
||||
"AccurateRip database" % arHeading)
|
||||
else:
|
||||
nonHTOA = len(ripResult.tracks)
|
||||
if ripResult.tracks[0].number == 0:
|
||||
nonHTOA -= 1
|
||||
if self._accuratelyRipped == 0:
|
||||
lines.append("%s No tracks could be verified as accurate "
|
||||
"(you may have a different pressing from the "
|
||||
"one(s) in the database)" % arHeading)
|
||||
elif self._accuratelyRipped < nonHTOA:
|
||||
accurateTracks = nonHTOA - self._accuratelyRipped
|
||||
lines.append("%s Some tracks could not be verified as "
|
||||
"accurate (%d/%d got no match)" % (
|
||||
arHeading, accurateTracks, nonHTOA))
|
||||
else:
|
||||
lines.append("%s All tracks accurately ripped" % arHeading)
|
||||
|
||||
hsHeading = " Health status:"
|
||||
if self._errors:
|
||||
lines.append("%s There were errors" % hsHeading)
|
||||
else:
|
||||
lines.append("%s No errors occurred" % hsHeading)
|
||||
lines.append(" EOF: End of status report")
|
||||
lines.append("")
|
||||
|
||||
# Log hash
|
||||
hasher = hashlib.sha256()
|
||||
hasher.update("\n".join(lines).encode("utf-8"))
|
||||
lines.append("SHA-256 hash: %s" % hasher.hexdigest().upper())
|
||||
lines.append("")
|
||||
return lines
|
||||
|
||||
def trackLog(self, trackResult):
|
||||
"""Returns Tracks section lines: data picked from trackResult"""
|
||||
|
||||
lines = []
|
||||
|
||||
# Track number
|
||||
lines.append(" %02d:" % trackResult.number)
|
||||
|
||||
# Filename (including path) of ripped track
|
||||
lines.append(" Filename: %s" % trackResult.filename)
|
||||
|
||||
# Pre-gap length
|
||||
pregap = trackResult.pregap
|
||||
if pregap:
|
||||
lines.append(" Pre-gap length: %s" % common.framesToMSF(pregap))
|
||||
|
||||
# Peak level
|
||||
peak = trackResult.peak
|
||||
lines.append(" Peak level: %.6f" % peak)
|
||||
|
||||
# Pre-emphasis status
|
||||
# Only implemented in whipper (trackResult.pre_emphasis)
|
||||
if trackResult.pre_emphasis:
|
||||
preEmph = "Yes"
|
||||
else:
|
||||
preEmph = "No"
|
||||
lines.append(" Pre-emphasis: %s" % preEmph)
|
||||
|
||||
# Extraction speed
|
||||
if trackResult.copyspeed:
|
||||
lines.append(" Extraction speed: %.1f X" % (
|
||||
trackResult.copyspeed))
|
||||
|
||||
# Extraction quality
|
||||
if trackResult.quality and trackResult.quality > 0.001:
|
||||
lines.append(" Extraction quality: %.2f %%" %
|
||||
(trackResult.quality * 100.0, ))
|
||||
|
||||
# Ripper Test CRC
|
||||
if trackResult.testcrc is not None:
|
||||
lines.append(" Test CRC: %08X" % trackResult.testcrc)
|
||||
|
||||
# Ripper Copy CRC
|
||||
if trackResult.copycrc is not None:
|
||||
lines.append(" Copy CRC: %08X" % trackResult.copycrc)
|
||||
|
||||
# AccurateRip track status
|
||||
# Currently there's no support for AccurateRip V2
|
||||
if trackResult.accurip:
|
||||
lines.append(" AccurateRip V1:")
|
||||
self._inARDatabase += 1
|
||||
if trackResult.ARCRC == trackResult.ARDBCRC:
|
||||
lines.append(" Result: Found, exact match")
|
||||
self._accuratelyRipped += 1
|
||||
else:
|
||||
lines.append(" Result: Found, NO exact match")
|
||||
lines.append(" Confidence: %d" %
|
||||
trackResult.ARDBConfidence)
|
||||
lines.append(" Local CRC: %08X" % trackResult.ARCRC)
|
||||
lines.append(" Remote CRC: %08X" % trackResult.ARDBCRC)
|
||||
elif trackResult.number != 0:
|
||||
lines.append(" AccurateRip V1:")
|
||||
lines.append(" Result: Track not present in "
|
||||
"AccurateRip database")
|
||||
|
||||
# Check if Test & Copy CRCs are equal
|
||||
if trackResult.testcrc == trackResult.copycrc:
|
||||
lines.append(" Status: Copy OK")
|
||||
else:
|
||||
self._errors = True
|
||||
lines.append(" Status: Error, CRC mismatch")
|
||||
return lines
|
||||
172
whipper/result/result.py
Normal file
172
whipper/result/result.py
Normal file
@@ -0,0 +1,172 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_result_result -*-
|
||||
# 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 pkg_resources
|
||||
import time
|
||||
|
||||
|
||||
class TrackResult:
|
||||
"""
|
||||
@type filename: unicode
|
||||
@ivar testcrc: 4-byte CRC for the test read
|
||||
@type testcrc: int
|
||||
@ivar copycrc: 4-byte CRC for the copy read
|
||||
@type copycrc: int
|
||||
|
||||
@var accurip: whether this track's AR CRC was found in the
|
||||
database, and thus whether the track is considered
|
||||
accurately ripped.
|
||||
If false, it can be ripped wrong, not exist in
|
||||
the database, ...
|
||||
@type accurip: bool
|
||||
|
||||
@var ARCRC: our calculated 4 byte AccurateRip CRC for this
|
||||
track.
|
||||
@type ARCRC: int
|
||||
|
||||
@var ARDBCRC: the 4-byte AccurateRip CRC this
|
||||
track did or should have matched in the database.
|
||||
If None, the track is not in the database.
|
||||
@type ARDBCRC: int
|
||||
@var ARDBConfidence: confidence for the matched AccurateRip CRC for
|
||||
this track in the database.
|
||||
If None, the track is not in the database.
|
||||
@var ARDBMaxConfidence: maximum confidence in the AccurateRip database for
|
||||
this track; can still be 0.
|
||||
If None, the track is not in the database.
|
||||
"""
|
||||
number = None
|
||||
filename = None
|
||||
pregap = 0 # in frames
|
||||
pre_emphasis = None
|
||||
|
||||
peak = 0.0
|
||||
quality = 0.0
|
||||
testspeed = 0.0
|
||||
copyspeed = 0.0
|
||||
testduration = 0.0
|
||||
copyduration = 0.0
|
||||
testcrc = None
|
||||
copycrc = None
|
||||
accurip = False # whether it's in the database
|
||||
ARCRC = None
|
||||
ARDBCRC = None
|
||||
ARDBConfidence = None
|
||||
ARDBMaxConfidence = None
|
||||
|
||||
classVersion = 3
|
||||
|
||||
|
||||
class RipResult:
|
||||
"""
|
||||
I hold information about the result for rips.
|
||||
I can be used to write log files.
|
||||
|
||||
@ivar offset: sample read offset
|
||||
@ivar table: the full index table
|
||||
@type table: L{whipper.image.table.Table}
|
||||
|
||||
@ivar vendor: vendor of the CD drive
|
||||
@ivar model: model of the CD drive
|
||||
@ivar release: release of the CD drive
|
||||
|
||||
@ivar cdrdaoVersion: version of cdrdao used for the rip
|
||||
@ivar cdparanoiaVersion: version of cdparanoia used for the rip
|
||||
"""
|
||||
|
||||
offset = 0
|
||||
overread = None
|
||||
logger = None
|
||||
table = None
|
||||
artist = None
|
||||
title = None
|
||||
|
||||
vendor = None
|
||||
model = None
|
||||
release = None
|
||||
|
||||
cdrdaoVersion = None
|
||||
cdparanoiaVersion = None
|
||||
cdparanoiaDefeatsCache = None
|
||||
|
||||
classVersion = 3
|
||||
|
||||
def __init__(self):
|
||||
self.tracks = []
|
||||
|
||||
def getTrackResult(self, number):
|
||||
"""
|
||||
@param number: the track number (0 for HTOA)
|
||||
|
||||
@type number: int
|
||||
@rtype: L{TrackResult}
|
||||
"""
|
||||
for t in self.tracks:
|
||||
if t.number == number:
|
||||
return t
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class Logger(object):
|
||||
"""
|
||||
I log the result of a rip.
|
||||
"""
|
||||
|
||||
def log(self, ripResult, epoch=time.time()):
|
||||
"""
|
||||
Create a log from the given ripresult.
|
||||
|
||||
@param epoch: when the log file gets generated
|
||||
@type epoch: float
|
||||
@type ripResult: L{RipResult}
|
||||
|
||||
@rtype: str
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
# A setuptools-like entry point
|
||||
|
||||
|
||||
class EntryPoint(object):
|
||||
name = 'whipper'
|
||||
|
||||
def load(self):
|
||||
from whipper.result import logger
|
||||
return logger.MorituriLogger
|
||||
|
||||
|
||||
def getLoggers():
|
||||
"""
|
||||
Get all logger plugins with entry point 'whipper.logger'.
|
||||
|
||||
@rtype: dict of C{str} -> C{Logger}
|
||||
"""
|
||||
d = {}
|
||||
|
||||
pluggables = list(pkg_resources.iter_entry_points("whipper.logger"))
|
||||
for entrypoint in [EntryPoint(), ] + pluggables:
|
||||
plugin_class = entrypoint.load()
|
||||
d[entrypoint.name] = plugin_class
|
||||
|
||||
return d
|
||||
0
whipper/test/__init__.py
Normal file
0
whipper/test/__init__.py
Normal file
38
whipper/test/bloc.cue
Normal file
38
whipper/test/bloc.cue
Normal file
@@ -0,0 +1,38 @@
|
||||
REM DISCID AD0BE00D
|
||||
REM COMMENT "whipper 0.5.1"
|
||||
FILE "data.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
PREGAP 03:22:70
|
||||
INDEX 01 00:00:00
|
||||
TRACK 02 AUDIO
|
||||
INDEX 01 04:21:74
|
||||
TRACK 03 AUDIO
|
||||
INDEX 01 08:02:12
|
||||
TRACK 04 AUDIO
|
||||
INDEX 01 11:57:45
|
||||
TRACK 05 AUDIO
|
||||
INDEX 00 15:18:00
|
||||
INDEX 01 15:18:72
|
||||
TRACK 06 AUDIO
|
||||
INDEX 00 18:05:40
|
||||
INDEX 01 18:06:06
|
||||
TRACK 07 AUDIO
|
||||
INDEX 00 21:35:15
|
||||
INDEX 01 21:35:32
|
||||
TRACK 08 AUDIO
|
||||
INDEX 00 26:00:74
|
||||
INDEX 01 26:01:03
|
||||
TRACK 09 AUDIO
|
||||
INDEX 00 29:36:14
|
||||
INDEX 01 29:36:25
|
||||
TRACK 10 AUDIO
|
||||
INDEX 01 33:56:02
|
||||
TRACK 11 AUDIO
|
||||
INDEX 00 37:48:26
|
||||
INDEX 01 37:48:69
|
||||
TRACK 12 AUDIO
|
||||
INDEX 00 41:44:45
|
||||
INDEX 01 41:46:11
|
||||
TRACK 13 AUDIO
|
||||
INDEX 00 45:56:11
|
||||
INDEX 01 45:56:33
|
||||
116
whipper/test/bloc.toc
Normal file
116
whipper/test/bloc.toc
Normal file
@@ -0,0 +1,116 @@
|
||||
CD_DA
|
||||
|
||||
|
||||
// Track 1
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
SILENCE 03:22:70
|
||||
FILE "data.wav" 0 04:21:74
|
||||
START 03:22:70
|
||||
|
||||
|
||||
// Track 2
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
FILE "data.wav" 04:21:74 03:40:13
|
||||
|
||||
|
||||
// Track 3
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
FILE "data.wav" 08:02:12 03:55:33
|
||||
|
||||
|
||||
// Track 4
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
FILE "data.wav" 11:57:45 03:20:30
|
||||
|
||||
|
||||
// Track 5
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
FILE "data.wav" 15:18:00 02:47:40
|
||||
START 00:00:72
|
||||
|
||||
|
||||
// Track 6
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
FILE "data.wav" 18:05:40 03:29:50
|
||||
START 00:00:41
|
||||
|
||||
|
||||
// Track 7
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
FILE "data.wav" 21:35:15 04:25:59
|
||||
START 00:00:17
|
||||
|
||||
|
||||
// Track 8
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
FILE "data.wav" 26:00:74 03:35:15
|
||||
START 00:00:04
|
||||
|
||||
|
||||
// Track 9
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
FILE "data.wav" 29:36:14 04:19:63
|
||||
START 00:00:11
|
||||
|
||||
|
||||
// Track 10
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
FILE "data.wav" 33:56:02 03:52:24
|
||||
|
||||
|
||||
// Track 11
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
FILE "data.wav" 37:48:26 03:56:19
|
||||
START 00:00:43
|
||||
|
||||
|
||||
// Track 12
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
FILE "data.wav" 41:44:45 04:11:41
|
||||
START 00:01:41
|
||||
|
||||
|
||||
// Track 13
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
FILE "data.wav" 45:56:11 04:43:60
|
||||
START 00:00:22
|
||||
|
||||
64
whipper/test/breeders.cue
Normal file
64
whipper/test/breeders.cue
Normal file
@@ -0,0 +1,64 @@
|
||||
REM DISCID BE08990D
|
||||
REM COMMENT "whipper 0.5.1"
|
||||
CATALOG 0652637280326
|
||||
PERFORMER "THE BREEDERS"
|
||||
TITLE "MOUNTAIN BATTLES"
|
||||
FILE "data.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
TITLE "OVERGLAZED"
|
||||
ISRC GBAFL0700213
|
||||
INDEX 01 00:00:00
|
||||
TRACK 02 AUDIO
|
||||
TITLE "BANG ON"
|
||||
ISRC GBAFL0700214
|
||||
INDEX 00 02:14:51
|
||||
INDEX 01 02:15:26
|
||||
TRACK 03 AUDIO
|
||||
TITLE "NIGHT OF JOY"
|
||||
ISRC GBAFL0700215
|
||||
INDEX 00 04:17:74
|
||||
INDEX 01 04:18:34
|
||||
TRACK 04 AUDIO
|
||||
TITLE "WE'RE GONNA RISE"
|
||||
ISRC GBAFL0700216
|
||||
INDEX 01 07:44:22
|
||||
TRACK 05 AUDIO
|
||||
TITLE "GERMAN STUDIES"
|
||||
ISRC GBAFL0700217
|
||||
INDEX 01 11:37:39
|
||||
TRACK 06 AUDIO
|
||||
TITLE "SPARK"
|
||||
ISRC GBAFL0700218
|
||||
INDEX 00 13:51:54
|
||||
INDEX 01 13:53:38
|
||||
TRACK 07 AUDIO
|
||||
TITLE "INSTANBUL"
|
||||
ISRC GBAFL0700219
|
||||
INDEX 00 16:31:20
|
||||
INDEX 01 16:32:49
|
||||
TRACK 08 AUDIO
|
||||
TITLE "WALK IT OFF"
|
||||
ISRC GBAFL0700220
|
||||
INDEX 01 19:30:19
|
||||
TRACK 09 AUDIO
|
||||
TITLE "REGLAME ESTA NOCHE"
|
||||
ISRC GBAFL0700221
|
||||
INDEX 00 22:14:69
|
||||
INDEX 01 22:16:27
|
||||
TRACK 10 AUDIO
|
||||
TITLE "HERE NO MORE"
|
||||
ISRC GBAFL0700222
|
||||
INDEX 00 25:06:18
|
||||
INDEX 01 25:08:01
|
||||
TRACK 11 AUDIO
|
||||
TITLE "NO WAY"
|
||||
ISRC GBAFL0700223
|
||||
INDEX 01 27:46:64
|
||||
TRACK 12 AUDIO
|
||||
TITLE "IT'S THE LOVE"
|
||||
ISRC GBAFL0700224
|
||||
INDEX 01 30:19:39
|
||||
TRACK 13 AUDIO
|
||||
TITLE "MOUNTAIN BATTLES"
|
||||
ISRC GBAFL0700225
|
||||
INDEX 01 32:47:56
|
||||
217
whipper/test/breeders.toc
Normal file
217
whipper/test/breeders.toc
Normal file
@@ -0,0 +1,217 @@
|
||||
CD_DA
|
||||
|
||||
CATALOG "0652637280326"
|
||||
CD_TEXT {
|
||||
LANGUAGE_MAP {
|
||||
0: 9
|
||||
}
|
||||
LANGUAGE 0 {
|
||||
TITLE "MOUNTAIN BATTLES"
|
||||
PERFORMER "THE BREEDERS"
|
||||
DISC_ID "CADD2803CD"
|
||||
SIZE_INFO { 1, 1, 20, 0, 16, 3, 0, 0, 0, 0, 1, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 3, 22, 0, 0, 0,
|
||||
0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0}
|
||||
}
|
||||
}
|
||||
|
||||
// Track 1
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAFL0700213"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "OVERGLAZED"
|
||||
PERFORMER ""
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 0 02:14:51
|
||||
|
||||
|
||||
// Track 2
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAFL0700214"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "BANG ON"
|
||||
PERFORMER ""
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 02:14:51 02:03:23
|
||||
START 00:00:50
|
||||
|
||||
|
||||
// Track 3
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAFL0700215"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "NIGHT OF JOY"
|
||||
PERFORMER ""
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 04:17:74 03:26:23
|
||||
START 00:00:35
|
||||
|
||||
|
||||
// Track 4
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAFL0700216"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "WE'RE GONNA RISE"
|
||||
PERFORMER ""
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 07:44:22 03:53:17
|
||||
|
||||
|
||||
// Track 5
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAFL0700217"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "GERMAN STUDIES"
|
||||
PERFORMER ""
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 11:37:39 02:14:15
|
||||
|
||||
|
||||
// Track 6
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAFL0700218"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "SPARK"
|
||||
PERFORMER ""
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 13:51:54 02:39:41
|
||||
START 00:01:59
|
||||
|
||||
|
||||
// Track 7
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAFL0700219"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "INSTANBUL"
|
||||
PERFORMER ""
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 16:31:20 02:58:74
|
||||
START 00:01:29
|
||||
|
||||
|
||||
// Track 8
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAFL0700220"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "WALK IT OFF"
|
||||
PERFORMER ""
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 19:30:19 02:44:50
|
||||
|
||||
|
||||
// Track 9
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAFL0700221"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "REGLAME ESTA NOCHE"
|
||||
PERFORMER ""
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 22:14:69 02:51:24
|
||||
START 00:01:33
|
||||
|
||||
|
||||
// Track 10
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAFL0700222"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "HERE NO MORE"
|
||||
PERFORMER ""
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 25:06:18 02:40:46
|
||||
START 00:01:58
|
||||
|
||||
|
||||
// Track 11
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAFL0700223"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "NO WAY"
|
||||
PERFORMER ""
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 27:46:64 02:32:50
|
||||
|
||||
|
||||
// Track 12
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAFL0700224"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "IT'S THE LOVE"
|
||||
PERFORMER ""
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 30:19:39 02:28:17
|
||||
|
||||
|
||||
// Track 13
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAFL0700225"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "MOUNTAIN BATTLES"
|
||||
PERFORMER ""
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 32:47:56 03:53:66
|
||||
|
||||
BIN
whipper/test/cache/result/fe105a11.pickle
vendored
Normal file
BIN
whipper/test/cache/result/fe105a11.pickle
vendored
Normal file
Binary file not shown.
113
whipper/test/capital.1.toc
Normal file
113
whipper/test/capital.1.toc
Normal file
@@ -0,0 +1,113 @@
|
||||
CD_DA
|
||||
|
||||
|
||||
// Track 1
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300350"
|
||||
SILENCE 05:22:20
|
||||
FILE "data.wav" 0 04:32:55
|
||||
START 05:22:20
|
||||
|
||||
|
||||
// Track 2
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300351"
|
||||
FILE "data.wav" 04:32:55 04:16:02
|
||||
START 00:01:05
|
||||
|
||||
|
||||
// Track 3
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300352"
|
||||
FILE "data.wav" 08:48:57 03:03:65
|
||||
START 00:01:38
|
||||
|
||||
|
||||
// Track 4
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300353"
|
||||
FILE "data.wav" 11:52:47 02:16:03
|
||||
START 00:01:43
|
||||
|
||||
|
||||
// Track 5
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300355"
|
||||
FILE "data.wav" 14:08:50 03:32:55
|
||||
START 00:01:50
|
||||
|
||||
|
||||
// Track 6
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300356"
|
||||
FILE "data.wav" 17:41:30 03:09:70
|
||||
START 00:01:20
|
||||
|
||||
|
||||
// Track 7
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300357"
|
||||
FILE "data.wav" 20:51:25 02:27:25
|
||||
START 00:01:00
|
||||
|
||||
|
||||
// Track 8
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300358"
|
||||
FILE "data.wav" 23:18:50 02:46:35
|
||||
START 00:00:35
|
||||
|
||||
|
||||
// Track 9
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300359"
|
||||
FILE "data.wav" 26:05:10 05:02:72
|
||||
START 00:00:60
|
||||
|
||||
|
||||
// Track 10
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300360"
|
||||
FILE "data.wav" 31:08:07 03:50:38
|
||||
START 00:00:60
|
||||
|
||||
|
||||
// Track 11
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300361"
|
||||
FILE "data.wav" 34:58:45 03:35:10
|
||||
START 00:00:65
|
||||
|
||||
8
whipper/test/capital.2.toc
Normal file
8
whipper/test/capital.2.toc
Normal file
@@ -0,0 +1,8 @@
|
||||
CD_ROM
|
||||
|
||||
|
||||
// Track 1
|
||||
TRACK MODE1
|
||||
NO COPY
|
||||
DATAFILE "data_1" 27:30:00 // length in bytes: 253440000
|
||||
|
||||
109
whipper/test/capital.fast.toc
Normal file
109
whipper/test/capital.fast.toc
Normal file
@@ -0,0 +1,109 @@
|
||||
CD_ROM
|
||||
|
||||
|
||||
// Track 1
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300350"
|
||||
FILE "data.wav" 0 04:33:60
|
||||
|
||||
|
||||
// Track 2
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300351"
|
||||
FILE "data.wav" 04:33:60 04:16:35
|
||||
|
||||
|
||||
// Track 3
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300352"
|
||||
FILE "data.wav" 08:50:20 03:03:70
|
||||
|
||||
|
||||
// Track 4
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300353"
|
||||
FILE "data.wav" 11:54:15 02:16:10
|
||||
|
||||
|
||||
// Track 5
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300355"
|
||||
FILE "data.wav" 14:10:25 03:32:25
|
||||
|
||||
|
||||
// Track 6
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300356"
|
||||
FILE "data.wav" 17:42:50 03:09:50
|
||||
|
||||
|
||||
// Track 7
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300357"
|
||||
FILE "data.wav" 20:52:25 02:26:60
|
||||
|
||||
|
||||
// Track 8
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300358"
|
||||
FILE "data.wav" 23:19:10 02:46:60
|
||||
|
||||
|
||||
// Track 9
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300359"
|
||||
FILE "data.wav" 26:05:70 05:02:72
|
||||
|
||||
|
||||
// Track 10
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300360"
|
||||
FILE "data.wav" 31:08:67 03:50:43
|
||||
|
||||
|
||||
// Track 11
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBAAA0300361"
|
||||
FILE "data.wav" 34:59:35 06:04:20
|
||||
|
||||
|
||||
// Track 12
|
||||
TRACK MODE1
|
||||
NO COPY
|
||||
ZERO MODE1 00:02:00
|
||||
DATAFILE "data_12" 27:30:00 // length in bytes: 253440000
|
||||
START 00:02:00
|
||||
|
||||
1932
whipper/test/cdparanoia.progress
Normal file
1932
whipper/test/cdparanoia.progress
Normal file
File diff suppressed because it is too large
Load Diff
2501
whipper/test/cdparanoia.progress.error
Normal file
2501
whipper/test/cdparanoia.progress.error
Normal file
File diff suppressed because it is too large
Load Diff
111
whipper/test/cdparanoia.progress.strokes
Normal file
111
whipper/test/cdparanoia.progress.strokes
Normal file
@@ -0,0 +1,111 @@
|
||||
Sending all callbacks to stderr for wrapper script
|
||||
cdparanoia III release 10.2 (September 11, 2008)
|
||||
|
||||
Ripping from sector 0 (track 0 [0:00.00])
|
||||
to sector 0 (track 0 [0:00.00])
|
||||
|
||||
outputting to cdda.wav
|
||||
|
||||
##: 0 [read] @ 24696
|
||||
##: 0 [read] @ 56448
|
||||
##: 0 [read] @ 88200
|
||||
##: 0 [read] @ 119952
|
||||
##: 0 [read] @ 151704
|
||||
##: 0 [read] @ 183456
|
||||
##: 0 [read] @ 215208
|
||||
##: 0 [read] @ 246960
|
||||
##: 0 [read] @ 278712
|
||||
##: 0 [read] @ 310464
|
||||
##: 0 [read] @ 342216
|
||||
##: 0 [read] @ 373968
|
||||
##: 0 [read] @ 405720
|
||||
##: 0 [read] @ 437472
|
||||
##: 0 [read] @ 469224
|
||||
##: 0 [read] @ 500976
|
||||
##: 0 [read] @ 532728
|
||||
##: 0 [read] @ 564480
|
||||
##: 0 [read] @ 596232
|
||||
##: 0 [read] @ 627984
|
||||
##: 0 [read] @ 659736
|
||||
##: 0 [read] @ 691488
|
||||
##: 0 [read] @ 723240
|
||||
##: 0 [read] @ 754992
|
||||
##: 0 [read] @ 786744
|
||||
##: 0 [read] @ 818496
|
||||
##: 0 [read] @ 850248
|
||||
##: 0 [read] @ 882000
|
||||
##: 0 [read] @ 913752
|
||||
##: 0 [read] @ 945504
|
||||
##: 0 [read] @ 977256
|
||||
##: 0 [read] @ 1009008
|
||||
##: 0 [read] @ 1040760
|
||||
##: 0 [read] @ 1072512
|
||||
##: 0 [read] @ 1104264
|
||||
##: 0 [read] @ 1136016
|
||||
##: 0 [read] @ 1167768
|
||||
##: 0 [read] @ 1199520
|
||||
##: 0 [read] @ 1231272
|
||||
##: 0 [read] @ 1263024
|
||||
##: 0 [read] @ 1294776
|
||||
##: 0 [read] @ 1326528
|
||||
##: 0 [read] @ 1358280
|
||||
##: 0 [read] @ 1390032
|
||||
##: 0 [read] @ 1410024
|
||||
##: 0 [read] @ 23520
|
||||
##: 0 [read] @ 55272
|
||||
##: 0 [read] @ 87024
|
||||
##: 0 [read] @ 118776
|
||||
##: 0 [read] @ 150528
|
||||
##: 0 [read] @ 182280
|
||||
##: 0 [read] @ 214032
|
||||
##: 0 [read] @ 245784
|
||||
##: 0 [read] @ 277536
|
||||
##: 0 [read] @ 309288
|
||||
##: 0 [read] @ 341040
|
||||
##: 0 [read] @ 372792
|
||||
##: 0 [read] @ 404544
|
||||
##: 0 [read] @ 436296
|
||||
##: 0 [read] @ 468048
|
||||
##: 0 [read] @ 499800
|
||||
##: 0 [read] @ 531552
|
||||
##: 0 [read] @ 563304
|
||||
##: 0 [read] @ 595056
|
||||
##: 0 [read] @ 626808
|
||||
##: 0 [read] @ 658560
|
||||
##: 0 [read] @ 690312
|
||||
##: 0 [read] @ 722064
|
||||
##: 0 [read] @ 753816
|
||||
##: 0 [read] @ 785568
|
||||
##: 0 [read] @ 817320
|
||||
##: 0 [read] @ 849072
|
||||
##: 0 [read] @ 880824
|
||||
##: 0 [read] @ 912576
|
||||
##: 0 [read] @ 944328
|
||||
##: 0 [read] @ 976080
|
||||
##: 0 [read] @ 1007832
|
||||
##: 0 [read] @ 1039584
|
||||
##: 0 [read] @ 1071336
|
||||
##: 0 [read] @ 1103088
|
||||
##: 0 [read] @ 1134840
|
||||
##: 0 [read] @ 1166592
|
||||
##: 0 [read] @ 1198344
|
||||
##: 0 [read] @ 1230096
|
||||
##: 0 [read] @ 1261848
|
||||
##: 0 [read] @ 1293600
|
||||
##: 0 [read] @ 1325352
|
||||
##: 0 [read] @ 1357104
|
||||
##: 0 [read] @ 1388856
|
||||
##: 0 [read] @ 1410024
|
||||
##: 1 [verify] @ 0
|
||||
##: 3 [correction] @ 1005459
|
||||
##: 3 [correction] @ 1005480
|
||||
##: 1 [verify] @ 1005480
|
||||
##: 1 [verify] @ 1005480
|
||||
##: -2 [wrote] @ 1175
|
||||
##: -2 [wrote] @ 1176
|
||||
##: -1 [finished] @ 1175
|
||||
|
||||
|
||||
Done.
|
||||
|
||||
|
||||
373
whipper/test/cdparanoia/MATSHITA.cdparanoia-A.log
Normal file
373
whipper/test/cdparanoia/MATSHITA.cdparanoia-A.log
Normal file
@@ -0,0 +1,373 @@
|
||||
cdparanoia -A
|
||||
cdparanoia III release 10.2 (September 11, 2008)
|
||||
|
||||
Using cdda library version: 10.2
|
||||
Using paranoia library version: 10.2
|
||||
|
||||
Attempting to set cdrom to full speed...
|
||||
drive returned OK.
|
||||
|
||||
=================== Checking drive cache/timing behavior ===================
|
||||
|
||||
Seek/read timing:
|
||||
|
||||
[45:24.28]:
|
||||
204328:1:46 204329:27:33 204356:27:35 204383:27:33 204410:27:35 204437:27:33 204464:27:35 204491:27:33 204518:27:35 204545:27:33 204572:27:35 204599:27:32 204626:27:35 204653:27:33 204680:27:35 204707:27:33 204734:27:35 204761:27:33 204788:27:35 204815:27:33 204842:27:35 204869:27:33 204896:27:35 204923:27:32 204950:27:35 204977:27:33 205004:27:35 205031:27:33 205058:27:35 205085:27:33 205112:27:35 205139:27:33 205166:27:35 205193:27:33 205220:27:35 205247:27:33 205274:27:35 205301:27:33
|
||||
Initial seek latency (1000 sectors): 46ms
|
||||
Average read latency: 1.26ms/sector (raw speed: 10.6x)
|
||||
Read latency standard deviation: 0.04ms/sector
|
||||
[45:24.27]:
|
||||
204327:1:45 204328:27:33 204355:27:35 204382:27:33 204409:27:35 204436:27:33 204463:27:35 204490:27:33 204517:27:35 204544:27:32 204571:27:35 204598:27:32 204625:27:35 204652:27:33 204679:27:35 204706:27:33 204733:27:35 204760:27:33 204787:27:35 204814:27:33 204841:27:35 204868:27:32 204895:27:35 204922:27:32 204949:27:35 204976:27:33 205003:27:35 205030:27:33 205057:27:35 205084:27:33 205111:27:35 205138:27:33 205165:27:35 205192:27:33 205219:27:35 205246:27:33 205273:27:35 205300:27:33
|
||||
Initial seek latency (1000 sectors): 45ms
|
||||
Average read latency: 1.25ms/sector (raw speed: 10.6x)
|
||||
Read latency standard deviation: 0.04ms/sector
|
||||
[45:24.26]:
|
||||
204326:1:45 204327:27:33 204354:27:35 204381:27:33 204408:27:35 204435:27:33 204462:27:35 204489:27:33 204516:27:35 204543:27:33 204570:27:35 204597:27:33 204624:27:35 204651:27:33 204678:27:35 204705:27:33 204732:27:35 204759:27:33 204786:27:35 204813:27:33 204840:27:35 204867:27:33 204894:27:35 204921:27:33 204948:27:35 204975:27:33 205002:27:35 205029:27:33 205056:27:35 205083:27:32 205110:27:35 205137:27:33 205164:27:35 205191:27:33 205218:27:35 205245:27:33 205272:27:35 205299:27:32
|
||||
Initial seek latency (1000 sectors): 45ms
|
||||
Average read latency: 1.26ms/sector (raw speed: 10.6x)
|
||||
Read latency standard deviation: 0.04ms/sector
|
||||
[45:24.25]:
|
||||
204325:1:44 204326:27:33 204353:27:35 204380:27:33 204407:27:35 204434:27:32 204461:27:35 204488:27:33 204515:27:35 204542:27:33 204569:27:35 204596:27:33 204623:27:35 204650:27:33 204677:27:35 204704:27:33 204731:27:35 204758:27:32 204785:27:35 204812:27:33 204839:27:35 204866:27:33 204893:27:35 204920:27:33 204947:27:35 204974:27:33 205001:27:35 205028:27:33 205055:27:35 205082:27:33 205109:27:35 205136:27:33 205163:27:35 205190:27:32 205217:27:35 205244:27:33 205271:27:35 205298:27:33
|
||||
Initial seek latency (1000 sectors): 44ms
|
||||
Average read latency: 1.26ms/sector (raw speed: 10.6x)
|
||||
Read latency standard deviation: 0.04ms/sector
|
||||
[45:24.24]:
|
||||
204324:1:45 204325:27:33 204352:27:35 204379:27:32 204406:27:35 204433:27:33 204460:27:35 204487:27:33 204514:27:35 204541:27:33 204568:27:35 204595:27:32 204622:27:35 204649:27:33 204676:27:35 204703:27:33 204730:27:35 204757:27:33 204784:27:35 204811:27:32 204838:27:35 204865:27:33 204892:27:35 204919:27:33 204946:27:35 204973:27:33 205000:27:35 205027:27:33 205054:27:35 205081:27:33 205108:27:35 205135:27:33 205162:27:35 205189:27:33 205216:27:35 205243:27:33 205270:27:35 205297:27:33
|
||||
Initial seek latency (1000 sectors): 45ms
|
||||
Average read latency: 1.26ms/sector (raw speed: 10.6x)
|
||||
Read latency standard deviation: 0.04ms/sector
|
||||
[40:00.00]:
|
||||
180000:1:50 180001:27:34 180028:27:37 180055:27:34 180082:27:37 180109:27:34 180136:27:37 180163:27:34 180190:27:37 180217:27:34 180244:27:37 180271:27:34 180298:27:37 180325:27:34 180352:27:37 180379:27:34 180406:27:37 180433:27:34 180460:27:37 180487:27:34 180514:27:37 180541:27:34 180568:27:37 180595:27:34 180622:27:37 180649:27:34 180676:27:37 180703:27:34 180730:27:37 180757:27:34 180784:27:37 180811:27:34 180838:27:37 180865:27:34 180892:27:37 180919:27:34 180946:27:37 180973:27:34
|
||||
Initial seek latency (1000 sectors): 50ms
|
||||
Average read latency: 1.31ms/sector (raw speed: 10.2x)
|
||||
Read latency standard deviation: 0.06ms/sector
|
||||
[30:00.00]:
|
||||
135000:1:64 135001:27:38 135028:27:41 135055:27:38 135082:27:41 135109:27:38 135136:27:41 135163:27:38 135190:27:41 135217:27:38 135244:27:41 135271:27:38 135298:27:41 135325:27:38 135352:27:41 135379:27:38 135406:27:41 135433:27:38 135460:27:41 135487:27:38 135514:27:41 135541:27:38 135568:27:41 135595:27:38 135622:27:41 135649:27:38 135676:27:41 135703:27:38 135730:27:41 135757:27:38 135784:27:41 135811:27:38 135838:27:41 135865:27:38 135892:27:41 135919:27:38 135946:27:41 135973:27:38
|
||||
Initial seek latency (1000 sectors): 64ms
|
||||
Average read latency: 1.46ms/sector (raw speed: 9.1x)
|
||||
Read latency standard deviation: 0.06ms/sector
|
||||
[20:00.00]:
|
||||
90000:1:63 90001:27:43 90028:27:47 90055:27:43 90082:27:47 90109:27:43 90136:27:46 90163:27:43 90190:27:47 90217:27:43 90244:27:46 90271:27:43 90298:27:47 90325:27:43 90352:27:46 90379:27:43 90406:27:46 90433:27:43 90460:27:46 90487:27:43 90514:27:46 90541:27:43 90568:27:46 90595:27:43 90622:27:46 90649:27:43 90676:27:46 90703:27:43 90730:27:46 90757:27:43 90784:27:46 90811:27:43 90838:27:46 90865:27:43 90892:27:46 90919:27:43 90946:27:46 90973:27:43
|
||||
Initial seek latency (1000 sectors): 63ms
|
||||
Average read latency: 1.65ms/sector (raw speed: 8.1x)
|
||||
Read latency standard deviation: 0.06ms/sector
|
||||
[10:00.00]:
|
||||
45000:1:61 45001:27:51 45028:27:55 45055:27:51 45082:27:55 45109:27:52 45136:27:55 45163:27:51 45190:27:55 45217:27:51 45244:27:55 45271:27:51 45298:27:55 45325:27:51 45352:27:55 45379:27:51 45406:27:55 45433:27:51 45460:27:55 45487:27:51 45514:27:55 45541:27:51 45568:27:55 45595:27:51 45622:27:55 45649:27:51 45676:27:55 45703:27:51 45730:27:55 45757:27:51 45784:27:55 45811:27:51 45838:27:55 45865:27:51 45892:27:55 45919:27:51 45946:27:55 45973:27:51
|
||||
Initial seek latency (1000 sectors): 61ms
|
||||
Average read latency: 1.96ms/sector (raw speed: 6.8x)
|
||||
Read latency standard deviation: 0.07ms/sector
|
||||
[00:00.00]:
|
||||
0:1:84 1:27:67 28:27:72 55:27:67 82:27:72 109:27:67 136:27:72 163:27:67 190:27:72 217:27:67 244:27:72 271:27:67 298:27:72 325:27:67 352:27:72 379:27:67 406:27:72 433:27:67 460:27:72 487:27:67 514:27:72 541:27:67 568:27:72 595:27:67 622:27:72 649:27:67 676:27:72 703:27:67 730:27:72 757:27:67 784:27:72 811:27:67 838:27:72 865:27:67 892:27:72 919:27:67 946:27:72 973:27:67
|
||||
Initial seek latency (1000 sectors): 84ms
|
||||
Average read latency: 2.57ms/sector (raw speed: 5.2x)
|
||||
Read latency standard deviation: 0.09ms/sector
|
||||
|
||||
Analyzing cache behavior...
|
||||
Fast search for approximate cache size... 0 sectors
|
||||
>>> fast_read=10:1:71 seek_read=10:1:0
|
||||
Fast search for approximate cache size... 1 sectors
|
||||
>>> fast_read=11:1:0 seek_read=10:1:65
|
||||
>>> fast_read=11:1:0
|
||||
Fast search for approximate cache size... 2 sectors
|
||||
>>> fast_read=12:1:4 seek_read=10:1:61
|
||||
>>> fast_read=12:1:5
|
||||
Fast search for approximate cache size... 3 sectors
|
||||
>>> fast_read=13:1:0 seek_read=10:1:61
|
||||
>>> fast_read=13:1:5
|
||||
Fast search for approximate cache size... 4 sectors
|
||||
>>> fast_read=14:1:5 seek_read=10:1:56
|
||||
>>> fast_read=14:1:10 seek_read=10:1:56
|
||||
>>> fast_read=14:1:10 seek_read=10:1:56
|
||||
>>> fast_read=14:1:10 seek_read=10:1:56
|
||||
>>> fast_read=14:1:10 seek_read=10:1:56
|
||||
>>> fast_read=14:1:10 seek_read=10:1:56
|
||||
>>> fast_read=14:1:10 seek_read=10:1:56
|
||||
>>> fast_read=14:1:10 seek_read=10:1:56
|
||||
>>> fast_read=14:1:10 seek_read=10:1:56
|
||||
>>> fast_read=14:1:10 seek_read=10:1:56
|
||||
>>> fast_read=14:1:10 seek_read=10:1:56
|
||||
>>> fast_read=14:1:10 seek_read=10:1:56
|
||||
>>> fast_read=14:1:10 seek_read=10:1:56
|
||||
>>> fast_read=14:1:10 seek_read=10:1:56
|
||||
>>> fast_read=14:1:10 seek_read=10:1:56
|
||||
Slow verify for approximate cache size... 4 sectors
|
||||
Attempting to reduce read speed to 1x... drive said OK
|
||||
>>> slow_read=10:5:10 seek_read=10:1:0
|
||||
Attempting to reset read speed to full... drive said OK
|
||||
Fast search for approximate cache size... 5 sectors
|
||||
>>> fast_read=15:1:0 seek_read=10:1:54
|
||||
>>> fast_read=15:1:10 seek_read=10:1:56
|
||||
>>> fast_read=15:1:10 seek_read=10:1:56
|
||||
>>> fast_read=15:1:10 seek_read=10:1:56
|
||||
>>> fast_read=15:1:10 seek_read=10:1:56
|
||||
>>> fast_read=15:1:10 seek_read=10:1:56
|
||||
>>> fast_read=15:1:10 seek_read=10:1:56
|
||||
>>> fast_read=15:1:10 seek_read=10:1:56
|
||||
>>> fast_read=15:1:10 seek_read=10:1:56
|
||||
>>> fast_read=15:1:10 seek_read=10:1:56
|
||||
>>> fast_read=15:1:10 seek_read=10:1:56
|
||||
>>> fast_read=15:1:10 seek_read=10:1:56
|
||||
>>> fast_read=15:1:10 seek_read=10:1:56
|
||||
>>> fast_read=15:1:10 seek_read=10:1:56
|
||||
>>> fast_read=15:1:10 seek_read=10:1:56
|
||||
Slow verify for approximate cache size... 5 sectors
|
||||
Attempting to reduce read speed to 1x... drive said OK
|
||||
>>> slow_read=10:6:9 seek_read=10:1:0
|
||||
Attempting to reset read speed to full... drive said OK
|
||||
Fast search for approximate cache size... 6 sectors
|
||||
>>> fast_read=16:1:4 seek_read=10:1:51
|
||||
>>> fast_read=16:1:15 seek_read=10:1:51
|
||||
>>> fast_read=16:1:15 seek_read=10:1:51
|
||||
>>> fast_read=16:1:15 seek_read=10:1:51
|
||||
>>> fast_read=16:1:15 seek_read=10:1:51
|
||||
>>> fast_read=16:1:15 seek_read=10:1:51
|
||||
>>> fast_read=16:1:15 seek_read=10:1:51
|
||||
>>> fast_read=16:1:15 seek_read=10:1:51
|
||||
>>> fast_read=16:1:15 seek_read=10:1:51
|
||||
>>> fast_read=16:1:15 seek_read=10:1:51
|
||||
>>> fast_read=16:1:15 seek_read=10:1:51
|
||||
>>> fast_read=16:1:15 seek_read=10:1:51
|
||||
>>> fast_read=16:1:15 seek_read=10:1:51
|
||||
>>> fast_read=16:1:15 seek_read=10:1:51
|
||||
>>> fast_read=16:1:15 seek_read=10:1:51
|
||||
Slow verify for approximate cache size... 6 sectors
|
||||
Attempting to reduce read speed to 1x... drive said OK
|
||||
>>> slow_read=10:7:15 seek_read=10:1:0
|
||||
Attempting to reset read speed to full... drive said OK
|
||||
Fast search for approximate cache size... 7 sectors
|
||||
>>> fast_read=17:1:0 seek_read=10:1:49
|
||||
>>> fast_read=17:1:15 seek_read=10:1:51
|
||||
>>> fast_read=17:1:15 seek_read=10:1:51
|
||||
>>> fast_read=17:1:15 seek_read=10:1:51
|
||||
>>> fast_read=17:1:15 seek_read=10:1:51
|
||||
>>> fast_read=17:1:15 seek_read=10:1:51
|
||||
>>> fast_read=17:1:15 seek_read=10:1:51
|
||||
>>> fast_read=17:1:15 seek_read=10:1:51
|
||||
>>> fast_read=17:1:15 seek_read=10:1:51
|
||||
>>> fast_read=17:1:15 seek_read=10:1:51
|
||||
>>> fast_read=17:1:15 seek_read=10:1:51
|
||||
>>> fast_read=17:1:15 seek_read=10:1:51
|
||||
>>> fast_read=17:1:15 seek_read=10:1:51
|
||||
>>> fast_read=17:1:15 seek_read=10:1:51
|
||||
>>> fast_read=17:1:15 seek_read=10:1:51
|
||||
Slow verify for approximate cache size... 7 sectors
|
||||
Attempting to reduce read speed to 1x... drive said OK
|
||||
>>> slow_read=10:8:15 seek_read=10:1:0
|
||||
Attempting to reset read speed to full... drive said OK
|
||||
Fast search for approximate cache size... 8 sectors
|
||||
>>> fast_read=18:1:3 seek_read=10:1:46
|
||||
>>> fast_read=18:1:20 seek_read=10:1:46
|
||||
>>> fast_read=18:1:20 seek_read=10:1:46
|
||||
>>> fast_read=18:1:21 seek_read=10:1:46
|
||||
>>> fast_read=18:1:20 seek_read=10:1:46
|
||||
>>> fast_read=18:1:20 seek_read=10:1:46
|
||||
>>> fast_read=18:1:20 seek_read=10:1:46
|
||||
>>> fast_read=18:1:21 seek_read=10:1:45
|
||||
>>> fast_read=18:1:21 seek_read=10:1:45
|
||||
>>> fast_read=18:1:20 seek_read=10:1:46
|
||||
>>> fast_read=18:1:20 seek_read=10:1:46
|
||||
>>> fast_read=18:1:20 seek_read=10:1:46
|
||||
>>> fast_read=18:1:20 seek_read=10:1:46
|
||||
>>> fast_read=18:1:20 seek_read=10:1:46
|
||||
>>> fast_read=18:1:20 seek_read=10:1:46
|
||||
Slow verify for approximate cache size... 8 sectors
|
||||
Attempting to reduce read speed to 1x... drive said OK
|
||||
>>> slow_read=10:9:18 seek_read=10:1:46
|
||||
>>> slow_read=10:9:20 seek_read=10:1:46
|
||||
>>> slow_read=10:9:20 seek_read=10:1:45
|
||||
>>> slow_read=10:9:20 seek_read=10:1:46
|
||||
>>> slow_read=10:9:20 seek_read=10:1:46
|
||||
>>> slow_read=10:9:20 seek_read=10:1:46
|
||||
>>> slow_read=10:9:20 seek_read=10:1:46
|
||||
>>> slow_read=10:9:20 seek_read=10:1:46
|
||||
>>> slow_read=10:9:20 seek_read=10:1:46
|
||||
>>> slow_read=10:9:20 seek_read=10:1:46
|
||||
Approximate random access cache size: 8 sector(s)
|
||||
Attempting to reset read speed to full... drive said OK
|
||||
Verifying that cache is contiguous... >>> 34:1:61 seek_read:10:1:49
|
||||
>>> 34:1:62 seek_read:10:1:49
|
||||
>>> 34:1:62 seek_read:10:1:49
|
||||
>>> 34:1:62 seek_read:10:1:49
|
||||
>>> 34:1:62 seek_read:10:1:49
|
||||
>>> 34:1:62 seek_read:10:1:49
|
||||
>>> 34:1:62 seek_read:10:1:49
|
||||
>>> 34:1:62 seek_read:10:1:49
|
||||
>>> 34:1:62 seek_read:10:1:49
|
||||
>>> 34:1:62 seek_read:10:1:49
|
||||
>>> 34:1:62 seek_read:10:1:49
|
||||
>>> 34:1:62 seek_read:10:1:49
|
||||
>>> 34:1:62 seek_read:10:1:49
|
||||
>>> 34:1:62 seek_read:10:1:49
|
||||
>>> 34:1:62 seek_read:10:1:49
|
||||
>>> 34:1:62 seek_read:10:1:49
|
||||
>>> 34:1:62 seek_read:10:1:49
|
||||
>>> 34:1:62 seek_read:10:1:49
|
||||
>>> 34:1:62 seek_read:10:1:49
|
||||
>>> 34:1:62 seek_read:10:1:49
|
||||
Drive cache tests as contiguous
|
||||
|
||||
Testing background readahead past read cursor... 64
|
||||
0 >>> 10:8:15 sleep=197299us seek=81:1:0
|
||||
Testing background readahead past read cursor... 128
|
||||
0 >>> 10:8:69 sleep=394598us seek=145:1:0
|
||||
Testing background readahead past read cursor... 192
|
||||
0 >>> 10:8:73 sleep=591897us seek=209:1:0
|
||||
Testing background readahead past read cursor... 256
|
||||
0 >>> 10:8:77 sleep=789196us seek=273:1:0
|
||||
Testing background readahead past read cursor... 320
|
||||
0 >>> 10:8:81 sleep=986496us seek=337:1:50
|
||||
1 >>> 10:8:81 sleep=1150912us seek=337:1:64
|
||||
Retiming drive...
|
||||
10:1:65 11:27:67 38:27:72 65:27:67 92:27:72 119:27:67 146:27:72 173:27:67 200:27:72 227:27:67 254:27:72 281:27:67 308:27:72 335:27:67 362:27:72 389:27:67 416:27:72 443:27:67 470:27:72 497:27:67 524:27:72 551:27:67 578:27:72 605:27:67 632:27:72 659:27:67 686:27:72 713:27:67 740:27:72 767:27:67 794:27:72 821:27:67 848:27:72 875:27:67 902:27:72 929:27:67 956:27:72 983:27:67 1010:27:72 1037:27:67 1064:27:72 1091:27:67 1118:27:72 1145:27:67 1172:27:72 1199:27:67 1226:27:72 1253:27:67 1280:27:72 1307:27:67 1334:27:72 1361:27:67 1388:27:72 1415:27:66 1442:27:72 1469:27:67 1496:27:72 1523:27:66 1550:27:72 1577:27:66 1604:27:71 1631:27:66 1658:27:72 1685:27:66 1712:27:71 1739:27:66 1766:27:71 1793:27:66 1820:27:71 1847:27:66 1874:27:71 1901:27:66 1928:27:71 1955:1:0
|
||||
Initial seek latency (1946 sectors): 65ms
|
||||
Average read latency: 2.57ms/sector (raw speed: 5.2x)
|
||||
Read latency standard deviation: 0.11ms/sector
|
||||
Old mean=2.57ms/sec, New mean=2.56ms/sec
|
||||
|
||||
2 >>> 10:8:106 sleep=1315328us seek=337:1:56
|
||||
Testing background readahead past read cursor... 264
|
||||
0 >>> 10:8:81 sleep=813859us seek=281:1:0
|
||||
Testing background readahead past read cursor... 272
|
||||
0 >>> 10:8:79 sleep=838521us seek=289:1:50
|
||||
1 >>> 10:8:72 sleep=978275us seek=289:1:45
|
||||
Retiming drive...
|
||||
10:1:56 11:27:67 38:27:72 65:27:67 92:27:72 119:27:67 146:27:72 173:27:67 200:27:72 227:27:67 254:27:72 281:27:67 308:27:72 335:27:67 362:27:72 389:27:67 416:27:72 443:27:67 470:27:72 497:27:67 524:27:72 551:27:67 578:27:72 605:27:67 632:27:72 659:27:67 686:27:72 713:27:67 740:27:72 767:27:67 794:27:72 821:27:67 848:27:72 875:27:67 902:27:72 929:27:67 956:27:72 983:27:67 1010:27:72 1037:27:67 1064:27:72 1091:27:67 1118:27:72 1145:27:67 1172:27:72 1199:27:67 1226:27:72 1253:27:67 1280:27:72 1307:27:67 1334:27:72 1361:27:67 1388:27:72 1415:27:67 1442:27:72 1469:27:66 1496:27:72 1523:27:66 1550:27:72 1577:27:66 1604:27:72 1631:27:66 1658:27:72 1685:27:66 1712:27:71 1739:27:66 1766:27:71 1793:27:66 1820:27:71 1847:27:66 1874:27:71 1901:27:66 1928:27:71 1955:1:0
|
||||
Initial seek latency (1946 sectors): 56ms
|
||||
Average read latency: 2.57ms/sector (raw speed: 5.2x)
|
||||
Read latency standard deviation: 0.11ms/sector
|
||||
Old mean=2.57ms/sec, New mean=2.56ms/sec
|
||||
|
||||
2 >>> 10:8:105 sleep=1118028us seek=289:1:39
|
||||
Testing background readahead past read cursor... 265
|
||||
0 >>> 10:8:71 sleep=816942us seek=282:1:54
|
||||
1 >>> 10:8:67 sleep=953098us seek=282:1:52
|
||||
Retiming drive...
|
||||
10:1:51 11:27:67 38:27:72 65:27:67 92:27:72 119:27:67 146:27:72 173:27:67 200:27:72 227:27:67 254:27:72 281:27:67 308:27:72 335:27:67 362:27:72 389:27:67 416:27:72 443:27:67 470:27:72 497:27:67 524:27:72 551:27:67 578:27:72 605:27:67 632:27:72 659:27:67 686:27:72 713:27:67 740:27:72 767:27:67 794:27:72 821:27:67 848:27:72 875:27:67 902:27:72 929:27:67 956:27:72 983:27:67 1010:27:72 1037:27:67 1064:27:72 1091:27:67 1118:27:72 1145:27:67 1172:27:72 1199:27:67 1226:27:72 1253:27:67 1280:27:72 1307:27:67 1334:27:72 1361:27:67 1388:27:72 1415:27:67 1442:27:71 1469:27:67 1496:27:72 1523:27:66 1550:27:72 1577:27:67 1604:27:71 1631:27:66 1658:27:72 1685:27:66 1712:27:72 1739:27:67 1766:27:71 1793:27:66 1820:27:71 1847:27:66 1874:27:71 1901:27:67 1928:27:71 1955:1:0
|
||||
Initial seek latency (1946 sectors): 51ms
|
||||
Average read latency: 2.57ms/sector (raw speed: 5.2x)
|
||||
Read latency standard deviation: 0.11ms/sector
|
||||
Old mean=2.57ms/sec, New mean=2.57ms/sec
|
||||
|
||||
2 >>> 10:8:106 sleep=1089256us seek=282:1:50
|
||||
3 >>> 10:8:67 sleep=1225413us seek=282:1:48
|
||||
4 >>> 10:8:67 sleep=1361569us seek=282:1:46
|
||||
Retiming drive...
|
||||
10:1:52 11:27:67 38:27:72 65:27:67 92:27:72 119:27:67 146:27:72 173:27:67 200:27:72 227:27:67 254:27:72 281:27:67 308:27:72 335:27:67 362:27:72 389:27:67 416:27:72 443:27:67 470:27:72 497:27:67 524:27:72 551:27:67 578:27:72 605:27:67 632:27:72 659:27:67 686:27:72 713:27:67 740:27:72 767:27:67 794:27:72 821:27:67 848:27:72 875:27:67 902:27:72 929:27:67 956:27:72 983:27:67 1010:27:72 1037:27:67 1064:27:72 1091:27:67 1118:27:72 1145:27:67 1172:27:72 1199:27:67 1226:27:72 1253:27:67 1280:27:72 1307:27:67 1334:27:72 1361:27:67 1388:27:72 1415:27:66 1442:27:72 1469:27:66 1496:27:72 1523:27:67 1550:27:72 1577:27:66 1604:27:72 1631:27:66 1658:27:72 1685:27:66 1712:27:71 1739:27:67 1766:27:71 1793:27:66 1820:27:71 1847:27:66 1874:27:71 1901:27:66 1928:27:71 1955:1:0
|
||||
Initial seek latency (1946 sectors): 52ms
|
||||
Average read latency: 2.57ms/sector (raw speed: 5.2x)
|
||||
Read latency standard deviation: 0.11ms/sector
|
||||
Old mean=2.57ms/sec, New mean=2.56ms/sec
|
||||
|
||||
5 >>> 10:8:106 sleep=1497727us seek=282:1:44
|
||||
6 >>> 10:8:90 sleep=1633884us seek=282:1:42
|
||||
7 >>> 10:8:67 sleep=1770041us seek=282:1:40
|
||||
Retiming drive...
|
||||
10:1:52 11:27:67 38:27:72 65:27:67 92:27:72 119:27:67 146:27:72 173:27:67 200:27:72 227:27:67 254:27:72 281:27:67 308:27:72 335:27:67 362:27:72 389:27:67 416:27:72 443:27:67 470:27:72 497:27:67 524:27:72 551:27:67 578:27:72 605:27:67 632:27:72 659:27:67 686:27:72 713:27:67 740:27:72 767:27:67 794:27:72 821:27:67 848:27:72 875:27:67 902:27:72 929:27:67 956:27:72 983:27:67 1010:27:72 1037:27:67 1064:27:72 1091:27:67 1118:27:72 1145:27:67 1172:27:72 1199:27:67 1226:27:72 1253:27:67 1280:27:72 1307:27:66 1334:27:72 1361:27:67 1388:27:72 1415:27:67 1442:27:72 1469:27:66 1496:27:72 1523:27:66 1550:27:72 1577:27:67 1604:27:72 1631:27:66 1658:27:72 1685:27:66 1712:27:71 1739:27:66 1766:27:72 1793:27:66 1820:27:71 1847:27:66 1874:27:71 1901:27:66 1928:27:71 1955:1:0
|
||||
Initial seek latency (1946 sectors): 52ms
|
||||
Average read latency: 2.57ms/sector (raw speed: 5.2x)
|
||||
Read latency standard deviation: 0.11ms/sector
|
||||
Old mean=2.57ms/sec, New mean=2.56ms/sec
|
||||
|
||||
8 >>> 10:8:106 sleep=1906197us seek=282:1:60
|
||||
9 >>> 10:8:67 sleep=2042354us seek=282:1:58
|
||||
Drive readahead past read cursor: 264 sector(s)
|
||||
Testing cache tail cursor...
|
||||
>>> 10:8:67
|
||||
sleeping 1017324 microseconds
|
||||
<<< 7:1:0 6:1:55
|
||||
>>> 10:8:66
|
||||
sleeping 1017324 microseconds
|
||||
<<< 6:1:0 5:1:52
|
||||
>>> 10:8:69
|
||||
sleeping 1017324 microseconds
|
||||
<<< 5:1:0 4:1:49
|
||||
>>> 10:8:72
|
||||
sleeping 1017324 microseconds
|
||||
<<< 4:1:0 3:1:69
|
||||
>>> 10:8:74
|
||||
sleeping 1017324 microseconds
|
||||
<<< 3:1:0 2:1:67
|
||||
>>> 10:8:77
|
||||
sleeping 1017324 microseconds
|
||||
<<< 2:1:0 1:1:64
|
||||
>>> 10:8:79
|
||||
sleeping 1017324 microseconds
|
||||
<<< 1:1:0 0:1:61
|
||||
>>> 10:8:15
|
||||
sleeping 1017324 microseconds
|
||||
<<< 0:1:0
|
||||
Retiming drive...
|
||||
10:1:0 11:27:1 38:27:1 65:27:1 92:27:1 119:27:1 146:27:3 173:27:3 200:27:3 227:27:3 254:27:3 281:27:91 308:27:72 335:27:67 362:27:72 389:27:67 416:27:72 443:27:67 470:27:72 497:27:67 524:27:72 551:27:67 578:27:72 605:27:67 632:27:72 659:27:67 686:27:72 713:27:67 740:27:72 767:27:67 794:27:72 821:27:67 848:27:72 875:27:67 902:27:72 929:27:67 956:27:72 983:27:67 1010:27:72 1037:27:67 1064:27:72 1091:27:67 1118:27:72 1145:27:67 1172:27:72 1199:27:67 1226:27:72 1253:27:67 1280:27:72 1307:27:67 1334:27:72 1361:27:67 1388:27:72 1415:27:66 1442:27:71 1469:27:67 1496:27:72 1523:27:66 1550:27:71 1577:27:67 1604:27:71 1631:27:66 1658:27:72 1685:27:66 1712:27:71 1739:27:66 1766:27:71 1793:27:66 1820:27:71 1847:27:66 1874:27:71 1901:27:66 1928:27:71 1955:1:0
|
||||
Initial seek latency (1946 sectors): 0ms
|
||||
Average read latency: 2.23ms/sector (raw speed: 6.0x)
|
||||
Read latency standard deviation: 0.88ms/sector
|
||||
Old mean=2.57ms/sec, New mean=2.23ms/sec
|
||||
|
||||
Cache tail cursor tied to read cursor
|
||||
Testing granularity of cache tail
|
||||
>>> 10:9:112
|
||||
sleeping 1017324 microseconds
|
||||
<<< 18:1:0 17:1:52
|
||||
>>> 10:9:69
|
||||
sleeping 1017324 microseconds
|
||||
<<< 17:1:0 16:1:72
|
||||
>>> 10:9:80
|
||||
sleeping 1017324 microseconds
|
||||
<<< 16:1:0 15:1:69
|
||||
>>> 10:9:74
|
||||
sleeping 1017324 microseconds
|
||||
<<< 15:1:0 14:1:67
|
||||
>>> 10:9:77
|
||||
sleeping 1017324 microseconds
|
||||
<<< 14:1:0 13:1:64
|
||||
>>> 10:9:79
|
||||
sleeping 1017324 microseconds
|
||||
<<< 13:1:0 12:1:61
|
||||
>>> 10:9:82
|
||||
sleeping 1017324 microseconds
|
||||
<<< 12:1:0 11:1:59
|
||||
>>> 10:9:85
|
||||
sleeping 1017324 microseconds
|
||||
<<< 11:1:0 10:1:56
|
||||
>>> 10:9:20
|
||||
sleeping 1017324 microseconds
|
||||
<<< 10:1:57
|
||||
>>> 10:9:20
|
||||
sleeping 1017324 microseconds
|
||||
<<< 10:1:57
|
||||
>>> 10:9:20
|
||||
sleeping 1017324 microseconds
|
||||
<<< 10:1:57
|
||||
>>> 10:9:20
|
||||
sleeping 1017324 microseconds
|
||||
<<< 10:1:57
|
||||
>>> 10:9:20
|
||||
sleeping 1017324 microseconds
|
||||
<<< 10:1:57
|
||||
>>> 10:9:20
|
||||
sleeping 1017324 microseconds
|
||||
<<< 10:1:57
|
||||
>>> 10:9:20
|
||||
sleeping 1017324 microseconds
|
||||
<<< 10:1:57
|
||||
>>> 10:9:20
|
||||
sleeping 1017324 microseconds
|
||||
<<< 10:1:57
|
||||
>>> 10:9:20
|
||||
sleeping 1017324 microseconds
|
||||
<<< 10:1:57
|
||||
>>> 10:9:20
|
||||
sleeping 1017324 microseconds
|
||||
<<< 10:1:57
|
||||
Retiming drive...
|
||||
10:1:0 11:27:66 38:27:72 65:27:67 92:27:72 119:27:67 146:27:72 173:27:67 200:27:72 227:27:67 254:27:72 281:27:67 308:27:72 335:27:67 362:27:72 389:27:67 416:27:72 443:27:67 470:27:72 497:27:67 524:27:72 551:27:67 578:27:72 605:27:67 632:27:72 659:27:67 686:27:72 713:27:67 740:27:72 767:27:67 794:27:72 821:27:67 848:27:72 875:27:67 902:27:72 929:27:67 956:27:72 983:27:67 1010:27:72 1037:27:67 1064:27:72 1091:27:67 1118:27:72 1145:27:67 1172:27:72 1199:27:67 1226:27:72 1253:27:67 1280:27:72 1307:27:67 1334:27:71 1361:27:67 1388:27:72 1415:27:67 1442:27:72 1469:27:66 1496:27:72 1523:27:66 1550:27:72 1577:27:66 1604:27:71 1631:27:66 1658:27:72 1685:27:66 1712:27:71 1739:27:66 1766:27:71 1793:27:67 1820:27:71 1847:27:66 1874:27:71 1901:27:66 1928:27:71 1955:1:0
|
||||
Initial seek latency (1946 sectors): 0ms
|
||||
Average read latency: 2.56ms/sector (raw speed: 5.2x)
|
||||
Read latency standard deviation: 0.11ms/sector
|
||||
Old mean=2.57ms/sec, New mean=2.56ms/sec
|
||||
|
||||
Cache tail granularity: 1 sector(s)
|
||||
Cache size (considering rollbehind) too small to test cache speed.
|
||||
|
||||
Drive tests OK with Paranoia.
|
||||
|
||||
111
whipper/test/cdparanoia/MATSHITA.cdparanoia-A.stderr
Normal file
111
whipper/test/cdparanoia/MATSHITA.cdparanoia-A.stderr
Normal file
@@ -0,0 +1,111 @@
|
||||
cdparanoia III release 10.2 (September 11, 2008)
|
||||
|
||||
Using cdda library version: 10.2
|
||||
Using paranoia library version: 10.2
|
||||
Checking /dev/cdrom for cdrom...
|
||||
Could not stat /dev/cdrom: No such file or directory
|
||||
|
||||
Checking /dev/cdroms/cdrom0 for cdrom...
|
||||
Could not stat /dev/cdroms/cdrom0: No such file or directory
|
||||
|
||||
Checking /dev/cdroms/cdroma for cdrom...
|
||||
Could not stat /dev/cdroms/cdroma: No such file or directory
|
||||
|
||||
Checking /dev/cdroms/cdrom1 for cdrom...
|
||||
Could not stat /dev/cdroms/cdrom1: No such file or directory
|
||||
|
||||
Checking /dev/cdroms/cdromb for cdrom...
|
||||
Could not stat /dev/cdroms/cdromb: No such file or directory
|
||||
|
||||
Checking /dev/cdroms/cdrom2 for cdrom...
|
||||
Could not stat /dev/cdroms/cdrom2: No such file or directory
|
||||
|
||||
Checking /dev/cdroms/cdromc for cdrom...
|
||||
Could not stat /dev/cdroms/cdromc: No such file or directory
|
||||
|
||||
Checking /dev/cdroms/cdrom3 for cdrom...
|
||||
Could not stat /dev/cdroms/cdrom3: No such file or directory
|
||||
|
||||
Checking /dev/cdroms/cdromd for cdrom...
|
||||
Could not stat /dev/cdroms/cdromd: No such file or directory
|
||||
|
||||
Checking /dev/hd0 for cdrom...
|
||||
Could not stat /dev/hd0: No such file or directory
|
||||
|
||||
Checking /dev/hda for cdrom...
|
||||
Could not stat /dev/hda: No such file or directory
|
||||
|
||||
Checking /dev/hd1 for cdrom...
|
||||
Could not stat /dev/hd1: No such file or directory
|
||||
|
||||
Checking /dev/hdb for cdrom...
|
||||
Could not stat /dev/hdb: No such file or directory
|
||||
|
||||
Checking /dev/hd2 for cdrom...
|
||||
Could not stat /dev/hd2: No such file or directory
|
||||
|
||||
Checking /dev/hdc for cdrom...
|
||||
Could not stat /dev/hdc: No such file or directory
|
||||
|
||||
Checking /dev/hd3 for cdrom...
|
||||
Could not stat /dev/hd3: No such file or directory
|
||||
|
||||
Checking /dev/hdd for cdrom...
|
||||
Could not stat /dev/hdd: No such file or directory
|
||||
|
||||
Checking /dev/sg0 for cdrom...
|
||||
Testing /dev/sg0 for SCSI/MMC interface
|
||||
Could not access device /dev/sg0 to test for SG_IO support: Permission denied
|
||||
no SG_IO support for device: /dev/sg0
|
||||
Could not access device /dev/sg0: Permission denied
|
||||
generic device: /dev/sg0
|
||||
ioctl device: not found
|
||||
Could not open generic SCSI device /dev/sg0: Permission denied
|
||||
Testing /dev/sg0 for cooked ioctl() interface
|
||||
/dev/sg0 is not a cooked ioctl CDROM.
|
||||
|
||||
Checking /dev/sga for cdrom...
|
||||
Could not stat /dev/sga: No such file or directory
|
||||
|
||||
Checking /dev/sg1 for cdrom...
|
||||
Testing /dev/sg1 for SCSI/MMC interface
|
||||
SG_IO device: /dev/sg1
|
||||
|
||||
CDROM model sensed sensed: MATSHITA DVD-RAM UJ8A0A SB02
|
||||
|
||||
Checking for SCSI emulation...
|
||||
Drive is ATAPI (using SG_IO host adaptor emulation)
|
||||
|
||||
Checking for MMC style command set...
|
||||
Drive is MMC style
|
||||
DMA scatter/gather table entries: 167
|
||||
table entry size: 524288 bytes
|
||||
maximum theoretical transfer: 37074 sectors
|
||||
Setting default read size to 27 sectors (63504 bytes).
|
||||
|
||||
Verifying CDDA command set...
|
||||
Expected command set reads OK.
|
||||
|
||||
Attempting to set cdrom to full speed...
|
||||
drive returned OK.
|
||||
|
||||
=================== Checking drive cache/timing behavior ===================
|
||||
|
||||
Seek/read timing:
|
||||
|
||||
[45:24.28]: 46ms seek, 1.26ms/sec read [10.6x] spinning up...
|
||||
[45:24.27]: 45ms seek, 1.25ms/sec read [10.6x] spinning up...
|
||||
[45:24.26]: 45ms seek, 1.26ms/sec read [10.6x] spinning up...
|
||||
[45:24.25]: 44ms seek, 1.26ms/sec read [10.6x] spinning up...
|
||||
[45:24.24]: 45ms seek, 1.26ms/sec read [10.6x]
|
||||
[40:00.00]: 50ms seek, 1.31ms/sec read [10.2x]
|
||||
[30:00.00]: 64ms seek, 1.46ms/sec read [9.1x]
|
||||
[20:00.00]: 63ms seek, 1.65ms/sec read [8.1x]
|
||||
[10:00.00]: 61ms seek, 1.96ms/sec read [6.8x]
|
||||
[00:00.00]: 84ms seek, 2.57ms/sec read [5.2x]
|
||||
|
||||
Analyzing cache behavior...
|
||||
|
||||
Fast search for approximate cache size... 0 sectors
|
||||
Fast search for approximate cache size... 1 sectors
|
||||
Fast search for approximate cache size... 2 sectors
|
||||
158
whipper/test/cdparanoia/PX-L890SA.cdparanoia-A.log
Normal file
158
whipper/test/cdparanoia/PX-L890SA.cdparanoia-A.log
Normal file
@@ -0,0 +1,158 @@
|
||||
cdparanoia -A
|
||||
cdparanoia III release 10.2 (September 11, 2008)
|
||||
|
||||
Using cdda library version: 10.2
|
||||
Using paranoia library version: 10.2
|
||||
|
||||
|
||||
Attempting to set cdrom to full speed...
|
||||
drive returned OK.
|
||||
|
||||
=================== Checking drive cache/timing behavior ===================
|
||||
|
||||
Seek/read timing:
|
||||
|
||||
[39:43.53]:
|
||||
178778:1:19 178779:27:19 178806:27:19 178833:27:19 178860:27:19 178887:27:19 178914:27:19 178941:27:19 178968:27:19 178995:27:19 179022:27:19 179049:27:19 179076:27:19 179103:27:19 179130:27:19 179157:27:19 179184:27:19 179211:27:19 179238:27:19 179265:27:19 179292:27:19 179319:27:19 179346:27:19 179373:27:19 179400:27:19 179427:27:19 179454:27:20 179481:27:19 179508:27:19 179535:27:19 179562:27:19 179589:27:19 179616:27:19 179643:27:19 179670:27:19 179697:27:19 179724:27:19 179751:27:19
|
||||
Initial seek latency (1000 sectors): 19ms
|
||||
Average read latency: 0.70ms/sector (raw speed: 18.9x)
|
||||
Read latency standard deviation: 0.01ms/sector
|
||||
[39:43.52]:
|
||||
178777:1:19 178778:27:20 178805:27:20 178832:27:20 178859:27:20 178886:27:20 178913:27:20 178940:27:20 178967:27:732 178994:27:14 179021:27:14 179048:27:14 179075:27:14 179102:27:14 179129:27:14 179156:27:14 179183:27:14 179210:27:14 179237:27:14 179264:27:14 179291:27:14 179318:27:14 179345:27:14 179372:27:14 179399:27:14 179426:27:14 179453:27:14 179480:27:14 179507:27:14 179534:27:14 179561:27:14 179588:27:14 179615:27:14 179642:27:14 179669:27:14 179696:27:14 179723:27:14 179750:27:14
|
||||
Initial seek latency (1000 sectors): 19ms
|
||||
Average read latency: 1.28ms/sector (raw speed: 10.4x)
|
||||
Read latency standard deviation: 4.31ms/sector
|
||||
[39:43.51]:
|
||||
178776:1:23 178777:27:14 178804:27:14 178831:27:14 178858:27:14 178885:27:14 178912:27:14 178939:27:14 178966:27:14 178993:27:14 179020:27:14 179047:27:14 179074:27:14 179101:27:14 179128:27:14 179155:27:14 179182:27:14 179209:27:14 179236:27:14 179263:27:14 179290:27:14 179317:27:14 179344:27:14 179371:27:14 179398:27:14 179425:27:14 179452:27:14 179479:27:14 179506:27:14 179533:27:14 179560:27:14 179587:27:14 179614:27:14 179641:27:14 179668:27:14 179695:27:14 179722:27:14 179749:27:14
|
||||
Initial seek latency (1000 sectors): 23ms
|
||||
Average read latency: 0.52ms/sector (raw speed: 25.7x)
|
||||
Read latency standard deviation: -nanms/sector
|
||||
[39:43.50]:
|
||||
178775:1:231 178776:27:13 178803:27:13 178830:27:13 178857:27:13 178884:27:13 178911:27:13 178938:27:13 178965:27:13 178992:27:13 179019:27:13 179046:27:13 179073:27:13 179100:27:13 179127:27:13 179154:27:12 179181:27:12 179208:27:12 179235:27:13 179262:27:12 179289:27:12 179316:27:12 179343:27:12 179370:27:12 179397:27:12 179424:27:12 179451:27:12 179478:27:12 179505:27:12 179532:27:12 179559:27:12 179586:27:12 179613:27:12 179640:27:12 179667:27:12 179694:27:12 179721:27:12 179748:27:12
|
||||
Initial seek latency (1000 sectors): 231ms
|
||||
Average read latency: 0.46ms/sector (raw speed: 29.0x)
|
||||
Read latency standard deviation: 0.02ms/sector
|
||||
[39:43.49]:
|
||||
178774:1:18 178775:27:11 178802:27:11 178829:27:11 178856:27:11 178883:27:11 178910:27:11 178937:27:11 178964:27:11 178991:27:11 179018:27:11 179045:27:11 179072:27:11 179099:27:11 179126:27:11 179153:27:11 179180:27:11 179207:27:11 179234:27:11 179261:27:11 179288:27:11 179315:27:11 179342:27:11 179369:27:11 179396:27:11 179423:27:11 179450:27:11 179477:27:11 179504:27:11 179531:27:11 179558:27:11 179585:27:11 179612:27:11 179639:27:11 179666:27:11 179693:27:11 179720:27:11 179747:27:11
|
||||
Initial seek latency (1000 sectors): 18ms
|
||||
Average read latency: 0.41ms/sector (raw speed: 32.7x)
|
||||
Read latency standard deviation: 0.00ms/sector
|
||||
[39:43.48]:
|
||||
178773:1:18 178774:27:11 178801:27:11 178828:27:11 178855:27:11 178882:27:11 178909:27:941 178936:27:9 178963:27:9 178990:27:9 179017:27:9 179044:27:9 179071:27:9 179098:27:9 179125:27:9 179152:27:9 179179:27:9 179206:27:9 179233:27:9 179260:27:9 179287:27:9 179314:27:10 179341:27:9 179368:27:10 179395:27:9 179422:27:9 179449:27:9 179476:27:9 179503:27:10 179530:27:9 179557:27:9 179584:27:9 179611:27:10 179638:27:9 179665:27:9 179692:27:9 179719:27:10 179746:27:9
|
||||
Initial seek latency (1000 sectors): 18ms
|
||||
Average read latency: 1.28ms/sector (raw speed: 10.4x)
|
||||
Read latency standard deviation: 5.60ms/sector
|
||||
[39:43.47]:
|
||||
178772:1:21 178773:27:10 178800:27:10 178827:27:10 178854:27:10 178881:27:9 178908:27:10 178935:27:10 178962:27:10 178989:27:10 179016:27:10 179043:27:10 179070:27:10 179097:27:10 179124:27:10 179151:27:10 179178:27:10 179205:27:10 179232:27:9 179259:27:10 179286:27:10 179313:27:10 179340:27:10 179367:27:10 179394:27:10 179421:27:10 179448:27:10 179475:27:10 179502:27:10 179529:27:10 179556:27:10 179583:27:10 179610:27:10 179637:27:10 179664:27:10 179691:27:10 179718:27:10 179745:27:9
|
||||
Initial seek latency (1000 sectors): 21ms
|
||||
Average read latency: 0.37ms/sector (raw speed: 36.3x)
|
||||
Read latency standard deviation: 0.01ms/sector
|
||||
[39:43.46]:
|
||||
178771:1:21 178772:27:10 178799:27:9 178826:27:10 178853:27:10 178880:27:10 178907:27:10 178934:27:10 178961:27:10 178988:27:10 179015:27:10 179042:27:10 179069:27:10 179096:27:10 179123:27:10 179150:27:10 179177:27:10 179204:27:10 179231:27:10 179258:27:10 179285:27:10 179312:27:10 179339:27:10 179366:27:10 179393:27:10 179420:27:10 179447:27:10 179474:27:10 179501:27:10 179528:27:10 179555:27:10 179582:27:10 179609:27:10 179636:27:10 179663:27:10 179690:27:10 179717:27:10 179744:27:10
|
||||
Initial seek latency (1000 sectors): 21ms
|
||||
Average read latency: 0.37ms/sector (raw speed: 36.1x)
|
||||
Read latency standard deviation: 0.01ms/sector
|
||||
[39:43.45]:
|
||||
178770:1:15 178771:27:10 178798:27:10 178825:27:10 178852:27:10 178879:27:10 178906:27:10 178933:27:10 178960:27:10 178987:27:10 179014:27:10 179041:27:10 179068:27:10 179095:27:10 179122:27:10 179149:27:10 179176:27:10 179203:27:10 179230:27:10 179257:27:10 179284:27:10 179311:27:10 179338:27:10 179365:27:10 179392:27:10 179419:27:10 179446:27:10 179473:27:10 179500:27:10 179527:27:10 179554:27:10 179581:27:10 179608:27:10 179635:27:10 179662:27:10 179689:27:10 179716:27:10 179743:27:10
|
||||
Initial seek latency (1000 sectors): 15ms
|
||||
Average read latency: 0.37ms/sector (raw speed: 36.0x)
|
||||
Read latency standard deviation: -nanms/sector
|
||||
[39:43.44]:
|
||||
178769:1:21 178770:27:10 178797:27:10 178824:27:10 178851:27:10 178878:27:10 178905:27:10 178932:27:10 178959:27:10 178986:27:10 179013:27:10 179040:27:10 179067:27:10 179094:27:10 179121:27:10 179148:27:10 179175:27:10 179202:27:10 179229:27:10 179256:27:10 179283:27:10 179310:27:10 179337:27:10 179364:27:10 179391:27:10 179418:27:10 179445:27:10 179472:27:10 179499:27:10 179526:27:10 179553:27:10 179580:27:10 179607:27:10 179634:27:10 179661:27:10 179688:27:10 179715:27:10 179742:27:10
|
||||
Initial seek latency (1000 sectors): 21ms
|
||||
Average read latency: 0.37ms/sector (raw speed: 36.0x)
|
||||
Read latency standard deviation: -nanms/sector
|
||||
[39:43.43]:
|
||||
178768:1:15 178769:27:10 178796:27:10 178823:27:10 178850:27:10 178877:27:10 178904:27:10 178931:27:10 178958:27:10 178985:27:10 179012:27:10 179039:27:10 179066:27:10 179093:27:10 179120:27:10 179147:27:10 179174:27:10 179201:27:10 179228:27:10 179255:27:10 179282:27:10 179309:27:10 179336:27:10 179363:27:10 179390:27:10 179417:27:10 179444:27:10 179471:27:10 179498:27:10 179525:27:10 179552:27:10 179579:27:10 179606:27:10 179633:27:10 179660:27:10 179687:27:10 179714:27:10 179741:27:10
|
||||
Initial seek latency (1000 sectors): 15ms
|
||||
Average read latency: 0.37ms/sector (raw speed: 36.0x)
|
||||
Read latency standard deviation: -nanms/sector
|
||||
[39:43.42]:
|
||||
178767:1:21 178768:27:10 178795:27:10 178822:27:10 178849:27:10 178876:27:10 178903:27:10 178930:27:10 178957:27:10 178984:27:10 179011:27:10 179038:27:10 179065:27:10 179092:27:10 179119:27:10 179146:27:10 179173:27:10 179200:27:10 179227:27:10 179254:27:10 179281:27:10 179308:27:10 179335:27:10 179362:27:10 179389:27:10 179416:27:10 179443:27:10 179470:27:10 179497:27:10 179524:27:10 179551:27:10 179578:27:10 179605:27:10 179632:27:10 179659:27:10 179686:27:10 179713:27:10 179740:27:10
|
||||
Initial seek latency (1000 sectors): 21ms
|
||||
Average read latency: 0.37ms/sector (raw speed: 36.0x)
|
||||
Read latency standard deviation: -nanms/sector
|
||||
[39:43.41]:
|
||||
178766:1:15 178767:27:10 178794:27:10 178821:27:10 178848:27:10 178875:27:10 178902:27:10 178929:27:10 178956:27:10 178983:27:10 179010:27:10 179037:27:10 179064:27:10 179091:27:10 179118:27:10 179145:27:10 179172:27:10 179199:27:10 179226:27:10 179253:27:10 179280:27:10 179307:27:10 179334:27:10 179361:27:10 179388:27:10 179415:27:10 179442:27:10 179469:27:10 179496:27:10 179523:27:10 179550:27:10 179577:27:10 179604:27:10 179631:27:10 179658:27:10 179685:27:10 179712:27:10 179739:27:10
|
||||
Initial seek latency (1000 sectors): 15ms
|
||||
Average read latency: 0.37ms/sector (raw speed: 36.0x)
|
||||
Read latency standard deviation: -nanms/sector
|
||||
[39:43.40]:
|
||||
178765:1:21 178766:27:10 178793:27:10 178820:27:10 178847:27:10 178874:27:10 178901:27:10 178928:27:10 178955:27:10 178982:27:10 179009:27:10 179036:27:10 179063:27:10 179090:27:10 179117:27:10 179144:27:10 179171:27:10 179198:27:10 179225:27:10 179252:27:10 179279:27:10 179306:27:10 179333:27:10 179360:27:10 179387:27:10 179414:27:10 179441:27:10 179468:27:10 179495:27:10 179522:27:10 179549:27:10 179576:27:10 179603:27:10 179630:27:10 179657:27:10 179684:27:10 179711:27:10 179738:27:10
|
||||
Initial seek latency (1000 sectors): 21ms
|
||||
Average read latency: 0.37ms/sector (raw speed: 36.0x)
|
||||
Read latency standard deviation: -nanms/sector
|
||||
[39:43.39]:
|
||||
178764:1:21 178765:27:10 178792:27:10 178819:27:10 178846:27:10 178873:27:10 178900:27:10 178927:27:10 178954:27:10 178981:27:10 179008:27:10 179035:27:10 179062:27:10 179089:27:10 179116:27:10 179143:27:10 179170:27:10 179197:27:10 179224:27:10 179251:27:10 179278:27:10 179305:27:10 179332:27:10 179359:27:10 179386:27:10 179413:27:10 179440:27:10 179467:27:10 179494:27:10 179521:27:10 179548:27:10 179575:27:10 179602:27:10 179629:27:10 179656:27:10 179683:27:10 179710:27:10 179737:27:10
|
||||
Initial seek latency (1000 sectors): 21ms
|
||||
Average read latency: 0.37ms/sector (raw speed: 36.0x)
|
||||
Read latency standard deviation: -nanms/sector
|
||||
[39:43.38]:
|
||||
178763:1:15 178764:27:10 178791:27:10 178818:27:10 178845:27:10 178872:27:10 178899:27:10 178926:27:10 178953:27:10 178980:27:10 179007:27:10 179034:27:10 179061:27:10 179088:27:10 179115:27:10 179142:27:10 179169:27:10 179196:27:10 179223:27:10 179250:27:10 179277:27:10 179304:27:10 179331:27:10 179358:27:10 179385:27:10 179412:27:10 179439:27:10 179466:27:10 179493:27:10 179520:27:10 179547:27:10 179574:27:10 179601:27:10 179628:27:10 179655:27:10 179682:27:10 179709:27:10 179736:27:10
|
||||
Initial seek latency (1000 sectors): 15ms
|
||||
Average read latency: 0.37ms/sector (raw speed: 36.0x)
|
||||
Read latency standard deviation: -nanms/sector
|
||||
[39:43.37]:
|
||||
178762:1:21 178763:27:10 178790:27:10 178817:27:10 178844:27:10 178871:27:10 178898:27:10 178925:27:10 178952:27:10 178979:27:10 179006:27:10 179033:27:10 179060:27:10 179087:27:10 179114:27:10 179141:27:10 179168:27:10 179195:27:10 179222:27:10 179249:27:10 179276:27:10 179303:27:10 179330:27:10 179357:27:10 179384:27:10 179411:27:10 179438:27:10 179465:27:10 179492:27:10 179519:27:10 179546:27:10 179573:27:10 179600:27:10 179627:27:10 179654:27:10 179681:27:10 179708:27:10 179735:27:10
|
||||
Initial seek latency (1000 sectors): 21ms
|
||||
Average read latency: 0.37ms/sector (raw speed: 36.0x)
|
||||
Read latency standard deviation: -nanms/sector
|
||||
[39:43.36]:
|
||||
178761:1:15 178762:27:10 178789:27:10 178816:27:10 178843:27:10 178870:27:10 178897:27:10 178924:27:10 178951:27:10 178978:27:10 179005:27:10 179032:27:10 179059:27:10 179086:27:10 179113:27:10 179140:27:10 179167:27:10 179194:27:10 179221:27:10 179248:27:10 179275:27:10 179302:27:10 179329:27:10 179356:27:10 179383:27:10 179410:27:10 179437:27:10 179464:27:10 179491:27:10 179518:27:10 179545:27:10 179572:27:10 179599:27:10 179626:27:10 179653:27:10 179680:27:10 179707:27:10 179734:27:10
|
||||
Initial seek latency (1000 sectors): 15ms
|
||||
Average read latency: 0.37ms/sector (raw speed: 36.0x)
|
||||
Read latency standard deviation: -nanms/sector
|
||||
[30:00.00]:
|
||||
135000:1:21 135001:27:11 135028:27:11 135055:27:11 135082:27:11 135109:27:11 135136:27:11 135163:27:11 135190:27:11 135217:27:11 135244:27:11 135271:27:11 135298:27:11 135325:27:11 135352:27:11 135379:27:11 135406:27:11 135433:27:11 135460:27:11 135487:27:11 135514:27:11 135541:27:11 135568:27:11 135595:27:11 135622:27:11 135649:27:11 135676:27:11 135703:27:11 135730:27:11 135757:27:11 135784:27:11 135811:27:11 135838:27:11 135865:27:11 135892:27:11 135919:27:11 135946:27:11 135973:27:11
|
||||
Initial seek latency (1000 sectors): 21ms
|
||||
Average read latency: 0.41ms/sector (raw speed: 32.7x)
|
||||
Read latency standard deviation: 0.00ms/sector
|
||||
[20:00.00]:
|
||||
90000:1:22 90001:27:12 90028:27:12 90055:27:12 90082:27:12 90109:27:12 90136:27:12 90163:27:12 90190:27:12 90217:27:12 90244:27:12 90271:27:12 90298:27:12 90325:27:12 90352:27:12 90379:27:12 90406:27:12 90433:27:12 90460:27:12 90487:27:12 90514:27:12 90541:27:12 90568:27:12 90595:27:12 90622:27:12 90649:27:12 90676:27:12 90703:27:12 90730:27:12 90757:27:12 90784:27:12 90811:27:12 90838:27:12 90865:27:12 90892:27:12 90919:27:12 90946:27:12 90973:27:12
|
||||
Initial seek latency (1000 sectors): 22ms
|
||||
Average read latency: 0.44ms/sector (raw speed: 30.0x)
|
||||
Read latency standard deviation: 0.00ms/sector
|
||||
[10:00.00]:
|
||||
45000:1:30 45001:27:14 45028:27:14 45055:27:14 45082:27:14 45109:27:14 45136:27:14 45163:27:14 45190:27:14 45217:27:14 45244:27:14 45271:27:14 45298:27:14 45325:27:14 45352:27:14 45379:27:14 45406:27:14 45433:27:14 45460:27:14 45487:27:14 45514:27:14 45541:27:14 45568:27:14 45595:27:14 45622:27:14 45649:27:14 45676:27:14 45703:27:14 45730:27:14 45757:27:14 45784:27:14 45811:27:14 45838:27:14 45865:27:14 45892:27:14 45919:27:14 45946:27:14 45973:27:14
|
||||
Initial seek latency (1000 sectors): 30ms
|
||||
Average read latency: 0.52ms/sector (raw speed: 25.7x)
|
||||
Read latency standard deviation: -nanms/sector
|
||||
[00:00.00]:
|
||||
0:1:33 1:27:19 28:27:19 55:27:19 82:27:19 109:27:19 136:27:19 163:27:19 190:27:19 217:27:19 244:27:19 271:27:19 298:27:19 325:27:19 352:27:19 379:27:19 406:27:19 433:27:19 460:27:19 487:27:19 514:27:19 541:27:19 568:27:19 595:27:19 622:27:19 649:27:19 676:27:19 703:27:19 730:27:19 757:27:19 784:27:19 811:27:19 838:27:19 865:27:19 892:27:19 919:27:19 946:27:19 973:27:19
|
||||
Initial seek latency (1000 sectors): 33ms
|
||||
Average read latency: 0.70ms/sector (raw speed: 18.9x)
|
||||
Read latency standard deviation: -nanms/sector
|
||||
|
||||
Analyzing cache behavior...
|
||||
Fast search for approximate cache size... 0 sectors
|
||||
>>> fast_read=10:1:35 seek_read=10:1:18
|
||||
>>> fast_read=10:1:18 seek_read=10:1:18
|
||||
>>> fast_read=10:1:18 seek_read=10:1:18
|
||||
>>> fast_read=10:1:18 seek_read=10:1:18
|
||||
>>> fast_read=10:1:18 seek_read=10:1:18
|
||||
>>> fast_read=10:1:18 seek_read=10:1:18
|
||||
>>> fast_read=10:1:18 seek_read=10:1:18
|
||||
>>> fast_read=10:1:18 seek_read=10:1:18
|
||||
>>> fast_read=10:1:18 seek_read=10:1:364
|
||||
>>> fast_read=10:1:21 seek_read=10:1:21
|
||||
>>> fast_read=10:1:22 seek_read=10:1:22
|
||||
>>> fast_read=10:1:22 seek_read=10:1:22
|
||||
>>> fast_read=10:1:22 seek_read=10:1:22
|
||||
>>> fast_read=10:1:22 seek_read=10:1:22
|
||||
>>> fast_read=10:1:22 seek_read=10:1:22
|
||||
Slow verify for approximate cache size... 0 sectors
|
||||
Attempting to reduce read speed to 1x... drive said OK
|
||||
>>> slow_read=10:1:21 seek_read=10:1:22
|
||||
>>> slow_read=10:1:22 seek_read=10:1:22
|
||||
>>> slow_read=10:1:22 seek_read=10:1:22
|
||||
>>> slow_read=10:1:22 seek_read=10:1:22
|
||||
>>> slow_read=10:1:22 seek_read=10:1:22
|
||||
>>> slow_read=10:1:22 seek_read=10:1:22
|
||||
>>> slow_read=10:1:22 seek_read=10:1:22
|
||||
>>> slow_read=10:1:22 seek_read=10:1:22
|
||||
>>> slow_read=10:1:22 seek_read=10:1:22
|
||||
>>> slow_read=10:1:22 seek_read=10:1:22
|
||||
Drive does not cache nonlinear access
|
||||
|
||||
Drive tests OK with Paranoia.
|
||||
|
||||
41
whipper/test/cdparanoia/PX-L890SA.cdparanoia-A.stderr
Normal file
41
whipper/test/cdparanoia/PX-L890SA.cdparanoia-A.stderr
Normal file
@@ -0,0 +1,41 @@
|
||||
cdparanoia III release 10.2 (September 11, 2008)
|
||||
|
||||
Using cdda library version: 10.2
|
||||
Using paranoia library version: 10.2
|
||||
Checking /dev/cdrom for cdrom...
|
||||
Testing /dev/cdrom for SCSI/MMC interface
|
||||
SG_IO device: /dev/sr0
|
||||
|
||||
CDROM model sensed sensed: PLEXTOR DVDR PX-L890SA 1.05
|
||||
|
||||
|
||||
Checking for SCSI emulation...
|
||||
Drive is ATAPI (using SG_IO host adaptor emulation)
|
||||
|
||||
Checking for MMC style command set...
|
||||
Drive is MMC style
|
||||
DMA scatter/gather table entries: 1
|
||||
table entry size: 524288 bytes
|
||||
maximum theoretical transfer: 222 sectors
|
||||
Setting default read size to 27 sectors (63504 bytes).
|
||||
|
||||
Verifying CDDA command set...
|
||||
Expected command set reads OK.
|
||||
|
||||
Attempting to set cdrom to full speed...
|
||||
drive returned OK.
|
||||
|
||||
=================== Checking drive cache/timing behavior ===================
|
||||
|
||||
Seek/read timing:
|
||||
|
||||
[39:43.53]: 19ms seek, 0.70ms/sec read [18.9x] spinning up...
|
||||
[39:43.52]: 19ms seek, 1.28ms/sec read [10.4x] spinning up...
|
||||
[39:43.51]: 23ms seek, 0.52ms/sec read [25.7x] spinning up...
|
||||
[39:43.50]: 231ms seek, 0.46ms/sec read [29.0x] spinning up...
|
||||
[39:43.49]: 18ms seek, 0.41ms/sec read [32.7x] spinning up...
|
||||
[39:43.48]: 18ms seek, 1.28ms/sec read [10.4x] spinning up...
|
||||
[39:43.47]: 21ms seek, 0.37ms/sec read [36.3x] spinning up...
|
||||
[39:43.46]: 21ms seek, 0.37ms/sec read [36.1x] spinning up...
|
||||
[39:43.45]: 15ms seek, 0.37ms/sec read [36.0x] spinning up...
|
||||
[39:43.44]: 21ms seek, 0.37ms/sec read [36.0x] spinning up...
|
||||
71
whipper/test/cdrdao.readtoc.progress
Normal file
71
whipper/test/cdrdao.readtoc.progress
Normal file
@@ -0,0 +1,71 @@
|
||||
Cdrdao version 1.2.2 - (C) Andreas Mueller <andreas@daneb.de>
|
||||
SCSI interface library - (C) Joerg Schilling
|
||||
Paranoia DAE library - (C) Monty
|
||||
|
||||
Check http://cdrdao.sourceforge.net/drives.html#dt for current driver tables.
|
||||
|
||||
Using libscg version 'schily-0.8'
|
||||
|
||||
/dev/cdrecorder: PLEXTOR DVDR PX-810SA Rev: 1.00
|
||||
Using driver: Generic SCSI-3/MMC - Version 2.0 (options 0x0000)
|
||||
|
||||
Reading toc data...
|
||||
|
||||
Track Mode Flags Start Length
|
||||
------------------------------------------------------------
|
||||
1 AUDIO 0 00:00:00( 0) 03:04:64( 13864)
|
||||
2 AUDIO 0 03:04:64( 13864) 04:00:57( 18057)
|
||||
3 AUDIO 0 07:05:46( 31921) 03:38:61( 16411)
|
||||
4 AUDIO 0 10:44:32( 48332) 02:58:51( 13401)
|
||||
5 AUDIO 0 13:43:08( 61733) 04:16:28( 19228)
|
||||
6 AUDIO 0 17:59:36( 80961) 04:16:58( 19258)
|
||||
7 AUDIO 0 22:16:19(100219) 03:34:22( 16072)
|
||||
8 AUDIO 0 25:50:41(116291) 04:25:22( 19897)
|
||||
9 AUDIO 0 30:15:63(136188) 04:44:16( 21316)
|
||||
10 AUDIO 0 35:00:04(157504) 03:56:71( 17771)
|
||||
11 AUDIO 0 38:57:00(175275) 06:24:30( 28830)
|
||||
Leadout AUDIO 0 45:21:30(204105)
|
||||
|
||||
PQ sub-channel reading (audio track) is supported, data format is BCD.
|
||||
Raw P-W sub-channel reading (audio track) is supported.
|
||||
Analyzing track 01 (AUDIO): start 00:00:00, length 03:04:64...
|
||||
00:01:00
|
||||
00:02:00
|
||||
00:03:00
|
||||
00:04:00
|
||||
00:05:00
|
||||
00:06:00
|
||||
00:07:00
|
||||
00:08:00
|
||||
00:09:00
|
||||
00:10:00
|
||||
00:11:00
|
||||
00:12:00
|
||||
00:13:00
|
||||
00:14:00
|
||||
00:15:00
|
||||
00:16:00
|
||||
00:17:00
|
||||
00:18:00
|
||||
00:19:00
|
||||
00:20:00
|
||||
00:21:00
|
||||
00:22:00
|
||||
00:23:00
|
||||
00:24:00
|
||||
00:25:00
|
||||
00:26:00
|
||||
00:27:00
|
||||
00:28:00
|
||||
00:29:00
|
||||
00:30:00
|
||||
00:31:00
|
||||
00:32:00
|
||||
00:33:00
|
||||
00:34:00
|
||||
00:35:00
|
||||
00:36:00
|
||||
00:37:00
|
||||
00:38:00
|
||||
00:39:00
|
||||
00:40:00
|
||||
87
whipper/test/common.py
Normal file
87
whipper/test/common.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# -*- Mode: Python -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
|
||||
import whipper
|
||||
|
||||
# twisted's unittests have skip support, standard unittest don't
|
||||
from twisted.trial import unittest
|
||||
|
||||
# lifted from flumotion
|
||||
|
||||
|
||||
def _diff(old, new, desc):
|
||||
import difflib
|
||||
lines = difflib.unified_diff(old, new)
|
||||
lines = list(lines)
|
||||
if not lines:
|
||||
return
|
||||
output = ''
|
||||
for line in lines:
|
||||
output += '%s: %s\n' % (desc, line[:-1])
|
||||
|
||||
raise AssertionError(
|
||||
("\nError while comparing strings:\n"
|
||||
"%s") % (output.encode('utf-8'), ))
|
||||
|
||||
|
||||
def diffStrings(orig, new, desc='input'):
|
||||
|
||||
assert type(orig) == type(new), 'type %s and %s are different' % (
|
||||
type(orig), type(new))
|
||||
|
||||
def _tolines(s):
|
||||
return [line + '\n' for line in s.split('\n')]
|
||||
|
||||
return _diff(_tolines(orig),
|
||||
_tolines(new),
|
||||
desc=desc)
|
||||
|
||||
|
||||
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
|
||||
|
||||
def failUnlessRaises(self, exception, f, *args, **kwargs):
|
||||
try:
|
||||
result = f(*args, **kwargs)
|
||||
except exception, inst:
|
||||
return inst
|
||||
except exception, e:
|
||||
raise Exception('%s raised instead of %s:\n %s' %
|
||||
(sys.exec_info()[0], exception.__name__, str(e))
|
||||
)
|
||||
else:
|
||||
raise Exception('%s not raised (%r returned)' %
|
||||
(exception.__name__, result)
|
||||
)
|
||||
|
||||
assertRaises = failUnlessRaises
|
||||
|
||||
def readCue(self, name):
|
||||
"""
|
||||
Read a .cue file, and replace the version comment with the current
|
||||
version so we can use it in comparisons.
|
||||
"""
|
||||
ret = open(os.path.join(os.path.dirname(__file__), name)).read(
|
||||
).decode('utf-8')
|
||||
ret = re.sub(
|
||||
'REM COMMENT "whipper.*',
|
||||
'REM COMMENT "whipper %s"' % (whipper.__version__),
|
||||
ret, re.MULTILINE)
|
||||
|
||||
return ret
|
||||
|
||||
class UnicodeTestMixin:
|
||||
# A helper mixin to skip tests if we're not in a UTF-8 locale
|
||||
|
||||
try:
|
||||
os.stat(u'morituri.test.B\xeate Noire.empty')
|
||||
except UnicodeEncodeError:
|
||||
skip = 'No UTF-8 locale'
|
||||
except OSError:
|
||||
pass
|
||||
55
whipper/test/cure.cue
Normal file
55
whipper/test/cure.cue
Normal file
@@ -0,0 +1,55 @@
|
||||
REM DISCID B90C650D
|
||||
REM COMMENT "whipper 0.5.1"
|
||||
CATALOG 0602517642256
|
||||
FILE "data.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
ISRC USUM70839873
|
||||
INDEX 01 00:00:00
|
||||
TRACK 02 AUDIO
|
||||
ISRC USUM70839874
|
||||
INDEX 00 06:16:45
|
||||
INDEX 01 06:17:49
|
||||
TRACK 03 AUDIO
|
||||
ISRC USUM70839875
|
||||
INDEX 00 10:13:02
|
||||
INDEX 01 10:14:60
|
||||
TRACK 04 AUDIO
|
||||
ISRC USUM70839876
|
||||
INDEX 00 14:50:07
|
||||
INDEX 01 14:50:17
|
||||
TRACK 05 AUDIO
|
||||
ISRC USUM70839877
|
||||
INDEX 00 17:18:42
|
||||
INDEX 01 17:20:47
|
||||
TRACK 06 AUDIO
|
||||
ISRC USUM70839878
|
||||
INDEX 00 19:43:06
|
||||
INDEX 01 19:43:10
|
||||
TRACK 07 AUDIO
|
||||
ISRC USUM70839879
|
||||
INDEX 00 24:25:07
|
||||
INDEX 01 24:26:41
|
||||
TRACK 08 AUDIO
|
||||
ISRC USUM70839880
|
||||
INDEX 00 28:56:00
|
||||
INDEX 01 28:56:09
|
||||
TRACK 09 AUDIO
|
||||
ISRC USUM70839881
|
||||
INDEX 00 32:38:11
|
||||
INDEX 01 32:40:45
|
||||
TRACK 10 AUDIO
|
||||
ISRC USUM70839882
|
||||
INDEX 00 36:01:58
|
||||
INDEX 01 36:02:04
|
||||
TRACK 11 AUDIO
|
||||
ISRC USUM70839883
|
||||
INDEX 00 40:08:30
|
||||
INDEX 01 40:08:53
|
||||
TRACK 12 AUDIO
|
||||
ISRC USUM70839884
|
||||
INDEX 00 43:59:51
|
||||
INDEX 01 44:00:27
|
||||
TRACK 13 AUDIO
|
||||
ISRC USUM70839885
|
||||
INDEX 00 48:35:63
|
||||
INDEX 01 48:36:71
|
||||
132
whipper/test/cure.toc
Normal file
132
whipper/test/cure.toc
Normal file
@@ -0,0 +1,132 @@
|
||||
CD_DA
|
||||
|
||||
CATALOG "0602517642256"
|
||||
|
||||
// Track 1
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "USUM70839873"
|
||||
FILE "data.wav" 0 06:16:45
|
||||
|
||||
|
||||
// Track 2
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "USUM70839874"
|
||||
FILE "data.wav" 06:16:45 03:56:32
|
||||
START 00:01:04
|
||||
|
||||
|
||||
// Track 3
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "USUM70839875"
|
||||
FILE "data.wav" 10:13:02 04:37:05
|
||||
START 00:01:58
|
||||
|
||||
|
||||
// Track 4
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "USUM70839876"
|
||||
FILE "data.wav" 14:50:07 02:28:35
|
||||
START 00:00:10
|
||||
|
||||
|
||||
// Track 5
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "USUM70839877"
|
||||
FILE "data.wav" 17:18:42 02:24:39
|
||||
START 00:02:05
|
||||
|
||||
|
||||
// Track 6
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "USUM70839878"
|
||||
FILE "data.wav" 19:43:06 04:42:01
|
||||
START 00:00:04
|
||||
|
||||
|
||||
// Track 7
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "USUM70839879"
|
||||
FILE "data.wav" 24:25:07 04:30:68
|
||||
START 00:01:34
|
||||
|
||||
|
||||
// Track 8
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "USUM70839880"
|
||||
FILE "data.wav" 28:56:00 03:42:11
|
||||
START 00:00:09
|
||||
|
||||
|
||||
// Track 9
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "USUM70839881"
|
||||
FILE "data.wav" 32:38:11 03:23:47
|
||||
START 00:02:34
|
||||
|
||||
|
||||
// Track 10
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "USUM70839882"
|
||||
FILE "data.wav" 36:01:58 04:06:47
|
||||
START 00:00:21
|
||||
|
||||
|
||||
// Track 11
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "USUM70839883"
|
||||
FILE "data.wav" 40:08:30 03:51:21
|
||||
START 00:00:23
|
||||
|
||||
|
||||
// Track 12
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "USUM70839884"
|
||||
FILE "data.wav" 43:59:51 04:36:12
|
||||
START 00:00:51
|
||||
|
||||
|
||||
// Track 13
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "USUM70839885"
|
||||
FILE "data.wav" 48:35:63 04:17:71
|
||||
START 00:01:08
|
||||
|
||||
BIN
whipper/test/dBAR-011-0010e284-009228a3-9809ff0b.bin
Normal file
BIN
whipper/test/dBAR-011-0010e284-009228a3-9809ff0b.bin
Normal file
Binary file not shown.
BIN
whipper/test/dBAR-020-002e5023-029d8e49-040eaa14.bin
Normal file
BIN
whipper/test/dBAR-020-002e5023-029d8e49-040eaa14.bin
Normal file
Binary file not shown.
167
whipper/test/jose.toc
Normal file
167
whipper/test/jose.toc
Normal file
@@ -0,0 +1,167 @@
|
||||
CD_DA
|
||||
|
||||
CD_TEXT {
|
||||
LANGUAGE_MAP {
|
||||
0: 9
|
||||
}
|
||||
LANGUAGE 0 {
|
||||
TITLE "In Our Nature"
|
||||
PERFORMER "Jos\351 Gonz\341lez"
|
||||
GENRE { 0, 0, 0}
|
||||
SIZE_INFO { 1, 1, 10, 0, 12, 13, 0, 0, 0, 0, 0, 1,
|
||||
0, 0, 0, 0, 0, 0, 0, 3, 28, 0, 0, 0,
|
||||
0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0}
|
||||
}
|
||||
}
|
||||
|
||||
// Track 1
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "SEVVX0700301"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "How Low"
|
||||
PERFORMER "Jos\351 Gonz\341lez"
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 0 02:40:01
|
||||
|
||||
|
||||
// Track 2
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "SEVVX0700302"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "Down The Line"
|
||||
PERFORMER "Jos\351 Gonz\341lez"
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 02:40:01 03:10:62
|
||||
|
||||
|
||||
// Track 3
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "SEVVX0700303"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "Killing For Love"
|
||||
PERFORMER "Jos\351 Gonz\341lez"
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 05:50:63 03:02:67
|
||||
|
||||
|
||||
// Track 4
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "SEVVX0700304"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "In Our Nature"
|
||||
PERFORMER "Jos\351 Gonz\341lez"
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 08:53:55 02:42:51
|
||||
|
||||
|
||||
// Track 5
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "SEVVX0700305"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "Teardrop"
|
||||
PERFORMER "Jos\351 Gonz\341lez"
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 11:36:31 03:20:64
|
||||
|
||||
|
||||
// Track 6
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "SEVVX0700306"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "Abram"
|
||||
PERFORMER "Jos\351 Gonz\341lez"
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 14:57:20 01:58:56
|
||||
START 00:12:24
|
||||
|
||||
|
||||
// Track 7
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "SEVVX0700307"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "Time To Send Someone Away"
|
||||
PERFORMER "Jos\351 Gonz\341lez"
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 16:56:01 02:49:68
|
||||
START 00:02:05
|
||||
|
||||
|
||||
// Track 8
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "SEVVX0700308"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "The Nest"
|
||||
PERFORMER "Jos\351 Gonz\341lez"
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 19:45:69 02:23:66
|
||||
|
||||
|
||||
// Track 9
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "SEVVX0700309"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "Fold"
|
||||
PERFORMER "Jos\351 Gonz\341lez"
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 22:09:60 02:54:58
|
||||
|
||||
|
||||
// Track 10
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "SEVVX0700310"
|
||||
CD_TEXT {
|
||||
LANGUAGE 0 {
|
||||
TITLE "Cycling Trivialities"
|
||||
PERFORMER "Jos\351 Gonz\341lez"
|
||||
}
|
||||
}
|
||||
FILE "data.wav" 25:04:43 08:09:16
|
||||
|
||||
88
whipper/test/kanye.cue
Normal file
88
whipper/test/kanye.cue
Normal file
@@ -0,0 +1,88 @@
|
||||
REM GENRE "Hip Hop"
|
||||
REM DATE 2008
|
||||
REM DISCID A90D2E0D
|
||||
REM COMMENT "ExactAudioCopy v0.99pb4"
|
||||
CATALOG 0602517931596
|
||||
PERFORMER "Kanye West"
|
||||
TITLE "808s & Heartbreak"
|
||||
FILE "Kanye West - 808s & Heartbreak\Kanye West - Say You Will.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
TITLE "Say You Will"
|
||||
PERFORMER "Kanye West"
|
||||
ISRC USUM70846386
|
||||
INDEX 01 00:00:00
|
||||
FILE "Kanye West - 808s & Heartbreak\Kanye West - Welcome To Heartbreak (Feat. Kid Cudi).wav" WAVE
|
||||
TRACK 02 AUDIO
|
||||
TITLE "Welcome To Heartbreak (Feat. Kid Cudi)"
|
||||
PERFORMER "Kanye West"
|
||||
ISRC USUM70846387
|
||||
INDEX 01 00:00:00
|
||||
TRACK 03 AUDIO
|
||||
TITLE "Heartless"
|
||||
PERFORMER "Kanye West"
|
||||
ISRC USUM70840511
|
||||
INDEX 00 04:22:70
|
||||
FILE "Kanye West - 808s & Heartbreak\Kanye West - Heartless.wav" WAVE
|
||||
INDEX 01 00:00:00
|
||||
FILE "Kanye West - 808s & Heartbreak\Kanye West - Amazing (Feat. Young Jeezy).wav" WAVE
|
||||
TRACK 04 AUDIO
|
||||
TITLE "Amazing (Feat. Young Jeezy)"
|
||||
PERFORMER "Kanye West"
|
||||
ISRC USUM70846401
|
||||
INDEX 01 00:00:00
|
||||
FILE "Kanye West - 808s & Heartbreak\Kanye West - Love Lockdown.wav" WAVE
|
||||
TRACK 05 AUDIO
|
||||
TITLE "Love Lockdown"
|
||||
PERFORMER "Kanye West"
|
||||
ISRC USUM70837229
|
||||
INDEX 01 00:00:00
|
||||
TRACK 06 AUDIO
|
||||
TITLE "Paranoid (Feat. Mr. Hudson)"
|
||||
PERFORMER "Kanye West"
|
||||
ISRC USUM70846402
|
||||
INDEX 00 04:30:23
|
||||
FILE "Kanye West - 808s & Heartbreak\Kanye West - Paranoid (Feat. Mr. Hudson).wav" WAVE
|
||||
INDEX 01 00:00:00
|
||||
FILE "Kanye West - 808s & Heartbreak\Kanye West - RoboCop.wav" WAVE
|
||||
TRACK 07 AUDIO
|
||||
TITLE "RoboCop"
|
||||
PERFORMER "Kanye West"
|
||||
ISRC USUM70846388
|
||||
INDEX 01 00:00:00
|
||||
TRACK 08 AUDIO
|
||||
TITLE "Street Lights"
|
||||
PERFORMER "Kanye West"
|
||||
ISRC USUM70846403
|
||||
INDEX 00 04:34:27
|
||||
FILE "Kanye West - 808s & Heartbreak\Kanye West - Street Lights.wav" WAVE
|
||||
INDEX 01 00:00:00
|
||||
FILE "Kanye West - 808s & Heartbreak\Kanye West - Bad News.wav" WAVE
|
||||
TRACK 09 AUDIO
|
||||
TITLE "Bad News"
|
||||
PERFORMER "Kanye West"
|
||||
ISRC USUM70846389
|
||||
INDEX 01 00:00:00
|
||||
FILE "Kanye West - 808s & Heartbreak\Kanye West - See You In My Nightmares (Feat. Lil Wayne).wav" WAVE
|
||||
TRACK 10 AUDIO
|
||||
TITLE "See You In My Nightmares (Feat. Lil Wayne)"
|
||||
PERFORMER "Kanye West"
|
||||
ISRC USUM70846390
|
||||
INDEX 01 00:00:00
|
||||
TRACK 11 AUDIO
|
||||
TITLE "Coldest Winter"
|
||||
PERFORMER "Kanye West"
|
||||
ISRC USUM70846400
|
||||
INDEX 00 04:18:09
|
||||
FILE "Kanye West - 808s & Heartbreak\Kanye West - Coldest Winter.wav" WAVE
|
||||
INDEX 01 00:00:00
|
||||
TRACK 12 AUDIO
|
||||
TITLE "Pinocchio Story (Freestyle Live From Singapore)"
|
||||
PERFORMER "Kanye West"
|
||||
ISRC USUM70846838
|
||||
INDEX 00 02:44:25
|
||||
FILE "Kanye West - 808s & Heartbreak\Kanye West - Pinocchio Story (Freestyle Live From Singapore).wav" WAVE
|
||||
INDEX 01 00:00:00
|
||||
TRACK 13 MODEx/2xxx
|
||||
TITLE "Data Track"
|
||||
PERFORMER "Kanye West"
|
||||
INDEX 00 06:01:45
|
||||
61
whipper/test/kings-separate.cue
Normal file
61
whipper/test/kings-separate.cue
Normal file
@@ -0,0 +1,61 @@
|
||||
REM GENRE Alternative
|
||||
REM DATE 2008
|
||||
REM DISCID 9809FF0B
|
||||
REM COMMENT "ExactAudioCopy v0.99pb4"
|
||||
PERFORMER "Kings of Leon"
|
||||
TITLE "Only By the Night"
|
||||
FILE "Kings of Leon - Only By the Night\Kings of Leon - Closer.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
TITLE "Closer"
|
||||
PERFORMER "Kings of Leon"
|
||||
INDEX 01 00:00:00
|
||||
FILE "Kings of Leon - Only By the Night\Kings of Leon - Crawl.wav" WAVE
|
||||
TRACK 02 AUDIO
|
||||
TITLE "Crawl"
|
||||
PERFORMER "Kings of Leon"
|
||||
INDEX 01 00:00:00
|
||||
FILE "Kings of Leon - Only By the Night\Kings of Leon - Sex On Fire.wav" WAVE
|
||||
TRACK 03 AUDIO
|
||||
TITLE "Sex On Fire"
|
||||
PERFORMER "Kings of Leon"
|
||||
INDEX 01 00:00:00
|
||||
FILE "Kings of Leon - Only By the Night\Kings of Leon - Use Somebody.wav" WAVE
|
||||
TRACK 04 AUDIO
|
||||
TITLE "Use Somebody"
|
||||
PERFORMER "Kings of Leon"
|
||||
INDEX 01 00:00:00
|
||||
FILE "Kings of Leon - Only By the Night\Kings of Leon - Manhattan.wav" WAVE
|
||||
TRACK 05 AUDIO
|
||||
TITLE "Manhattan"
|
||||
PERFORMER "Kings of Leon"
|
||||
INDEX 01 00:00:00
|
||||
FILE "Kings of Leon - Only By the Night\Kings of Leon - Revelry.wav" WAVE
|
||||
TRACK 06 AUDIO
|
||||
TITLE "Revelry"
|
||||
PERFORMER "Kings of Leon"
|
||||
INDEX 01 00:00:00
|
||||
FILE "Kings of Leon - Only By the Night\Kings of Leon - 17.wav" WAVE
|
||||
TRACK 07 AUDIO
|
||||
TITLE "17"
|
||||
PERFORMER "Kings of Leon"
|
||||
INDEX 01 00:00:00
|
||||
FILE "Kings of Leon - Only By the Night\Kings of Leon - Notion.wav" WAVE
|
||||
TRACK 08 AUDIO
|
||||
TITLE "Notion"
|
||||
PERFORMER "Kings of Leon"
|
||||
INDEX 01 00:00:00
|
||||
FILE "Kings of Leon - Only By the Night\Kings of Leon - I Want You.wav" WAVE
|
||||
TRACK 09 AUDIO
|
||||
TITLE "I Want You"
|
||||
PERFORMER "Kings of Leon"
|
||||
INDEX 01 00:00:00
|
||||
FILE "Kings of Leon - Only By the Night\Kings of Leon - Be Somebody.wav" WAVE
|
||||
TRACK 10 AUDIO
|
||||
TITLE "Be Somebody"
|
||||
PERFORMER "Kings of Leon"
|
||||
INDEX 01 00:00:00
|
||||
FILE "Kings of Leon - Only By the Night\Kings of Leon - Cold Desert.wav" WAVE
|
||||
TRACK 11 AUDIO
|
||||
TITLE "Cold Desert"
|
||||
PERFORMER "Kings of Leon"
|
||||
INDEX 01 00:00:00
|
||||
23
whipper/test/kings-single.cue
Normal file
23
whipper/test/kings-single.cue
Normal file
@@ -0,0 +1,23 @@
|
||||
FILE "dummy.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
INDEX 01 00:00:00
|
||||
TRACK 02 AUDIO
|
||||
INDEX 01 03:57:36
|
||||
TRACK 03 AUDIO
|
||||
INDEX 01 08:03:67
|
||||
TRACK 04 AUDIO
|
||||
INDEX 01 11:27:18
|
||||
TRACK 05 AUDIO
|
||||
INDEX 01 15:18:00
|
||||
TRACK 06 AUDIO
|
||||
INDEX 01 18:42:17
|
||||
TRACK 07 AUDIO
|
||||
INDEX 01 22:03:72
|
||||
TRACK 08 AUDIO
|
||||
INDEX 01 25:09:25
|
||||
TRACK 09 AUDIO
|
||||
INDEX 01 28:10:13
|
||||
TRACK 10 AUDIO
|
||||
INDEX 01 33:17:47
|
||||
TRACK 11 AUDIO
|
||||
INDEX 01 37:04:58
|
||||
130
whipper/test/ladyhawke.toc
Normal file
130
whipper/test/ladyhawke.toc
Normal file
@@ -0,0 +1,130 @@
|
||||
CD_ROM_XA
|
||||
|
||||
CATALOG "0602517818866"
|
||||
|
||||
// Track 1
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBUM70808708"
|
||||
FILE "data.wav" 0 03:26:53
|
||||
|
||||
|
||||
// Track 2
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBUM70810780"
|
||||
FILE "data.wav" 03:26:53 03:35:46
|
||||
START 00:00:34
|
||||
|
||||
|
||||
// Track 3
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBUM70808705"
|
||||
FILE "data.wav" 07:02:24 04:15:03
|
||||
START 00:00:17
|
||||
|
||||
|
||||
// Track 4
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBUM70810804"
|
||||
FILE "data.wav" 11:17:27 03:26:60
|
||||
START 00:00:64
|
||||
|
||||
|
||||
// Track 5
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBUM70810795"
|
||||
FILE "data.wav" 14:44:12 03:17:34
|
||||
START 00:02:04
|
||||
|
||||
|
||||
// Track 6
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBUM70810805"
|
||||
FILE "data.wav" 18:01:46 04:03:13
|
||||
START 00:01:06
|
||||
|
||||
|
||||
// Track 7
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "AUUM70800167"
|
||||
FILE "data.wav" 22:04:59 03:40:53
|
||||
START 00:00:50
|
||||
|
||||
|
||||
// Track 8
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBUM70808467"
|
||||
FILE "data.wav" 25:45:37 03:47:38
|
||||
START 00:00:08
|
||||
|
||||
|
||||
// Track 9
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBUM70810807"
|
||||
FILE "data.wav" 29:33:00 03:44:32
|
||||
START 00:01:43
|
||||
|
||||
|
||||
// Track 10
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBUM70811315"
|
||||
FILE "data.wav" 33:17:32 02:36:03
|
||||
START 00:00:40
|
||||
|
||||
|
||||
// Track 11
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBUM70810809"
|
||||
FILE "data.wav" 35:53:35 03:34:50
|
||||
START 00:00:50
|
||||
|
||||
|
||||
// Track 12
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "GBUM70810814"
|
||||
FILE "data.wav" 39:28:10 06:31:21
|
||||
START 00:00:72
|
||||
|
||||
|
||||
// Track 13
|
||||
TRACK MODE2_FORM_MIX
|
||||
NO COPY
|
||||
ZERO MODE2_FORM_MIX 00:02:00
|
||||
DATAFILE "data_13" 00:43:54 // length in bytes: 7659744
|
||||
START 00:02:00
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"release": {"status": "Official", "artist-credit": [{"artist": {"sort-name": "Buckley, Jeff", "id": "e6e879c0-3d56-4f12-b3c5-3ce459661a8e", "name": "Jeff Buckley"}}], "text-representation": {"language": "eng", "script": "Latn"}, "title": "Everybody Here Wants You", "artist-credit-phrase": "Jeff Buckley", "quality": "normal", "id": "3451f29c-9bb8-4cc5-bfcc-bd50104b94f8", "medium-list": [{"disc-list": [{"id": "C6N7.QADBQ968Qr8OOjxfQlGtA8-", "sectors": "122983"}, {"id": "wbjbST2jUHRZaB1inCyxxsL7Eqc-", "sectors": "122833"}], "position": "1", "track-list": [{"recording": {"artist-credit": [{"artist": {"sort-name": "Buckley, Jeff", "id": "e6e879c0-3d56-4f12-b3c5-3ce459661a8e", "name": "Jeff Buckley"}}], "length": "286920", "artist-credit-phrase": "Jeff Buckley", "id": "8f8c284b-6818-4a66-a517-37dc8c04a881", "title": "Everybody Here Wants You"}, "position": "1"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Buckley, Jeff", "id": "e6e879c0-3d56-4f12-b3c5-3ce459661a8e", "name": "Jeff Buckley"}}], "length": "204746", "artist-credit-phrase": "Jeff Buckley", "id": "7d939d14-06a2-478e-b279-ebe20fae8b2f", "title": "Thousand Fold"}, "position": "2"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Buckley, Jeff", "id": "e6e879c0-3d56-4f12-b3c5-3ce459661a8e", "name": "Jeff Buckley"}}], "length": "288466", "artist-credit-phrase": "Jeff Buckley", "id": "54323c4c-e0f6-4a81-8b80-e1c0b822a3f7", "title": "Eternal Life (road version)"}, "position": "3"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Buckley, Jeff", "id": "e6e879c0-3d56-4f12-b3c5-3ce459661a8e", "name": "Jeff Buckley"}}], "length": "574026", "artist-credit-phrase": "Jeff Buckley", "id": "4dda67d1-8123-4545-9a78-7b4232089e96", "title": "Hallelujah (live)"}, "position": "4"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Buckley, Jeff", "id": "e6e879c0-3d56-4f12-b3c5-3ce459661a8e", "name": "Jeff Buckley"}}], "length": "284000", "artist-credit-phrase": "Jeff Buckley", "id": "5db42013-aa5c-4eb4-a549-46ca721990cf", "title": "Last Goodbye (live from Sydney)"}, "position": "5"}], "format": "CD"}]}}
|
||||
@@ -0,0 +1 @@
|
||||
{"release": {"status": "Official", "asin": "B008R78K1Y", "label-info-list": [{"label": {"sort-name": "Brownswood Recordings", "id": "6483a614-d00f-42b0-af39-a602b3ce5daa", "name": "Brownswood Recordings"}, "catalog-number": "BWOOD090CD"}], "title": "Mala in Cuba", "country": "GB", "barcode": "5060180321505", "artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "medium-list": [{"disc-list": [{"id": "u0aKVpO.59JBy6eQRX2vYcoqQZ0-", "sectors": "257868"}], "position": "1", "track-list": [{"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "155000", "artist-credit-phrase": "Mala", "id": "3fa9c442-6ae7-4242-ae3b-0150a3002da4", "title": "Introduction"}, "position": "1"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "195626", "artist-credit-phrase": "Mala", "id": "983ad5e0-c52e-459d-8828-85718ceff2cc", "title": "Mulata"}, "position": "2"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "242826", "artist-credit-phrase": "Mala", "id": "6855abf0-32a3-4fe2-a3fb-858f3157d42b", "title": "Tribal"}, "position": "3"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "263760", "artist-credit-phrase": "Mala", "id": "2f938885-94ad-4b11-b251-f18c3a2a5fa9", "title": "Changuito"}, "position": "4"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "274520", "artist-credit-phrase": "Mala", "id": "a5ecfa15-06d0-44cf-a28e-c748e8270488", "title": "Revolution"}, "position": "5"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}, " feat. ", {"artist": {"sort-name": "Dreiser", "id": "ec07a209-55ff-4084-bc41-9d4d1764e075", "name": "Dreiser"}}, " & ", {"artist": {"sort-name": "Sexto Sentido", "id": "f626b92e-07b1-4a19-ad13-c09d690db66c", "name": "Sexto Sentido"}}], "length": "227800", "artist-credit-phrase": "Mala feat. Dreiser & Sexto Sentido", "id": "cfb3ddaf-584c-4c86-b58c-752c63977bb8", "title": "Como como"}, "position": "6"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "276693", "artist-credit-phrase": "Mala", "id": "90da8ada-21e2-4e7b-ab46-ff04004a3d84", "title": "Cuba Electronic"}, "position": "7"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "267973", "artist-credit-phrase": "Mala", "id": "2bf67b46-30f5-4746-ab91-4c9675221a21", "title": "The Tunnel"}, "position": "8"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "246000", "artist-credit-phrase": "Mala", "id": "0cd61fa9-a97a-41e3-b3c3-db36f633b611", "title": "Ghost"}, "position": "9"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "250000", "artist-credit-phrase": "Mala", "id": "136989e9-f24f-4872-9026-1487869cc8de", "title": "Curfew"}, "position": "10"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "174000", "artist-credit-phrase": "Mala", "id": "26b6fd89-7021-4239-b6a7-76eca8c0515a", "title": "The Tourist"}, "position": "11"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "270733", "artist-credit-phrase": "Mala", "id": "62f7a892-f63b-4a2b-866f-db2a36533f8c", "title": "Change"}, "position": "12"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}], "length": "251853", "artist-credit-phrase": "Mala", "id": "4395c91a-d5e9-4fe4-92d2-deee3e0ebb5a", "title": "Calle F"}, "position": "13"}, {"recording": {"artist-credit": [{"artist": {"sort-name": "Mala", "id": "09f221eb-c97e-4da5-ac22-d7ab7c555bbb", "name": "Mala"}}, " feat. ", {"artist": {"sort-name": "Suarez, Danay", "id": "82f04998-7da8-4259-aa7f-d623e6ea2b91", "name": "Danay Suarez"}}], "length": "338000", "artist-credit-phrase": "Mala feat. Danay Suarez", "id": "e47a4fd9-8359-4a33-add8-e8c690e59055", "title": "Noche sue\u00f1os"}, "position": "14"}], "format": "CD"}], "text-representation": {"language": "eng", "script": "Latn"}, "date": "2012-09-17", "artist-credit-phrase": "Mala", "quality": "normal", "id": "61c6fd9b-18f8-4a45-963a-ba3c5d990cae"}}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- gotten from http://www.musicbrainz.org/ws/1/release/08397059-86c1-463b-8ed0-cd596dbd174f?type=xml&inc=tracks+release-events+artist -->
|
||||
<metadata xmlns="http://musicbrainz.org/ns/mmd-1.0#" >
|
||||
<release type="Album Official" id="08397059-86c1-463b-8ed0-cd596dbd174f">
|
||||
<title>Das Capital: The Songwriting Genius of Luke Haines and The Auteurs</title><text-representation script="Latn" language="ENG" />
|
||||
<asin>B00009XG2O</asin>
|
||||
<artist id="08bca401-88d5-4de7-b9c3-560a2e4c1abc">
|
||||
<name>Luke Haines</name><sort-name>Haines, Luke</sort-name>
|
||||
</artist>
|
||||
<track-list>
|
||||
<track id="38afa7d3-ddc4-4c19-a54b-1de33657417e">
|
||||
<title>How Could I Be Wrong</title><duration>273800</duration>
|
||||
</track>
|
||||
<track id="811fb30e-5d6d-4a03-b2c7-989032039317">
|
||||
<title>Showgirl</title><duration>256466</duration>
|
||||
</track>
|
||||
<track id="40a5b530-c65b-4ca0-9cfa-79f4f9075d38">
|
||||
<title>Baader Meinhof</title><duration>183933</duration>
|
||||
</track>
|
||||
<track id="939ca81e-d633-47ad-a662-08705a4c8ff9">
|
||||
<title>Lenny Valentino</title><duration>136133</duration>
|
||||
</track>
|
||||
<track id="fe713beb-362c-4fac-91a8-f212fd5e59a7">
|
||||
<title>Starstruck</title><duration>212333</duration>
|
||||
</track>
|
||||
<track id="52f15292-2a52-41a7-a46d-fa84321c26a2">
|
||||
<title>Satan Wants Me</title><duration>189666</duration>
|
||||
</track>
|
||||
<track id="9fd3460a-34c3-488c-82b4-9c95c6b8278d">
|
||||
<title>Unsolved Child Murder</title><duration>146800</duration>
|
||||
</track>
|
||||
<track id="ab3aa8d7-309d-45b4-8533-2f779e3952c1">
|
||||
<title>Junk Shop Clothes</title><duration>166800</duration>
|
||||
</track>
|
||||
<track id="12fed492-5e10-4959-af15-ddf5379e5850">
|
||||
<title>The Mitford Sisters</title><duration>302960</duration>
|
||||
</track>
|
||||
<track id="8c773366-373d-432b-b287-8357d77958d5">
|
||||
<title>Bugger Bognor</title><duration>230573</duration>
|
||||
</track>
|
||||
<track id="48c92b0a-3b99-4662-99b9-21649c33f3ef">
|
||||
<title>Future Generation</title><duration>216266</duration>
|
||||
</track>
|
||||
</track-list>
|
||||
<release-event-list>
|
||||
<event country="GB" format="CD" date="2003-07-21" barcode="724359051727" catalog-number="CDHUT 81" />
|
||||
</release-event-list>
|
||||
</release>
|
||||
</metadata>
|
||||
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<metadata xmlns="http://musicbrainz.org/ns/mmd-1.0#" >
|
||||
<release type="Album Official" id="93a6268c-ddf1-4898-bf93-fb862b1c5c5e">
|
||||
<title>Ladyhawke</title><text-representation script="Latn" language="ENG" />
|
||||
<artist id="2e547c75-36c1-49d0-984e-b14498c936f0">
|
||||
<name>Ladyhawke</name><sort-name>Ladyhawke</sort-name>
|
||||
</artist>
|
||||
<track-list>
|
||||
<track id="ad89d86b-f5c7-47f2-a97a-f2e91a05129a">
|
||||
<title>Magic</title><duration>207000</duration>
|
||||
</track>
|
||||
<track id="05b6718a-eab1-424a-a403-54ff1ef2300f">
|
||||
<title>Manipulating Woman</title><duration>215000</duration>
|
||||
</track>
|
||||
<track id="fc7c9e1e-c68f-41c7-a545-9bcd6b798e1e">
|
||||
<title>My Delirium</title><duration>255000</duration>
|
||||
</track>
|
||||
<track id="2c64b152-63ad-4c91-af62-14a1633ae346">
|
||||
<title>Better Than Sunday</title><duration>208000</duration>
|
||||
</track>
|
||||
<track id="2d85d4e3-8dc4-40e7-b8c7-b44c92d71c47">
|
||||
<title>Another Runaway</title><duration>196000</duration>
|
||||
</track>
|
||||
<track id="a0f8fba5-7c49-43c8-8e52-1834aaa09604">
|
||||
<title>Love Don't Live Here</title><duration>242000</duration>
|
||||
</track>
|
||||
<track id="7cf5b2b6-b39d-4357-8fba-210189f656c2">
|
||||
<title>Back of the Van</title><duration>220000</duration>
|
||||
</track>
|
||||
<track id="0830bd0d-5ffe-40e4-93b3-c125eeff36a0">
|
||||
<title>Paris Is Burning</title><duration>229000</duration>
|
||||
</track>
|
||||
<track id="b7b99255-f4ed-4602-96d0-0de712f37cc7">
|
||||
<title>Professional Suicide</title><duration>223000</duration>
|
||||
</track>
|
||||
<track id="3b5d71ca-bd52-4204-bd82-7ae098f96e58">
|
||||
<title>Dusk Till Dawn</title><duration>156000</duration>
|
||||
</track>
|
||||
<track id="1a2ab650-7084-4b2b-85bd-6d6a91593084">
|
||||
<title>Crazy World</title><duration>215000</duration>
|
||||
</track>
|
||||
<track id="070a3ce4-5cdb-4a99-8b1c-675c02eaf236">
|
||||
<title>Morning Dreams</title><duration>240000</duration>
|
||||
</track>
|
||||
</track-list>
|
||||
<release-event-list>
|
||||
<event country="AU" format="CD" date="2008-09-20" barcode="00602517801974" catalog-number="MODCD093" />
|
||||
</release-event-list>
|
||||
</release>
|
||||
</metadata>
|
||||
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- gotten from get "http://www.musicbrainz.org/ws/1/release/c7d919f4-3ea0-4c4b-a230-b3605f069440?type=xml&inc=tracks+release-events+artist"
|
||||
-->
|
||||
<metadata xmlns="http://musicbrainz.org/ns/mmd-1.0#" >
|
||||
<release type="Album Official" id="c7d919f4-3ea0-4c4b-a230-b3605f069440">
|
||||
<title>Lamprey</title><text-representation script="Latn" language="ENG" />
|
||||
<asin>B00000581T</asin>
|
||||
<artist id="89e4aade-fc77-4a18-8d0c-3554cc9b8c54">
|
||||
<name>Bettie Serveert</name><sort-name>Bettie Serveert</sort-name>
|
||||
</artist>
|
||||
<track-list>
|
||||
<track id="a89c2320-1eae-4f2d-8b0e-6dfac4ae1451">
|
||||
<title>Keepsake</title><duration>378693</duration>
|
||||
</track>
|
||||
<track id="730df656-f504-4866-b5d0-c59ee01524a3">
|
||||
<title>Ray Ray Rain</title><duration>262106</duration>
|
||||
</track>
|
||||
<track id="62ff731e-3cec-4f7c-a614-72adb062fd6c">
|
||||
<title>D. Feathers</title><duration>332626</duration>
|
||||
</track>
|
||||
<track id="e2702c6e-1dae-4a5d-ab16-aed1be105f94">
|
||||
<title>Re-Feel-It</title><duration>238240</duration>
|
||||
</track>
|
||||
<track id="8bc5865e-6286-4cf2-83c8-94061cf4e25f">
|
||||
<title>21 Days</title><duration>203826</duration>
|
||||
</track>
|
||||
<track id="484f7bc6-51bd-44dd-bac6-b8218602d3f0">
|
||||
<title>Cybor*D</title><duration>241800</duration>
|
||||
</track>
|
||||
<track id="e0f2ce29-392e-499d-8ff9-f573e01fb3dc">
|
||||
<title>Tell Me, Sad</title><duration>318333</duration>
|
||||
</track>
|
||||
<track id="a6d4d047-fef8-41f8-88c0-ed077de875a8">
|
||||
<title>Crutches</title><duration>292373</duration>
|
||||
</track>
|
||||
<track id="8a05d376-b09f-4f92-b0d4-10cf177df0f3">
|
||||
<title>Something So Wild</title><duration>171466</duration>
|
||||
</track>
|
||||
<track id="8b3402bd-aa83-4c01-961b-8fd01cf47228">
|
||||
<title>Totally Freaked Out</title><duration>250893</duration>
|
||||
</track>
|
||||
<track id="b5628295-f5be-438e-ae89-6213666fd552">
|
||||
<title>Silent Spring</title><duration>272533</duration>
|
||||
</track>
|
||||
</track-list>
|
||||
<release-event-list>
|
||||
<event country="US" format="CD" date="1995-04-16" barcode="075679250421" catalog-number="OLE-121-2" />
|
||||
</release-event-list>
|
||||
</release>
|
||||
</metadata>
|
||||
408
whipper/test/silentalarm.result.pickle
Normal file
408
whipper/test/silentalarm.result.pickle
Normal file
@@ -0,0 +1,408 @@
|
||||
(lp0
|
||||
(iwhipper.result.result
|
||||
TrackResult
|
||||
p1
|
||||
(dp2
|
||||
S'testcrc'
|
||||
p3
|
||||
L133637600L
|
||||
sS'peak'
|
||||
p4
|
||||
F0.651947021484375
|
||||
sS'copycrc'
|
||||
p5
|
||||
L133637600L
|
||||
sS'quality'
|
||||
p6
|
||||
F1.0
|
||||
sS'number'
|
||||
p7
|
||||
I0
|
||||
sS'filename'
|
||||
p8
|
||||
V/home/thomas/Bloc Party - Silent Alarm/00. Bloc Party - Hidden Track One Audio.flac
|
||||
p9
|
||||
sba(iwhipper.result.result
|
||||
TrackResult
|
||||
p10
|
||||
(dp11
|
||||
S'ARCRC'
|
||||
p12
|
||||
L1726732487L
|
||||
sg3
|
||||
L1476997036L
|
||||
sS'ARDBConfidence'
|
||||
p13
|
||||
I66
|
||||
sg7
|
||||
I1
|
||||
sS'ARDBMaxConfidence'
|
||||
p14
|
||||
I66
|
||||
sS'ARDBCRC'
|
||||
p15
|
||||
I1726732487
|
||||
sg4
|
||||
F0.99993896484375
|
||||
sg6
|
||||
F1.0
|
||||
sS'accurip'
|
||||
p16
|
||||
I01
|
||||
sg8
|
||||
V/home/thomas/Bloc Party - Silent Alarm/01. Bloc Party - Like Eating Glass.flac
|
||||
p17
|
||||
sS'pregap'
|
||||
p18
|
||||
I15220
|
||||
sg5
|
||||
L1476997036L
|
||||
sba(iwhipper.result.result
|
||||
TrackResult
|
||||
p19
|
||||
(dp20
|
||||
g12
|
||||
L3896378645L
|
||||
sg3
|
||||
L2118180996L
|
||||
sg13
|
||||
I65
|
||||
sg7
|
||||
I2
|
||||
sg14
|
||||
I65
|
||||
sg15
|
||||
L3896378645L
|
||||
sg4
|
||||
F0.99908447265625
|
||||
sg6
|
||||
F1.0
|
||||
sg16
|
||||
I01
|
||||
sg8
|
||||
V/home/thomas/Bloc Party - Silent Alarm/02. Bloc Party - Helicopter.flac
|
||||
p21
|
||||
sg18
|
||||
I0
|
||||
sg5
|
||||
L2118180996L
|
||||
sba(iwhipper.result.result
|
||||
TrackResult
|
||||
p22
|
||||
(dp23
|
||||
g12
|
||||
L1246554911L
|
||||
sg3
|
||||
L2397618238L
|
||||
sg13
|
||||
I66
|
||||
sg7
|
||||
I3
|
||||
sg14
|
||||
I66
|
||||
sg15
|
||||
I1246554911
|
||||
sg4
|
||||
F0.999969482421875
|
||||
sg6
|
||||
F1.0
|
||||
sg16
|
||||
I01
|
||||
sg8
|
||||
V/home/thomas/Bloc Party - Silent Alarm/03. Bloc Party - Positive Tension.flac
|
||||
p24
|
||||
sg18
|
||||
I0
|
||||
sg5
|
||||
L2397618238L
|
||||
sba(iwhipper.result.result
|
||||
TrackResult
|
||||
p25
|
||||
(dp26
|
||||
g12
|
||||
L175751014L
|
||||
sg3
|
||||
L1340624205L
|
||||
sg13
|
||||
I65
|
||||
sg7
|
||||
I4
|
||||
sg14
|
||||
I65
|
||||
sg15
|
||||
I175751014
|
||||
sg4
|
||||
F0.9990234375
|
||||
sg6
|
||||
F1.0
|
||||
sg16
|
||||
I01
|
||||
sg8
|
||||
V/home/thomas/Bloc Party - Silent Alarm/04. Bloc Party - Banquet.flac
|
||||
p27
|
||||
sg18
|
||||
I0
|
||||
sg5
|
||||
L1340624205L
|
||||
sba(iwhipper.result.result
|
||||
TrackResult
|
||||
p28
|
||||
(dp29
|
||||
g12
|
||||
L3375033750L
|
||||
sg3
|
||||
L183201985L
|
||||
sg13
|
||||
I66
|
||||
sg7
|
||||
I5
|
||||
sg14
|
||||
I66
|
||||
sg15
|
||||
L3375033750L
|
||||
sg4
|
||||
F0.9990234375
|
||||
sg6
|
||||
F1.0
|
||||
sg16
|
||||
I01
|
||||
sg8
|
||||
V/home/thomas/Bloc Party - Silent Alarm/05. Bloc Party - Blue Light.flac
|
||||
p30
|
||||
sg18
|
||||
I72
|
||||
sg5
|
||||
L183201985L
|
||||
sba(iwhipper.result.result
|
||||
TrackResult
|
||||
p31
|
||||
(dp32
|
||||
g12
|
||||
L3357757503L
|
||||
sg3
|
||||
L221401921L
|
||||
sg13
|
||||
I66
|
||||
sg7
|
||||
I6
|
||||
sg14
|
||||
I66
|
||||
sg15
|
||||
L3357757503L
|
||||
sg4
|
||||
F0.9990234375
|
||||
sg6
|
||||
F1.0
|
||||
sg16
|
||||
I01
|
||||
sg8
|
||||
V/home/thomas/Bloc Party - Silent Alarm/06. Bloc Party - She's Hearing Voices.flac
|
||||
p33
|
||||
sg18
|
||||
I41
|
||||
sg5
|
||||
L221401921L
|
||||
sba(iwhipper.result.result
|
||||
TrackResult
|
||||
p34
|
||||
(dp35
|
||||
g12
|
||||
L3964329421L
|
||||
sg3
|
||||
L3133726276L
|
||||
sg13
|
||||
I65
|
||||
sg7
|
||||
I7
|
||||
sg14
|
||||
I65
|
||||
sg15
|
||||
L3964329421L
|
||||
sg4
|
||||
F0.999969482421875
|
||||
sg6
|
||||
F1.0
|
||||
sg16
|
||||
I01
|
||||
sg8
|
||||
V/home/thomas/Bloc Party - Silent Alarm/07. Bloc Party - This Modern Love.flac
|
||||
p36
|
||||
sg18
|
||||
I17
|
||||
sg5
|
||||
L3133726276L
|
||||
sba(iwhipper.result.result
|
||||
TrackResult
|
||||
p37
|
||||
(dp38
|
||||
g12
|
||||
L1808393808L
|
||||
sg3
|
||||
L2318646110L
|
||||
sg13
|
||||
I66
|
||||
sg7
|
||||
I8
|
||||
sg14
|
||||
I66
|
||||
sg15
|
||||
I1808393808
|
||||
sg4
|
||||
F0.9990234375
|
||||
sg6
|
||||
F1.0
|
||||
sg16
|
||||
I01
|
||||
sg8
|
||||
V/home/thomas/Bloc Party - Silent Alarm/08. Bloc Party - The Pioneers.flac
|
||||
p39
|
||||
sg18
|
||||
I4
|
||||
sg5
|
||||
L2318646110L
|
||||
sba(iwhipper.result.result
|
||||
TrackResult
|
||||
p40
|
||||
(dp41
|
||||
g12
|
||||
L4144642428L
|
||||
sg3
|
||||
L3145161267L
|
||||
sg13
|
||||
I66
|
||||
sg7
|
||||
I9
|
||||
sg14
|
||||
I66
|
||||
sg15
|
||||
L4144642428L
|
||||
sg4
|
||||
F0.9990234375
|
||||
sg6
|
||||
F1.0
|
||||
sg16
|
||||
I01
|
||||
sg8
|
||||
V/home/thomas/Bloc Party - Silent Alarm/09. Bloc Party - Price of Gasoline.flac
|
||||
p42
|
||||
sg18
|
||||
I11
|
||||
sg5
|
||||
L3145161267L
|
||||
sba(iwhipper.result.result
|
||||
TrackResult
|
||||
p43
|
||||
(dp44
|
||||
g12
|
||||
L4287362638L
|
||||
sg3
|
||||
L3022257630L
|
||||
sg13
|
||||
I65
|
||||
sg7
|
||||
I10
|
||||
sg14
|
||||
I65
|
||||
sg15
|
||||
L4287362638L
|
||||
sg4
|
||||
F0.9990234375
|
||||
sg6
|
||||
F1.0
|
||||
sg16
|
||||
I01
|
||||
sg8
|
||||
V/home/thomas/Bloc Party - Silent Alarm/10. Bloc Party - So Here We Are.flac
|
||||
p45
|
||||
sg18
|
||||
I0
|
||||
sg5
|
||||
L3022257630L
|
||||
sba(iwhipper.result.result
|
||||
TrackResult
|
||||
p46
|
||||
(dp47
|
||||
g12
|
||||
L4127263616L
|
||||
sg3
|
||||
L2011827324L
|
||||
sg13
|
||||
I65
|
||||
sg7
|
||||
I11
|
||||
sg14
|
||||
I65
|
||||
sg15
|
||||
L4127263616L
|
||||
sg4
|
||||
F0.999481201171875
|
||||
sg6
|
||||
F1.0
|
||||
sg16
|
||||
I01
|
||||
sg8
|
||||
V/home/thomas/Bloc Party - Silent Alarm/11. Bloc Party - Luno.flac
|
||||
p48
|
||||
sg18
|
||||
I43
|
||||
sg5
|
||||
L2011827324L
|
||||
sba(iwhipper.result.result
|
||||
TrackResult
|
||||
p49
|
||||
(dp50
|
||||
g12
|
||||
L2559991386L
|
||||
sg3
|
||||
L933582879L
|
||||
sg13
|
||||
I65
|
||||
sg7
|
||||
I12
|
||||
sg14
|
||||
I65
|
||||
sg15
|
||||
L2559991386L
|
||||
sg4
|
||||
F0.999969482421875
|
||||
sg6
|
||||
F1.0
|
||||
sg16
|
||||
I01
|
||||
sg8
|
||||
V/home/thomas/Bloc Party - Silent Alarm/12. Bloc Party - Plans.flac
|
||||
p51
|
||||
sg18
|
||||
I116
|
||||
sg5
|
||||
L933582879L
|
||||
sba(iwhipper.result.result
|
||||
TrackResult
|
||||
p52
|
||||
(dp53
|
||||
g12
|
||||
L2915053507L
|
||||
sg3
|
||||
L1187281525L
|
||||
sg13
|
||||
I66
|
||||
sg7
|
||||
I13
|
||||
sg14
|
||||
I66
|
||||
sg15
|
||||
L2915053507L
|
||||
sg4
|
||||
F0.999969482421875
|
||||
sg6
|
||||
F1.0
|
||||
sg16
|
||||
I01
|
||||
sg8
|
||||
V/home/thomas/Bloc Party - Silent Alarm/13. Bloc Party - Compliments.flac
|
||||
p54
|
||||
sg18
|
||||
I22
|
||||
sg5
|
||||
L1187281525L
|
||||
sba.
|
||||
13
whipper/test/strokes-someday.eac.cue
Normal file
13
whipper/test/strokes-someday.eac.cue
Normal file
@@ -0,0 +1,13 @@
|
||||
REM GENRE "Alternative Rock"
|
||||
REM DATE 2001
|
||||
REM DISCID 0200BA01
|
||||
REM COMMENT "ExactAudioCopy v0.99pb4"
|
||||
PERFORMER "The Strokes"
|
||||
TITLE "Someday"
|
||||
FILE "The Strokes - Someday\01 - The Strokes - Someday.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
TITLE "Someday"
|
||||
PERFORMER "The Strokes"
|
||||
FLAGS DCP
|
||||
PREGAP 00:00:01
|
||||
INDEX 01 00:00:00
|
||||
12
whipper/test/strokes-someday.toc
Normal file
12
whipper/test/strokes-someday.toc
Normal file
@@ -0,0 +1,12 @@
|
||||
CD_DA
|
||||
|
||||
|
||||
// Track 1
|
||||
TRACK AUDIO
|
||||
COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
SILENCE 00:00:01
|
||||
FILE "data.wav" 0 03:06:59
|
||||
START 00:00:01
|
||||
|
||||
136
whipper/test/surferrosa.eac.corrected.cue
Normal file
136
whipper/test/surferrosa.eac.corrected.cue
Normal file
@@ -0,0 +1,136 @@
|
||||
REM GENRE Alternative
|
||||
REM DATE 1987
|
||||
REM DISCID 350CAA15
|
||||
REM COMMENT "ExactAudioCopy v0.99pb4"
|
||||
CATALOG 0000000000000
|
||||
PERFORMER "Pixies"
|
||||
TITLE "Surfer Rosa & Come on Pilgrim"
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\01 - Pixies - Bone Machine.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
TITLE "Bone Machine"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 00 00:00:00
|
||||
INDEX 01 00:00:32
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\02 - Pixies - Break My Body.wav" WAVE
|
||||
TRACK 02 AUDIO
|
||||
TITLE "Break My Body"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\03 - Pixies - Something Against You.wav" WAVE
|
||||
TRACK 03 AUDIO
|
||||
TITLE "Something Against You"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 00 00:00:00
|
||||
INDEX 01 00:00:45
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\04 - Pixies - Broken Face.wav" WAVE
|
||||
TRACK 04 AUDIO
|
||||
TITLE "Broken Face"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\05 - Pixies - Gigantic.wav" WAVE
|
||||
TRACK 05 AUDIO
|
||||
TITLE "Gigantic"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\06 - Pixies - River Euphrates.wav" WAVE
|
||||
TRACK 06 AUDIO
|
||||
TITLE "River Euphrates"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\07 - Pixies - Where Is My Mind .wav" WAVE
|
||||
TRACK 07 AUDIO
|
||||
TITLE "Where Is My Mind?"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\08 - Pixies - Cactus.wav" WAVE
|
||||
TRACK 08 AUDIO
|
||||
TITLE "Cactus"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\09 - Pixies - Tony's Theme.wav" WAVE
|
||||
TRACK 09 AUDIO
|
||||
TITLE "Tony's Theme"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\10 - Pixies - Oh My Golly!.wav" WAVE
|
||||
TRACK 10 AUDIO
|
||||
TITLE "Oh My Golly!"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\11 - Pixies - Vamos.wav" WAVE
|
||||
TRACK 11 AUDIO
|
||||
TITLE "Vamos"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
INDEX 02 00:44:70
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\12 - Pixies - I'm Amazed.wav" WAVE
|
||||
TRACK 12 AUDIO
|
||||
TITLE "I'm Amazed"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\13 - Pixies - Brick is Red.wav" WAVE
|
||||
TRACK 13 AUDIO
|
||||
TITLE "Brick is Red"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\14 - Pixies - Caribou.wav" WAVE
|
||||
TRACK 14 AUDIO
|
||||
TITLE "Caribou"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\15 - Pixies - Vamos.wav" WAVE
|
||||
TRACK 15 AUDIO
|
||||
TITLE "Vamos"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\16 - Pixies - Isla de Encanta.wav" WAVE
|
||||
TRACK 16 AUDIO
|
||||
TITLE "Isla de Encanta"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\17 - Pixies - Ed is Dead.wav" WAVE
|
||||
TRACK 17 AUDIO
|
||||
TITLE "Ed is Dead"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\18 - Pixies - The Holyday Song.wav" WAVE
|
||||
TRACK 18 AUDIO
|
||||
TITLE "The Holyday Song"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\19 - Pixies - Nimrod's Son.wav" WAVE
|
||||
TRACK 19 AUDIO
|
||||
TITLE "Nimrod's Son"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\20 - Pixies - I've Been Tired.wav" WAVE
|
||||
TRACK 20 AUDIO
|
||||
TITLE "I've Been Tired"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\21 - Pixies - Levitate Me.wav" WAVE
|
||||
TRACK 21 AUDIO
|
||||
TITLE "Levitate Me"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
136
whipper/test/surferrosa.eac.currentgap.cue
Normal file
136
whipper/test/surferrosa.eac.currentgap.cue
Normal file
@@ -0,0 +1,136 @@
|
||||
REM GENRE Alternative
|
||||
REM DATE 1987
|
||||
REM DISCID 350CAA15
|
||||
REM COMMENT "ExactAudioCopy v0.99pb4"
|
||||
CATALOG 0000000000000
|
||||
PERFORMER "Pixies"
|
||||
TITLE "Surfer Rosa & Come on Pilgrim"
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\01 - Pixies - Bone Machine.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
TITLE "Bone Machine"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
PREGAP 00:00:32
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\02 - Pixies - Break My Body.wav" WAVE
|
||||
TRACK 02 AUDIO
|
||||
TITLE "Break My Body"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
TRACK 03 AUDIO
|
||||
TITLE "Something Against You"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 00 02:05:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\03 - Pixies - Something Against You.wav" WAVE
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\04 - Pixies - Broken Face.wav" WAVE
|
||||
TRACK 04 AUDIO
|
||||
TITLE "Broken Face"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\05 - Pixies - Gigantic.wav" WAVE
|
||||
TRACK 05 AUDIO
|
||||
TITLE "Gigantic"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\06 - Pixies - River Euphrates.wav" WAVE
|
||||
TRACK 06 AUDIO
|
||||
TITLE "River Euphrates"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\07 - Pixies - Where Is My Mind .wav" WAVE
|
||||
TRACK 07 AUDIO
|
||||
TITLE "Where Is My Mind?"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\08 - Pixies - Cactus.wav" WAVE
|
||||
TRACK 08 AUDIO
|
||||
TITLE "Cactus"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\09 - Pixies - Tony's Theme.wav" WAVE
|
||||
TRACK 09 AUDIO
|
||||
TITLE "Tony's Theme"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\10 - Pixies - Oh My Golly!.wav" WAVE
|
||||
TRACK 10 AUDIO
|
||||
TITLE "Oh My Golly!"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\11 - Pixies - Vamos.wav" WAVE
|
||||
TRACK 11 AUDIO
|
||||
TITLE "Vamos"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
INDEX 02 00:44:70
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\12 - Pixies - I'm Amazed.wav" WAVE
|
||||
TRACK 12 AUDIO
|
||||
TITLE "I'm Amazed"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\13 - Pixies - Brick is Red.wav" WAVE
|
||||
TRACK 13 AUDIO
|
||||
TITLE "Brick is Red"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\14 - Pixies - Caribou.wav" WAVE
|
||||
TRACK 14 AUDIO
|
||||
TITLE "Caribou"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\15 - Pixies - Vamos.wav" WAVE
|
||||
TRACK 15 AUDIO
|
||||
TITLE "Vamos"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\16 - Pixies - Isla de Encanta.wav" WAVE
|
||||
TRACK 16 AUDIO
|
||||
TITLE "Isla de Encanta"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\17 - Pixies - Ed is Dead.wav" WAVE
|
||||
TRACK 17 AUDIO
|
||||
TITLE "Ed is Dead"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\18 - Pixies - The Holyday Song.wav" WAVE
|
||||
TRACK 18 AUDIO
|
||||
TITLE "The Holyday Song"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\19 - Pixies - Nimrod's Son.wav" WAVE
|
||||
TRACK 19 AUDIO
|
||||
TITLE "Nimrod's Son"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\20 - Pixies - I've Been Tired.wav" WAVE
|
||||
TRACK 20 AUDIO
|
||||
TITLE "I've Been Tired"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\21 - Pixies - Levitate Me.wav" WAVE
|
||||
TRACK 21 AUDIO
|
||||
TITLE "Levitate Me"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
136
whipper/test/surferrosa.eac.leftout.cue
Normal file
136
whipper/test/surferrosa.eac.leftout.cue
Normal file
@@ -0,0 +1,136 @@
|
||||
REM GENRE Alternative
|
||||
REM DATE 1987
|
||||
REM DISCID 350CAA15
|
||||
REM COMMENT "ExactAudioCopy v0.99pb4"
|
||||
CATALOG 0000000000000
|
||||
PERFORMER "Pixies"
|
||||
TITLE "Surfer Rosa & Come on Pilgrim"
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\01 - Pixies - Bone Machine.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
TITLE "Bone Machine"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
PREGAP 00:00:32
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\02 - Pixies - Break My Body.wav" WAVE
|
||||
TRACK 02 AUDIO
|
||||
TITLE "Break My Body"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\03 - Pixies - Something Against You.wav" WAVE
|
||||
TRACK 03 AUDIO
|
||||
TITLE "Something Against You"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
PREGAP 00:00:45
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\04 - Pixies - Broken Face.wav" WAVE
|
||||
TRACK 04 AUDIO
|
||||
TITLE "Broken Face"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\05 - Pixies - Gigantic.wav" WAVE
|
||||
TRACK 05 AUDIO
|
||||
TITLE "Gigantic"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\06 - Pixies - River Euphrates.wav" WAVE
|
||||
TRACK 06 AUDIO
|
||||
TITLE "River Euphrates"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\07 - Pixies - Where Is My Mind .wav" WAVE
|
||||
TRACK 07 AUDIO
|
||||
TITLE "Where Is My Mind?"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\08 - Pixies - Cactus.wav" WAVE
|
||||
TRACK 08 AUDIO
|
||||
TITLE "Cactus"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\09 - Pixies - Tony's Theme.wav" WAVE
|
||||
TRACK 09 AUDIO
|
||||
TITLE "Tony's Theme"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\10 - Pixies - Oh My Golly!.wav" WAVE
|
||||
TRACK 10 AUDIO
|
||||
TITLE "Oh My Golly!"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\11 - Pixies - Vamos.wav" WAVE
|
||||
TRACK 11 AUDIO
|
||||
TITLE "Vamos"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
INDEX 02 00:44:70
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\12 - Pixies - I'm Amazed.wav" WAVE
|
||||
TRACK 12 AUDIO
|
||||
TITLE "I'm Amazed"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\13 - Pixies - Brick is Red.wav" WAVE
|
||||
TRACK 13 AUDIO
|
||||
TITLE "Brick is Red"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\14 - Pixies - Caribou.wav" WAVE
|
||||
TRACK 14 AUDIO
|
||||
TITLE "Caribou"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\15 - Pixies - Vamos.wav" WAVE
|
||||
TRACK 15 AUDIO
|
||||
TITLE "Vamos"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\16 - Pixies - Isla de Encanta.wav" WAVE
|
||||
TRACK 16 AUDIO
|
||||
TITLE "Isla de Encanta"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\17 - Pixies - Ed is Dead.wav" WAVE
|
||||
TRACK 17 AUDIO
|
||||
TITLE "Ed is Dead"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\18 - Pixies - The Holyday Song.wav" WAVE
|
||||
TRACK 18 AUDIO
|
||||
TITLE "The Holyday Song"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\19 - Pixies - Nimrod's Son.wav" WAVE
|
||||
TRACK 19 AUDIO
|
||||
TITLE "Nimrod's Son"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\20 - Pixies - I've Been Tired.wav" WAVE
|
||||
TRACK 20 AUDIO
|
||||
TITLE "I've Been Tired"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\21 - Pixies - Levitate Me.wav" WAVE
|
||||
TRACK 21 AUDIO
|
||||
TITLE "Levitate Me"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
136
whipper/test/surferrosa.eac.noncompliant.cue
Normal file
136
whipper/test/surferrosa.eac.noncompliant.cue
Normal file
@@ -0,0 +1,136 @@
|
||||
REM GENRE Alternative
|
||||
REM DATE 1987
|
||||
REM DISCID 350CAA15
|
||||
REM COMMENT "ExactAudioCopy v0.99pb4"
|
||||
CATALOG 0000000000000
|
||||
PERFORMER "Pixies"
|
||||
TITLE "Surfer Rosa & Come on Pilgrim"
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\01 - Pixies - Bone Machine.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
TITLE "Bone Machine"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
PREGAP 00:00:32
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\02 - Pixies - Break My Body.wav" WAVE
|
||||
TRACK 02 AUDIO
|
||||
TITLE "Break My Body"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
TRACK 03 AUDIO
|
||||
TITLE "Something Against You"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 00 02:05:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\03 - Pixies - Something Against You.wav" WAVE
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\04 - Pixies - Broken Face.wav" WAVE
|
||||
TRACK 04 AUDIO
|
||||
TITLE "Broken Face"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\05 - Pixies - Gigantic.wav" WAVE
|
||||
TRACK 05 AUDIO
|
||||
TITLE "Gigantic"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\06 - Pixies - River Euphrates.wav" WAVE
|
||||
TRACK 06 AUDIO
|
||||
TITLE "River Euphrates"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\07 - Pixies - Where Is My Mind .wav" WAVE
|
||||
TRACK 07 AUDIO
|
||||
TITLE "Where Is My Mind?"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\08 - Pixies - Cactus.wav" WAVE
|
||||
TRACK 08 AUDIO
|
||||
TITLE "Cactus"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\09 - Pixies - Tony's Theme.wav" WAVE
|
||||
TRACK 09 AUDIO
|
||||
TITLE "Tony's Theme"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\10 - Pixies - Oh My Golly!.wav" WAVE
|
||||
TRACK 10 AUDIO
|
||||
TITLE "Oh My Golly!"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\11 - Pixies - Vamos.wav" WAVE
|
||||
TRACK 11 AUDIO
|
||||
TITLE "Vamos"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
INDEX 02 00:44:70
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\12 - Pixies - I'm Amazed.wav" WAVE
|
||||
TRACK 12 AUDIO
|
||||
TITLE "I'm Amazed"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\13 - Pixies - Brick is Red.wav" WAVE
|
||||
TRACK 13 AUDIO
|
||||
TITLE "Brick is Red"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\14 - Pixies - Caribou.wav" WAVE
|
||||
TRACK 14 AUDIO
|
||||
TITLE "Caribou"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\15 - Pixies - Vamos.wav" WAVE
|
||||
TRACK 15 AUDIO
|
||||
TITLE "Vamos"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\16 - Pixies - Isla de Encanta.wav" WAVE
|
||||
TRACK 16 AUDIO
|
||||
TITLE "Isla de Encanta"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\17 - Pixies - Ed is Dead.wav" WAVE
|
||||
TRACK 17 AUDIO
|
||||
TITLE "Ed is Dead"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\18 - Pixies - The Holyday Song.wav" WAVE
|
||||
TRACK 18 AUDIO
|
||||
TITLE "The Holyday Song"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\19 - Pixies - Nimrod's Son.wav" WAVE
|
||||
TRACK 19 AUDIO
|
||||
TITLE "Nimrod's Son"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\20 - Pixies - I've Been Tired.wav" WAVE
|
||||
TRACK 20 AUDIO
|
||||
TITLE "I've Been Tired"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
FILE "Pixies - Surfer Rosa & Come on Pilgrim\21 - Pixies - Levitate Me.wav" WAVE
|
||||
TRACK 21 AUDIO
|
||||
TITLE "Levitate Me"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 00:00:00
|
||||
116
whipper/test/surferrosa.eac.single.cue
Normal file
116
whipper/test/surferrosa.eac.single.cue
Normal file
@@ -0,0 +1,116 @@
|
||||
REM GENRE Alternative
|
||||
REM DATE 1987
|
||||
REM DISCID 350CAA15
|
||||
REM COMMENT "ExactAudioCopy v0.99pb4"
|
||||
CATALOG 0000000000000
|
||||
PERFORMER "Pixies"
|
||||
TITLE "Surfer Rosa & Come on Pilgrim"
|
||||
FILE "Range.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
TITLE "Bone Machine"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 00 00:00:00
|
||||
INDEX 01 00:00:32
|
||||
TRACK 02 AUDIO
|
||||
TITLE "Break My Body"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 03:03:42
|
||||
TRACK 03 AUDIO
|
||||
TITLE "Something Against You"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 00 05:08:42
|
||||
INDEX 01 05:09:12
|
||||
TRACK 04 AUDIO
|
||||
TITLE "Broken Face"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 06:56:67
|
||||
TRACK 05 AUDIO
|
||||
TITLE "Gigantic"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 08:27:00
|
||||
TRACK 06 AUDIO
|
||||
TITLE "River Euphrates"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 12:21:70
|
||||
TRACK 07 AUDIO
|
||||
TITLE "Where Is My Mind?"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 14:53:60
|
||||
TRACK 08 AUDIO
|
||||
TITLE "Cactus"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 18:47:15
|
||||
TRACK 09 AUDIO
|
||||
TITLE "Tony's Theme"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 21:03:70
|
||||
TRACK 10 AUDIO
|
||||
TITLE "Oh My Golly!"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 22:56:15
|
||||
TRACK 11 AUDIO
|
||||
TITLE "Vamos"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 24:43:32
|
||||
INDEX 02 25:28:27
|
||||
TRACK 12 AUDIO
|
||||
TITLE "I'm Amazed"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 29:49:20
|
||||
TRACK 13 AUDIO
|
||||
TITLE "Brick is Red"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 31:31:27
|
||||
TRACK 14 AUDIO
|
||||
TITLE "Caribou"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 33:32:20
|
||||
TRACK 15 AUDIO
|
||||
TITLE "Vamos"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 36:46:45
|
||||
TRACK 16 AUDIO
|
||||
TITLE "Isla de Encanta"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 39:40:22
|
||||
TRACK 17 AUDIO
|
||||
TITLE "Ed is Dead"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 41:21:47
|
||||
TRACK 18 AUDIO
|
||||
TITLE "The Holyday Song"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 43:51:47
|
||||
TRACK 19 AUDIO
|
||||
TITLE "Nimrod's Son"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 46:06:10
|
||||
TRACK 20 AUDIO
|
||||
TITLE "I've Been Tired"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 48:23:25
|
||||
TRACK 21 AUDIO
|
||||
TITLE "Levitate Me"
|
||||
PERFORMER "Pixies"
|
||||
ISRC 000000000000
|
||||
INDEX 01 51:24:07
|
||||
196
whipper/test/surferrosa.toc
Normal file
196
whipper/test/surferrosa.toc
Normal file
@@ -0,0 +1,196 @@
|
||||
CD_DA
|
||||
|
||||
CATALOG "0000000000000"
|
||||
|
||||
// Track 1
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
SILENCE 00:00:32
|
||||
FILE "data.wav" 0 03:03:10
|
||||
START 00:00:32
|
||||
|
||||
|
||||
// Track 2
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 03:03:10 02:05:00
|
||||
|
||||
|
||||
// Track 3
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 05:08:10 01:48:25
|
||||
START 00:00:45
|
||||
|
||||
|
||||
// Track 4
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 06:56:35 01:30:08
|
||||
|
||||
|
||||
// Track 5
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 08:26:43 03:54:70
|
||||
|
||||
|
||||
// Track 6
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 12:21:38 02:31:65
|
||||
|
||||
|
||||
// Track 7
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 14:53:28 03:53:30
|
||||
|
||||
|
||||
// Track 8
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 18:46:58 02:16:55
|
||||
|
||||
|
||||
// Track 9
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 21:03:38 01:52:20
|
||||
|
||||
|
||||
// Track 10
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 22:55:58 01:47:17
|
||||
|
||||
|
||||
// Track 11
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 24:43:00 05:05:63
|
||||
INDEX 00:44:70
|
||||
|
||||
|
||||
// Track 12
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 29:48:63 01:42:07
|
||||
|
||||
|
||||
// Track 13
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 31:30:70 02:00:68
|
||||
|
||||
|
||||
// Track 14
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 33:31:63 03:14:25
|
||||
|
||||
|
||||
// Track 15
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 36:46:13 02:53:52
|
||||
|
||||
|
||||
// Track 16
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 39:39:65 01:41:25
|
||||
|
||||
|
||||
// Track 17
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 41:21:15 02:30:00
|
||||
|
||||
|
||||
// Track 18
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 43:51:15 02:14:38
|
||||
|
||||
|
||||
// Track 19
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 46:05:53 02:17:15
|
||||
|
||||
|
||||
// Track 20
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 48:22:68 03:00:57
|
||||
|
||||
|
||||
// Track 21
|
||||
TRACK AUDIO
|
||||
NO COPY
|
||||
NO PRE_EMPHASIS
|
||||
TWO_CHANNEL_AUDIO
|
||||
ISRC "000000000000"
|
||||
FILE "data.wav" 51:23:50 02:38:38
|
||||
|
||||
32
whipper/test/test_common_accurip.py
Normal file
32
whipper/test/test_common_accurip.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_accurip -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
|
||||
from whipper.common import accurip
|
||||
|
||||
from whipper.test import common as tcommon
|
||||
|
||||
|
||||
class AccurateRipResponseTestCase(tcommon.TestCase):
|
||||
|
||||
def testResponse(self):
|
||||
path = os.path.join(os.path.dirname(__file__),
|
||||
'dBAR-011-0010e284-009228a3-9809ff0b.bin')
|
||||
data = open(path, "rb").read()
|
||||
|
||||
responses = accurip.getAccurateRipResponses(data)
|
||||
self.assertEquals(len(responses), 3)
|
||||
|
||||
|
||||
response = responses[0]
|
||||
|
||||
self.assertEquals(response.trackCount, 11)
|
||||
self.assertEquals(response.discId1, "0010e284")
|
||||
self.assertEquals(response.discId2, "009228a3")
|
||||
self.assertEquals(response.cddbDiscId, "9809ff0b")
|
||||
|
||||
for i in range(11):
|
||||
self.assertEquals(response.confidences[i], 35)
|
||||
self.assertEquals(response.checksums[0], "beea32c8")
|
||||
self.assertEquals(response.checksums[10], "acee98ca")
|
||||
23
whipper/test/test_common_cache.py
Normal file
23
whipper/test/test_common_cache.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_cache -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
|
||||
from whipper.common import cache
|
||||
|
||||
from whipper.test import common as tcommon
|
||||
|
||||
|
||||
class ResultCacheTestCase(tcommon.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.cache = cache.ResultCache(
|
||||
os.path.join(os.path.dirname(__file__), 'cache', 'result'))
|
||||
|
||||
def testGetResult(self):
|
||||
result = self.cache.getRipResult('fe105a11')
|
||||
self.assertEquals(result.object.title, "The Writing's on the Wall")
|
||||
|
||||
def testGetIds(self):
|
||||
ids = self.cache.getIds()
|
||||
self.assertEquals(ids, ['fe105a11'])
|
||||
67
whipper/test/test_common_common.py
Normal file
67
whipper/test/test_common_common.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_common -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from whipper.common import common
|
||||
|
||||
from whipper.test import common as tcommon
|
||||
|
||||
|
||||
class ShrinkTestCase(tcommon.TestCase):
|
||||
|
||||
def testSufjan(self):
|
||||
path = (u'morituri/Sufjan Stevens - Illinois/02. Sufjan Stevens - '
|
||||
'The Black Hawk War, or, How to Demolish an Entire '
|
||||
'Civilization and Still Feel Good About Yourself in the '
|
||||
'Morning, or, We Apologize for the Inconvenience but '
|
||||
'You\'re Going to Have to Leave Now, or, "I Have Fought '
|
||||
'the Big Knives and Will Continue to Fight Them Until They '
|
||||
'Are Off Our Lands!".flac')
|
||||
|
||||
shorter = common.shrinkPath(path)
|
||||
self.failUnless(os.path.splitext(path)[0].startswith(
|
||||
os.path.splitext(shorter)[0]))
|
||||
self.failIfEquals(path, shorter)
|
||||
|
||||
|
||||
class FramesTestCase(tcommon.TestCase):
|
||||
|
||||
def testFrames(self):
|
||||
self.assertEquals(common.framesToHMSF(123456), '00:27:26.06')
|
||||
|
||||
|
||||
class FormatTimeTestCase(tcommon.TestCase):
|
||||
|
||||
def testFormatTime(self):
|
||||
self.assertEquals(common.formatTime(7202), '02:00:02.000')
|
||||
|
||||
|
||||
class GetRelativePathTestCase(tcommon.TestCase):
|
||||
|
||||
def testRelativeOutputDirectory(self):
|
||||
directory = '.Placebo - Black Market Music (2000)'
|
||||
cue = './' + directory + '/Placebo - Black Market Music (2000)'
|
||||
track = './' + directory + '/01. Placebo - Taste in Men.flac'
|
||||
|
||||
self.assertEquals(common.getRelativePath(track, cue),
|
||||
'01. Placebo - Taste in Men.flac')
|
||||
|
||||
|
||||
class GetRealPathTestCase(tcommon.TestCase):
|
||||
|
||||
def testRealWithBackslash(self):
|
||||
fd, path = tempfile.mkstemp(suffix=u'back\\slash.flac')
|
||||
refPath = os.path.join(os.path.dirname(path), 'fake.cue')
|
||||
|
||||
self.assertEquals(common.getRealPath(refPath, path),
|
||||
path)
|
||||
|
||||
# same path, but with wav extension, will point to flac file
|
||||
wavPath = path[:-4] + 'wav'
|
||||
self.assertEquals(common.getRealPath(refPath, wavPath),
|
||||
path)
|
||||
|
||||
os.close(fd)
|
||||
os.unlink(path)
|
||||
68
whipper/test/test_common_config.py
Normal file
68
whipper/test/test_common_config.py
Normal file
@@ -0,0 +1,68 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_config -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from whipper.common import config
|
||||
|
||||
from whipper.test import common as tcommon
|
||||
|
||||
|
||||
class ConfigTestCase(tcommon.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
fd, self._path = tempfile.mkstemp(suffix=u'.morituri.test.config')
|
||||
os.close(fd)
|
||||
self._config = config.Config(self._path)
|
||||
|
||||
def tearDown(self):
|
||||
os.unlink(self._path)
|
||||
|
||||
def testAddReadOffset(self):
|
||||
self.assertRaises(KeyError,
|
||||
self._config.getReadOffset, 'PLEXTOR ', 'DVDR PX-L890SA', '1.05')
|
||||
self._config.setReadOffset('PLEXTOR ', 'DVDR PX-L890SA', '1.05', 6)
|
||||
|
||||
# getting it from memory should work
|
||||
offset = self._config.getReadOffset('PLEXTOR ', 'DVDR PX-L890SA',
|
||||
'1.05')
|
||||
self.assertEquals(offset, 6)
|
||||
|
||||
# and so should getting it after reading it again
|
||||
self._config.open()
|
||||
offset = self._config.getReadOffset('PLEXTOR ', 'DVDR PX-L890SA',
|
||||
'1.05')
|
||||
self.assertEquals(offset, 6)
|
||||
|
||||
def testAddReadOffsetSpaced(self):
|
||||
self.assertRaises(KeyError,
|
||||
self._config.getReadOffset, 'Slimtype', 'eSAU208 2 ', 'ML03')
|
||||
self._config.setReadOffset('Slimtype', 'eSAU208 2 ', 'ML03', 6)
|
||||
|
||||
# getting it from memory should work
|
||||
offset = self._config.getReadOffset(
|
||||
'Slimtype', 'eSAU208 2 ', 'ML03')
|
||||
self.assertEquals(offset, 6)
|
||||
|
||||
# and so should getting it after reading it again
|
||||
self._config.open()
|
||||
offset = self._config.getReadOffset(
|
||||
'Slimtype', 'eSAU208 2 ', 'ML03')
|
||||
self.assertEquals(offset, 6)
|
||||
|
||||
def testDefeatsCache(self):
|
||||
self.assertRaises(KeyError, self._config.getDefeatsCache,
|
||||
'PLEXTOR ', 'DVDR PX-L890SA', '1.05')
|
||||
|
||||
self._config.setDefeatsCache(
|
||||
'PLEXTOR ', 'DVDR PX-L890SA', '1.05', False)
|
||||
defeats = self._config.getDefeatsCache(
|
||||
'PLEXTOR ', 'DVDR PX-L890SA', '1.05')
|
||||
self.assertEquals(defeats, False)
|
||||
|
||||
self._config.setDefeatsCache(
|
||||
'PLEXTOR ', 'DVDR PX-L890SA', '1.05', True)
|
||||
defeats = self._config.getDefeatsCache(
|
||||
'PLEXTOR ', 'DVDR PX-L890SA', '1.05')
|
||||
self.assertEquals(defeats, True)
|
||||
16
whipper/test/test_common_directory.py
Normal file
16
whipper/test/test_common_directory.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_directory -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
from whipper.common import directory
|
||||
|
||||
from whipper.test import common
|
||||
|
||||
|
||||
class DirectoryTestCase(common.TestCase):
|
||||
|
||||
def testAll(self):
|
||||
path = directory.config_path()
|
||||
self.failUnless(path.startswith('/home'))
|
||||
|
||||
path = directory.cache_path()
|
||||
self.failUnless(path.startswith('/home'))
|
||||
16
whipper/test/test_common_drive.py
Normal file
16
whipper/test/test_common_drive.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_drive -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
from whipper.test import common
|
||||
from whipper.common import drive
|
||||
|
||||
|
||||
class ListifyTestCase(common.TestCase):
|
||||
|
||||
def testString(self):
|
||||
string = '/dev/sr0'
|
||||
self.assertEquals(drive._listify(string), [string, ])
|
||||
|
||||
def testList(self):
|
||||
lst = ['/dev/scd0', '/dev/sr0']
|
||||
self.assertEquals(drive._listify(lst), lst)
|
||||
118
whipper/test/test_common_mbngs.py
Normal file
118
whipper/test/test_common_mbngs.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_mbngs -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
import json
|
||||
|
||||
import unittest
|
||||
|
||||
from whipper.common import mbngs
|
||||
|
||||
|
||||
class MetadataTestCase(unittest.TestCase):
|
||||
|
||||
# Generated with rip -R cd info
|
||||
def testJeffEverybodySingle(self):
|
||||
path = os.path.join(os.path.dirname(__file__),
|
||||
'morituri.release.3451f29c-9bb8-4cc5-bfcc-bd50104b94f8.json')
|
||||
handle = open(path, "rb")
|
||||
response = json.loads(handle.read())
|
||||
handle.close()
|
||||
discid = "wbjbST2jUHRZaB1inCyxxsL7Eqc-"
|
||||
|
||||
metadata = mbngs._getMetadata({}, response['release'], discid)
|
||||
|
||||
self.failIf(metadata.release)
|
||||
|
||||
def test2MeterSessies10(self):
|
||||
# various artists, multiple artists per track
|
||||
path = os.path.join(os.path.dirname(__file__),
|
||||
'morituri.release.a76714e0-32b1-4ed4-b28e-f86d99642193.json')
|
||||
handle = open(path, "rb")
|
||||
response = json.loads(handle.read())
|
||||
handle.close()
|
||||
discid = "f7XO36a7n1LCCskkCiulReWbwZA-"
|
||||
|
||||
metadata = mbngs._getMetadata({}, response['release'], discid)
|
||||
|
||||
self.assertEquals(metadata.artist, u'Various Artists')
|
||||
self.assertEquals(metadata.release, u'2001-10-15')
|
||||
self.assertEquals(metadata.mbidArtist,
|
||||
u'89ad4ac3-39f7-470e-963a-56509c546377')
|
||||
|
||||
self.assertEquals(len(metadata.tracks), 18)
|
||||
|
||||
track16 = metadata.tracks[15]
|
||||
|
||||
self.assertEquals(track16.artist, 'Tom Jones & Stereophonics')
|
||||
self.assertEquals(track16.mbidArtist,
|
||||
u'57c6f649-6cde-48a7-8114-2a200247601a'
|
||||
';0bfba3d3-6a04-4779-bb0a-df07df5b0558'
|
||||
)
|
||||
self.assertEquals(track16.sortName,
|
||||
u'Jones, Tom & Stereophonics')
|
||||
|
||||
def testBalladOfTheBrokenSeas(self):
|
||||
# various artists disc
|
||||
path = os.path.join(os.path.dirname(__file__),
|
||||
'morituri.release.e32ae79a-336e-4d33-945c-8c5e8206dbd3.json')
|
||||
handle = open(path, "rb")
|
||||
response = json.loads(handle.read())
|
||||
handle.close()
|
||||
discid = "xAq8L4ELMW14.6wI6tt7QAcxiDI-"
|
||||
|
||||
metadata = mbngs._getMetadata({}, response['release'], discid)
|
||||
|
||||
self.assertEquals(metadata.artist, u'Isobel Campbell & Mark Lanegan')
|
||||
self.assertEquals(metadata.sortName,
|
||||
u'Campbell, Isobel & Lanegan, Mark')
|
||||
self.assertEquals(metadata.release, u'2006-01-30')
|
||||
self.assertEquals(metadata.mbidArtist,
|
||||
u'd51f3a15-12a2-41a0-acfa-33b5eae71164;'
|
||||
'a9126556-f555-4920-9617-6e013f8228a7')
|
||||
|
||||
self.assertEquals(len(metadata.tracks), 12)
|
||||
|
||||
track12 = metadata.tracks[11]
|
||||
|
||||
self.assertEquals(track12.artist, u'Isobel Campbell & Mark Lanegan')
|
||||
self.assertEquals(track12.sortName,
|
||||
u'Campbell, Isobel'
|
||||
' & Lanegan, Mark'
|
||||
)
|
||||
self.assertEquals(track12.mbidArtist,
|
||||
u'd51f3a15-12a2-41a0-acfa-33b5eae71164;'
|
||||
'a9126556-f555-4920-9617-6e013f8228a7')
|
||||
|
||||
def testMalaInCuba(self):
|
||||
# single artist disc, but with multiple artists tracks
|
||||
# see https://github.com/thomasvs/morituri/issues/19
|
||||
path = os.path.join(os.path.dirname(__file__),
|
||||
'morituri.release.61c6fd9b-18f8-4a45-963a-ba3c5d990cae.json')
|
||||
handle = open(path, "rb")
|
||||
response = json.loads(handle.read())
|
||||
handle.close()
|
||||
discid = "u0aKVpO.59JBy6eQRX2vYcoqQZ0-"
|
||||
|
||||
metadata = mbngs._getMetadata({}, response['release'], discid)
|
||||
|
||||
self.assertEquals(metadata.artist, u'Mala')
|
||||
self.assertEquals(metadata.sortName, u'Mala')
|
||||
self.assertEquals(metadata.release, u'2012-09-17')
|
||||
self.assertEquals(metadata.mbidArtist,
|
||||
u'09f221eb-c97e-4da5-ac22-d7ab7c555bbb')
|
||||
|
||||
self.assertEquals(len(metadata.tracks), 14)
|
||||
|
||||
track6 = metadata.tracks[5]
|
||||
|
||||
self.assertEquals(track6.artist, u'Mala feat. Dreiser & Sexto Sentido')
|
||||
self.assertEquals(track6.sortName,
|
||||
u'Mala feat. Dreiser & Sexto Sentido')
|
||||
self.assertEquals(track6.mbidArtist,
|
||||
u'09f221eb-c97e-4da5-ac22-d7ab7c555bbb'
|
||||
';ec07a209-55ff-4084-bc41-9d4d1764e075'
|
||||
';f626b92e-07b1-4a19-ad13-c09d690db66c'
|
||||
)
|
||||
|
||||
|
||||
30
whipper/test/test_common_path.py
Normal file
30
whipper/test/test_common_path.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_path -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
from whipper.common import path
|
||||
|
||||
from whipper.test import common
|
||||
|
||||
|
||||
class FilterTestCase(common.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self._filter = path.PathFilter(special=True)
|
||||
|
||||
def testSlash(self):
|
||||
part = u'A Charm/A Blade'
|
||||
self.assertEquals(self._filter.filter(part), u'A Charm-A Blade')
|
||||
|
||||
def testFat(self):
|
||||
part = u'A Word: F**k you?'
|
||||
self.assertEquals(self._filter.filter(part), u'A Word - F__k you_')
|
||||
|
||||
def testSpecial(self):
|
||||
part = u'<<< $&*!\' "()`{}[]spaceship>>>'
|
||||
self.assertEquals(self._filter.filter(part),
|
||||
u'___ _____ ________spaceship___')
|
||||
|
||||
def testGreatest(self):
|
||||
part = u'Greatest Ever! Soul: The Definitive Collection'
|
||||
self.assertEquals(self._filter.filter(part),
|
||||
u'Greatest Ever_ Soul - The Definitive Collection')
|
||||
118
whipper/test/test_common_program.py
Normal file
118
whipper/test/test_common_program.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_common_program -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
|
||||
import os
|
||||
import pickle
|
||||
|
||||
import unittest
|
||||
|
||||
from whipper.result import result
|
||||
from whipper.common import program, accurip, mbngs, config
|
||||
from whipper.command.cd import DEFAULT_DISC_TEMPLATE
|
||||
|
||||
|
||||
class TrackImageVerifyTestCase(unittest.TestCase):
|
||||
# example taken from a rip of Luke Haines Is Dead, disc 1
|
||||
# AccurateRip database has 0 confidence for 1st track
|
||||
# Rip had a wrong result for track 9
|
||||
|
||||
def testVerify(self):
|
||||
path = os.path.join(os.path.dirname(__file__),
|
||||
'dBAR-020-002e5023-029d8e49-040eaa14.bin')
|
||||
data = open(path, "rb").read()
|
||||
responses = accurip.getAccurateRipResponses(data)
|
||||
|
||||
# these crc's were calculated from an actual rip
|
||||
checksums = [1644890007, 2945205445, 3983436658, 1528082495,
|
||||
1203704270, 1163423644, 3649097244, 100524219, 1583356174, 373652058,
|
||||
1842579359, 2850056507, 1329730252, 2526965856, 2525886806, 209743350,
|
||||
3184062337, 2099956663, 2943874164, 2321637196]
|
||||
|
||||
prog = program.Program(config.Config())
|
||||
prog.result = result.RipResult()
|
||||
# fill it with empty trackresults
|
||||
for i, c in enumerate(checksums):
|
||||
r = result.TrackResult()
|
||||
r.number = i + 1
|
||||
prog.result.tracks.append(r)
|
||||
|
||||
prog._verifyImageWithChecksums(responses, checksums)
|
||||
|
||||
# now check if the results were filled in properly
|
||||
tr = prog.result.getTrackResult(1)
|
||||
self.assertEquals(tr.accurip, False)
|
||||
self.assertEquals(tr.ARDBMaxConfidence, 0)
|
||||
self.assertEquals(tr.ARDBCRC, 0)
|
||||
self.assertEquals(tr.ARDBCRC, 0)
|
||||
|
||||
tr = prog.result.getTrackResult(2)
|
||||
self.assertEquals(tr.accurip, True)
|
||||
self.assertEquals(tr.ARDBMaxConfidence, 2)
|
||||
self.assertEquals(tr.ARDBCRC, checksums[2 - 1])
|
||||
|
||||
tr = prog.result.getTrackResult(10)
|
||||
self.assertEquals(tr.accurip, False)
|
||||
self.assertEquals(tr.ARDBMaxConfidence, 2)
|
||||
# we know track 10 was ripped wrong
|
||||
self.assertNotEquals(tr.ARDBCRC, checksums[10 - 1])
|
||||
|
||||
res = prog.getAccurateRipResults()
|
||||
self.assertEquals(res[1 - 1],
|
||||
"Track 1: rip NOT accurate (not found) "
|
||||
"[620b0797], DB [notfound]")
|
||||
self.assertEquals(res[2 - 1],
|
||||
"Track 2: rip accurate (max confidence 2) "
|
||||
"[af8c44c5], DB [af8c44c5]")
|
||||
self.assertEquals(res[10 - 1],
|
||||
"Track 10: rip NOT accurate (max confidence 2) "
|
||||
"[16457a5a], DB [eb6e55b4]")
|
||||
|
||||
|
||||
class HTOATestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
path = os.path.join(os.path.dirname(__file__),
|
||||
'silentalarm.result.pickle')
|
||||
self._tracks = pickle.load(open(path, 'rb'))
|
||||
|
||||
def testGetAccurateRipResults(self):
|
||||
prog = program.Program(config.Config())
|
||||
prog.result = result.RipResult()
|
||||
prog.result.tracks = self._tracks
|
||||
|
||||
prog.getAccurateRipResults()
|
||||
|
||||
|
||||
class PathTestCase(unittest.TestCase):
|
||||
|
||||
def testStandardTemplateEmpty(self):
|
||||
prog = program.Program(config.Config())
|
||||
|
||||
path = prog.getPath(u'/tmp', DEFAULT_DISC_TEMPLATE,
|
||||
'mbdiscid', 0)
|
||||
self.assertEquals(path,
|
||||
u'/tmp/unknown/Unknown Artist - mbdiscid/Unknown Artist - mbdiscid')
|
||||
|
||||
def testStandardTemplateFilled(self):
|
||||
prog = program.Program(config.Config())
|
||||
md = mbngs.DiscMetadata()
|
||||
md.artist = md.sortName = 'Jeff Buckley'
|
||||
md.title = 'Grace'
|
||||
prog.metadata = md
|
||||
|
||||
path = prog.getPath(u'/tmp', DEFAULT_DISC_TEMPLATE,
|
||||
'mbdiscid', 0)
|
||||
self.assertEquals(path,
|
||||
u'/tmp/unknown/Jeff Buckley - Grace/Jeff Buckley - Grace')
|
||||
|
||||
def testIssue66TemplateFilled(self):
|
||||
prog = program.Program(config.Config())
|
||||
md = mbngs.DiscMetadata()
|
||||
md.artist = md.sortName = 'Jeff Buckley'
|
||||
md.title = 'Grace'
|
||||
prog.metadata = md
|
||||
|
||||
path = prog.getPath(u'/tmp', u'%A/%d', 'mbdiscid', 0)
|
||||
self.assertEquals(path,
|
||||
u'/tmp/Jeff Buckley/Grace')
|
||||
154
whipper/test/test_common_renamer.py
Normal file
154
whipper/test/test_common_renamer.py
Normal file
@@ -0,0 +1,154 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_image_cue -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import unittest
|
||||
|
||||
from whipper.common import renamer
|
||||
|
||||
|
||||
class RenameInFileTestcase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
(fd, self._path) = tempfile.mkstemp(suffix='.morituri.renamer.infile')
|
||||
os.write(fd, 'This is a test\nThis is another\n')
|
||||
os.close(fd)
|
||||
|
||||
def testVerify(self):
|
||||
o = renamer.RenameInFile(self._path, 'is is a', 'at was some')
|
||||
self.assertEquals(o.verify(), None)
|
||||
os.unlink(self._path)
|
||||
self.assertRaises(AssertionError, o.verify)
|
||||
|
||||
def testDo(self):
|
||||
o = renamer.RenameInFile(self._path, 'is is a', 'at was some')
|
||||
o.do()
|
||||
output = open(self._path).read()
|
||||
self.assertEquals(output, 'That was some test\nThat was somenother\n')
|
||||
os.unlink(self._path)
|
||||
|
||||
def testSerialize(self):
|
||||
o = renamer.RenameInFile(self._path, 'is is a', 'at was some')
|
||||
data = o.serialize()
|
||||
o2 = renamer.RenameInFile.deserialize(data)
|
||||
o2.do()
|
||||
output = open(self._path).read()
|
||||
self.assertEquals(output, 'That was some test\nThat was somenother\n')
|
||||
os.unlink(self._path)
|
||||
|
||||
|
||||
class RenameFileTestcase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
(fd, self._source) = tempfile.mkstemp(suffix='.morituri.renamer.file')
|
||||
os.write(fd, 'This is a test\nThis is another\n')
|
||||
os.close(fd)
|
||||
(fd, self._destination) = tempfile.mkstemp(
|
||||
suffix='.morituri.renamer.file')
|
||||
os.close(fd)
|
||||
os.unlink(self._destination)
|
||||
self._operation = renamer.RenameFile(self._source, self._destination)
|
||||
|
||||
def testVerify(self):
|
||||
self.assertEquals(self._operation.verify(), None)
|
||||
|
||||
handle = open(self._destination, 'w')
|
||||
handle.close()
|
||||
self.assertRaises(AssertionError, self._operation.verify)
|
||||
|
||||
os.unlink(self._destination)
|
||||
self.assertEquals(self._operation.verify(), None)
|
||||
|
||||
os.unlink(self._source)
|
||||
self.assertRaises(AssertionError, self._operation.verify)
|
||||
|
||||
def testDo(self):
|
||||
self._operation.do()
|
||||
output = open(self._destination).read()
|
||||
self.assertEquals(output, 'This is a test\nThis is another\n')
|
||||
os.unlink(self._destination)
|
||||
|
||||
def testSerialize(self):
|
||||
data = self._operation.serialize()
|
||||
o = renamer.RenameFile.deserialize(data)
|
||||
o.do()
|
||||
output = open(self._destination).read()
|
||||
self.assertEquals(output, 'This is a test\nThis is another\n')
|
||||
os.unlink(self._destination)
|
||||
|
||||
|
||||
class OperatorTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self._statePath = tempfile.mkdtemp(suffix='.morituri.renamer.operator')
|
||||
self._operator = renamer.Operator(self._statePath, 'test')
|
||||
|
||||
(fd, self._source) = tempfile.mkstemp(
|
||||
suffix='.morituri.renamer.operator')
|
||||
os.write(fd, 'This is a test\nThis is another\n')
|
||||
os.close(fd)
|
||||
(fd, self._destination) = tempfile.mkstemp(
|
||||
suffix='.morituri.renamer.operator')
|
||||
os.close(fd)
|
||||
os.unlink(self._destination)
|
||||
self._operator.addOperation(
|
||||
renamer.RenameInFile(self._source, 'is is a', 'at was some'))
|
||||
self._operator.addOperation(
|
||||
renamer.RenameFile(self._source, self._destination))
|
||||
|
||||
def tearDown(self):
|
||||
os.system('rm -rf %s' % self._statePath)
|
||||
|
||||
def testLoadNoneDone(self):
|
||||
self._operator.save()
|
||||
|
||||
o = renamer.Operator(self._statePath, 'test')
|
||||
o.load()
|
||||
|
||||
self.assertEquals(o._todo, self._operator._todo)
|
||||
self.assertEquals(o._done, [])
|
||||
os.unlink(self._source)
|
||||
|
||||
def testLoadOneDone(self):
|
||||
self.assertEquals(len(self._operator._done), 0)
|
||||
self._operator.save()
|
||||
self._operator.next()
|
||||
self.assertEquals(len(self._operator._done), 1)
|
||||
|
||||
o = renamer.Operator(self._statePath, 'test')
|
||||
o.load()
|
||||
|
||||
self.assertEquals(len(o._done), 1)
|
||||
self.assertEquals(o._todo, self._operator._todo)
|
||||
self.assertEquals(o._done, self._operator._done)
|
||||
|
||||
# now continue
|
||||
o.next()
|
||||
self.assertEquals(len(o._done), 2)
|
||||
os.unlink(self._destination)
|
||||
|
||||
def testLoadOneInterrupted(self):
|
||||
self.assertEquals(len(self._operator._done), 0)
|
||||
self._operator.save()
|
||||
|
||||
# cheat by doing a task without saving
|
||||
self._operator._todo[0].do()
|
||||
|
||||
self.assertEquals(len(self._operator._done), 0)
|
||||
|
||||
o = renamer.Operator(self._statePath, 'test')
|
||||
o.load()
|
||||
|
||||
self.assertEquals(len(o._done), 0)
|
||||
self.assertEquals(o._todo, self._operator._todo)
|
||||
self.assertEquals(o._done, self._operator._done)
|
||||
|
||||
# now continue, resuming
|
||||
o.next()
|
||||
self.assertEquals(len(o._done), 1)
|
||||
o.next()
|
||||
self.assertEquals(len(o._done), 2)
|
||||
|
||||
os.unlink(self._destination)
|
||||
89
whipper/test/test_image_cue.py
Normal file
89
whipper/test/test_image_cue.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_image_cue -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import whipper
|
||||
|
||||
from whipper.image import table, cue
|
||||
|
||||
from whipper.test import common
|
||||
|
||||
class KingsSingleTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.cue = cue.CueFile(os.path.join(os.path.dirname(__file__),
|
||||
u'kings-single.cue'))
|
||||
self.cue.parse()
|
||||
self.assertEquals(len(self.cue.table.tracks), 11)
|
||||
|
||||
def testGetTrackLength(self):
|
||||
t = self.cue.table.tracks[0]
|
||||
self.assertEquals(self.cue.getTrackLength(t), 17811)
|
||||
# last track has unknown length
|
||||
t = self.cue.table.tracks[-1]
|
||||
self.assertEquals(self.cue.getTrackLength(t), -1)
|
||||
|
||||
|
||||
class KingsSeparateTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.cue = cue.CueFile(os.path.join(os.path.dirname(__file__),
|
||||
u'kings-separate.cue'))
|
||||
self.cue.parse()
|
||||
self.assertEquals(len(self.cue.table.tracks), 11)
|
||||
|
||||
def testGetTrackLength(self):
|
||||
# all tracks have unknown length
|
||||
t = self.cue.table.tracks[0]
|
||||
self.assertEquals(self.cue.getTrackLength(t), -1)
|
||||
t = self.cue.table.tracks[-1]
|
||||
self.assertEquals(self.cue.getTrackLength(t), -1)
|
||||
|
||||
|
||||
class KanyeMixedTestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.cue = cue.CueFile(os.path.join(os.path.dirname(__file__),
|
||||
u'kanye.cue'))
|
||||
self.cue.parse()
|
||||
self.assertEquals(len(self.cue.table.tracks), 13)
|
||||
|
||||
def testGetTrackLength(self):
|
||||
t = self.cue.table.tracks[0]
|
||||
self.assertEquals(self.cue.getTrackLength(t), -1)
|
||||
|
||||
|
||||
class WriteCueFileTestCase(unittest.TestCase):
|
||||
|
||||
def testWrite(self):
|
||||
fd, path = tempfile.mkstemp(suffix=u'.morituri.test.cue')
|
||||
os.close(fd)
|
||||
|
||||
it = table.Table()
|
||||
|
||||
t = table.Track(1)
|
||||
t.index(1, absolute=0, path=u'track01.wav', relative=0, counter=1)
|
||||
it.tracks.append(t)
|
||||
|
||||
t = table.Track(2)
|
||||
t.index(0, absolute=1000, path=u'track01.wav',
|
||||
relative=1000, counter=1)
|
||||
t.index(1, absolute=2000, path=u'track02.wav', relative=0, counter=2)
|
||||
it.tracks.append(t)
|
||||
it.absolutize()
|
||||
it.leadout = 3000
|
||||
|
||||
common.diffStrings(u"""REM DISCID 0C002802
|
||||
REM COMMENT "whipper %s"
|
||||
FILE "track01.wav" WAVE
|
||||
TRACK 01 AUDIO
|
||||
INDEX 01 00:00:00
|
||||
TRACK 02 AUDIO
|
||||
INDEX 00 00:13:25
|
||||
FILE "track02.wav" WAVE
|
||||
INDEX 01 00:00:00
|
||||
""" % whipper.__version__, it.cue())
|
||||
os.unlink(path)
|
||||
117
whipper/test/test_image_table.py
Normal file
117
whipper/test/test_image_table.py
Normal file
@@ -0,0 +1,117 @@
|
||||
# -*- Mode: Python; test-case-name: whipper.test.test_image_table -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
from whipper.image import table
|
||||
|
||||
from whipper.test import common as tcommon
|
||||
|
||||
|
||||
def h(i):
|
||||
return "0x%08x" % i
|
||||
|
||||
|
||||
class TrackTestCase(tcommon.TestCase):
|
||||
|
||||
def testRepr(self):
|
||||
track = table.Track(1)
|
||||
self.assertEquals(repr(track), "<Track 01>")
|
||||
|
||||
track.index(1, 100)
|
||||
self.failUnless(repr(track.indexes[1]).startswith('<Index 01 '))
|
||||
|
||||
|
||||
class LadyhawkeTestCase(tcommon.TestCase):
|
||||
# Ladyhawke - Ladyhawke - 0602517818866
|
||||
# contains 12 audio tracks and one data track
|
||||
# CDDB has been verified against freedb:
|
||||
# http://www.freedb.org/freedb/misc/c60af50d
|
||||
# http://www.freedb.org/freedb/jazz/c60af50d
|
||||
# AccurateRip URL has been verified against EAC's, using wireshark
|
||||
|
||||
def setUp(self):
|
||||
self.table = table.Table()
|
||||
|
||||
for i in range(12):
|
||||
self.table.tracks.append(table.Track(i + 1, audio=True))
|
||||
self.table.tracks.append(table.Track(13, audio=False))
|
||||
|
||||
offsets = [0, 15537, 31691, 50866, 66466, 81202, 99409,
|
||||
115920, 133093, 149847, 161560, 177682, 207106]
|
||||
t = self.table.tracks
|
||||
for i, offset in enumerate(offsets):
|
||||
t[i].index(1, absolute=offset)
|
||||
|
||||
self.failIf(self.table.hasTOC())
|
||||
|
||||
self.table.leadout = 210385
|
||||
|
||||
self.failUnless(self.table.hasTOC())
|
||||
self.assertEquals(self.table.tracks[0].getPregap(), 0)
|
||||
|
||||
def testCDDB(self):
|
||||
self.assertEquals(self.table.getCDDBDiscId(), "c60af50d")
|
||||
|
||||
def testMusicBrainz(self):
|
||||
# output from mb-submit-disc:
|
||||
# https://musicbrainz.org/cdtoc/attach?toc=1+12+195856+150+
|
||||
# 15687+31841+51016+66616+81352+99559+116070+133243+149997+161710+
|
||||
# 177832&tracks=12&id=KnpGsLhvH.lPrNc1PBL21lb9Bg4-
|
||||
# however, not (yet) in musicbrainz database
|
||||
|
||||
self.assertEquals(self.table.getMusicBrainzDiscId(),
|
||||
"KnpGsLhvH.lPrNc1PBL21lb9Bg4-")
|
||||
|
||||
def testAccurateRip(self):
|
||||
self.assertEquals(self.table.getAccurateRipIds(), (
|
||||
"0013bd5a", "00b8d489"))
|
||||
self.assertEquals(self.table.getAccurateRipURL(),
|
||||
"http://www.accuraterip.com/accuraterip/a/5/d/"
|
||||
"dBAR-012-0013bd5a-00b8d489-c60af50d.bin")
|
||||
|
||||
def testDuration(self):
|
||||
self.assertEquals(self.table.duration(), 2761413)
|
||||
|
||||
|
||||
class MusicBrainzTestCase(tcommon.TestCase):
|
||||
# example taken from https://musicbrainz.org/doc/Disc_ID_Calculation
|
||||
# disc is Ettella Diamant
|
||||
|
||||
def setUp(self):
|
||||
self.table = table.Table()
|
||||
|
||||
for i in range(6):
|
||||
self.table.tracks.append(table.Track(i + 1, audio=True))
|
||||
|
||||
offsets = [0, 15213, 32164, 46442, 63264, 80339]
|
||||
t = self.table.tracks
|
||||
for i, offset in enumerate(offsets):
|
||||
t[i].index(1, absolute=offset)
|
||||
|
||||
self.failIf(self.table.hasTOC())
|
||||
|
||||
self.table.leadout = 95312
|
||||
|
||||
self.failUnless(self.table.hasTOC())
|
||||
|
||||
def testMusicBrainz(self):
|
||||
self.assertEquals(self.table.getMusicBrainzDiscId(),
|
||||
'49HHV7Eb8UKF3aQiNmu1GR8vKTY-')
|
||||
|
||||
|
||||
class PregapTestCase(tcommon.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.table = table.Table()
|
||||
|
||||
for i in range(2):
|
||||
self.table.tracks.append(table.Track(i + 1, audio=True))
|
||||
|
||||
offsets = [0, 15537]
|
||||
t = self.table.tracks
|
||||
for i, offset in enumerate(offsets):
|
||||
t[i].index(1, absolute=offset)
|
||||
t[1].index(0, offsets[1] - 200)
|
||||
|
||||
def testPreGap(self):
|
||||
self.assertEquals(self.table.tracks[0].getPregap(), 0)
|
||||
self.assertEquals(self.table.tracks[1].getPregap(), 200)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user