Rename "morituri" module to "whipper".

Fixes https://github.com/JoeLametta/whipper/issues/100
This commit is contained in:
Frederik “Freso” S. Olesen
2017-04-26 16:51:11 +02:00
parent a8af9b79ab
commit ff309e468c
114 changed files with 198 additions and 198 deletions

14
whipper/__init__.py Normal file
View 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)

View File

102
whipper/command/accurip.py Normal file
View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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,
}

View File

147
whipper/common/accurip.py Normal file
View 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
View 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

View 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
View 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
View 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)

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

174
whipper/extern/asyncsub.py vendored Normal file
View 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
View 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
View File

567
whipper/extern/task/task.py vendored Normal file
View 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)

View File

207
whipper/image/cue.py Normal file
View 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
View 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
View 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
View 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, )

View File

52
whipper/program/arc.py Normal file
View 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

View 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
View 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
View 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
View 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
View 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
View 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)

View File

224
whipper/result/logger.py Normal file
View 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
View 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
View File

38
whipper/test/bloc.cue Normal file
View 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
View 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
View 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
View 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

Binary file not shown.

113
whipper/test/capital.1.toc Normal file
View 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

View File

@@ -0,0 +1,8 @@
CD_ROM
// Track 1
TRACK MODE1
NO COPY
DATAFILE "data_1" 27:30:00 // length in bytes: 253440000

View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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.

View 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.

View 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

View 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.

View 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...

View 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
View 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
View 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
View 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

Binary file not shown.

Binary file not shown.

167
whipper/test/jose.toc Normal file
View 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
View 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

View 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

View 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
View 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

View File

@@ -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"}]}}

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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.

View 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

View 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

View 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

View 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

View 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

View 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

View 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
View 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

View 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")

View 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'])

View 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)

View 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)

View 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'))

View 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)

View 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'
)

View 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')

View 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')

View 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)

View 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)

View 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