Rip out all code that directly uses gstreamer
We can now rip CDs without gstreamer. This is not the most clean attempt, but I have tried to remove most of the code that depends on gstreamer. I hope there is not a lot of code left that depends on code that I have removed - I can at least rip a CD fully.
This commit is contained in:
@@ -31,8 +31,9 @@ import gobject
|
||||
gobject.threads_init()
|
||||
|
||||
from morituri.command.basecommand import BaseCommand
|
||||
from morituri.common import encode
|
||||
from morituri.common import (
|
||||
accurip, common, config, drive, gstreamer, program, task
|
||||
accurip, common, config, drive, program, task
|
||||
)
|
||||
from morituri.program import cdrdao, cdparanoia, utils
|
||||
from morituri.result import result
|
||||
@@ -317,17 +318,6 @@ Log files will log the path to tracks relative to this directory.
|
||||
|
||||
|
||||
def doCommand(self):
|
||||
# here to avoid import gst eating our options
|
||||
from morituri.common import encode
|
||||
profile = encode.PROFILES['flac']()
|
||||
self.program.result.profileName = profile.name
|
||||
self.program.result.profilePipeline = profile.pipeline
|
||||
elementFactory = profile.pipeline.split(' ')[0]
|
||||
self.program.result.gstreamerVersion = gstreamer.gstreamerVersion()
|
||||
self.program.result.gstPythonVersion = gstreamer.gstPythonVersion()
|
||||
self.program.result.encoderVersion = gstreamer.elementFactoryVersion(
|
||||
elementFactory)
|
||||
|
||||
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)
|
||||
@@ -339,7 +329,7 @@ Log files will log the path to tracks relative to this directory.
|
||||
while True:
|
||||
discName = self.program.getPath(self.program.outdir,
|
||||
self.options.disc_template, self.mbdiscid, 0,
|
||||
profile=profile, disambiguate=disambiguate)
|
||||
disambiguate=disambiguate)
|
||||
dirname = os.path.dirname(discName)
|
||||
if os.path.exists(dirname):
|
||||
sys.stdout.write("Output directory %s already exists\n" %
|
||||
@@ -382,8 +372,8 @@ Log files will log the path to tracks relative to this directory.
|
||||
path = self.program.getPath(self.program.outdir,
|
||||
self.options.track_template,
|
||||
self.mbdiscid, number,
|
||||
profile=profile, disambiguate=disambiguate) \
|
||||
+ '.' + profile.extension
|
||||
disambiguate=disambiguate) \
|
||||
+ '.' + 'flac'
|
||||
logger.debug('ripIfNotRipped: path %r' % path)
|
||||
trackResult.number = number
|
||||
|
||||
@@ -429,7 +419,6 @@ Log files will log the path to tracks relative to this directory.
|
||||
self.program.ripTrack(self.runner, trackResult,
|
||||
offset=int(self.options.offset),
|
||||
device=self.device,
|
||||
profile=profile,
|
||||
taglist=self.program.getTagList(number),
|
||||
overread=self.options.overread,
|
||||
what='track %d of %d%s' % (
|
||||
@@ -509,7 +498,7 @@ Log files will log the path to tracks relative to this directory.
|
||||
### write disc files
|
||||
discName = self.program.getPath(self.program.outdir,
|
||||
self.options.disc_template, self.mbdiscid, 0,
|
||||
profile=profile, disambiguate=disambiguate)
|
||||
disambiguate=disambiguate)
|
||||
dirname = os.path.dirname(discName)
|
||||
if not os.path.exists(dirname):
|
||||
os.makedirs(dirname)
|
||||
@@ -541,8 +530,7 @@ Log files will log the path to tracks relative to this directory.
|
||||
|
||||
path = self.program.getPath(self.program.outdir,
|
||||
self.options.track_template, self.mbdiscid, i + 1,
|
||||
profile=profile,
|
||||
disambiguate=disambiguate) + '.' + profile.extension
|
||||
disambiguate=disambiguate) + '.' + 'flac'
|
||||
writeFile(handle, path,
|
||||
self.itable.getTrackLength(i + 1) / common.FRAMES_PER_SECOND)
|
||||
|
||||
|
||||
@@ -195,31 +195,6 @@ class Encode(BaseCommand):
|
||||
sys.stdout.write('Encoded to %s\n' % toPath.encode('utf-8'))
|
||||
|
||||
|
||||
class MaxSample(BaseCommand):
|
||||
summary = "run a max sample task"
|
||||
description = summary
|
||||
|
||||
def add_arguments(self):
|
||||
self.parser.add_argument('files', nargs='+', action='store',
|
||||
help="audio files to sample")
|
||||
|
||||
def do(self):
|
||||
runner = task.SyncRunner()
|
||||
# here to avoid import gst eating our options
|
||||
from morituri.common import checksum
|
||||
|
||||
for arg in self.options.files:
|
||||
fromPath = unicode(arg.decode('utf-8'))
|
||||
|
||||
checksumtask = checksum.MaxSampleTask(fromPath)
|
||||
|
||||
runner.run(checksumtask)
|
||||
|
||||
sys.stdout.write('%s\n' % arg)
|
||||
sys.stdout.write('Biggest absolute sample: %04x\n' %
|
||||
checksumtask.checksum)
|
||||
|
||||
|
||||
class Tag(BaseCommand):
|
||||
summary = "run a tag reading task"
|
||||
description = summary
|
||||
@@ -325,7 +300,6 @@ class Debug(BaseCommand):
|
||||
subcommands = {
|
||||
'checksum': Checksum,
|
||||
'encode': Encode,
|
||||
'maxsample': MaxSample,
|
||||
'tag': Tag,
|
||||
'musicbrainzngs': MusicBrainzNGS,
|
||||
'resultcache': ResultCache,
|
||||
|
||||
@@ -25,6 +25,7 @@ import sys
|
||||
|
||||
from morituri.command.basecommand import BaseCommand
|
||||
from morituri.common import accurip, config, program
|
||||
from morituri.common import encode
|
||||
from morituri.extern.task import task
|
||||
from morituri.image import image
|
||||
from morituri.result import result
|
||||
@@ -59,8 +60,6 @@ Retags the image from the given .cue files with tags obtained from MusicBrainz.
|
||||
)
|
||||
|
||||
def do(self):
|
||||
# here to avoid import gst eating our options
|
||||
from morituri.common import encode
|
||||
|
||||
prog = program.Program(config.Config(), stdout=sys.stdout)
|
||||
runner = task.SyncRunner()
|
||||
|
||||
@@ -32,6 +32,7 @@ from morituri.command.basecommand import BaseCommand
|
||||
from morituri.common import accurip, common, config, drive, program
|
||||
from morituri.common import task as ctask
|
||||
from morituri.program import cdrdao, cdparanoia, utils
|
||||
from morituri.common import checksum
|
||||
|
||||
from morituri.extern.task import task
|
||||
|
||||
@@ -209,8 +210,6 @@ CD in the AccurateRip database."""
|
||||
track, offset)
|
||||
runner.run(t)
|
||||
|
||||
# here to avoid import gst eating our options
|
||||
from morituri.common import checksum
|
||||
|
||||
# TODO MW: Update this to also use the v2 checksum(s)
|
||||
t = checksum.FastAccurateRipChecksumTask(path, trackNumber=track,
|
||||
|
||||
@@ -26,12 +26,8 @@ import zlib
|
||||
import binascii
|
||||
import wave
|
||||
|
||||
import gst
|
||||
|
||||
from morituri.common import common, task
|
||||
from morituri.common import gstreamer as cgstreamer
|
||||
|
||||
from morituri.extern.task import gstreamer
|
||||
from morituri.extern.task import task as etask
|
||||
|
||||
from morituri.program.arc import accuraterip_checksum
|
||||
@@ -42,238 +38,6 @@ logger = logging.getLogger(__name__)
|
||||
# checksums are not CRC's. a CRC is a specific type of checksum.
|
||||
|
||||
|
||||
class ChecksumTask(gstreamer.GstPipelineTask):
|
||||
"""
|
||||
I am a task that calculates a checksum of the decoded audio data.
|
||||
|
||||
@ivar checksum: the resulting checksum
|
||||
"""
|
||||
|
||||
logCategory = 'ChecksumTask'
|
||||
|
||||
# this object needs a main loop to stop
|
||||
description = 'Calculating checksum'
|
||||
|
||||
def __init__(self, path, sampleStart=0, sampleLength=-1):
|
||||
"""
|
||||
A sample is considered a set of samples for each channel;
|
||||
ie 16 bit stereo is 4 bytes per sample.
|
||||
If sampleLength < 0 it is treated as 'unknown' and calculated.
|
||||
|
||||
@type path: unicode
|
||||
@type sampleStart: int
|
||||
@param sampleStart: the sample to start at
|
||||
"""
|
||||
|
||||
# sampleLength can be e.g. -588 when it is -1 * SAMPLES_PER_FRAME
|
||||
|
||||
assert type(path) is unicode, "%r is not unicode" % path
|
||||
|
||||
self.logName = "ChecksumTask 0x%x" % id(self)
|
||||
|
||||
# use repr/%r because path can be unicode
|
||||
if sampleLength < 0:
|
||||
logger.debug(
|
||||
'Creating checksum task on %r from sample %d until the end',
|
||||
path, sampleStart)
|
||||
else:
|
||||
logger.debug(
|
||||
'Creating checksum task on %r from sample %d for %d samples',
|
||||
path, sampleStart, sampleLength)
|
||||
|
||||
if not os.path.exists(path):
|
||||
raise IndexError('%r does not exist' % path)
|
||||
|
||||
self._path = path
|
||||
self._sampleStart = sampleStart
|
||||
self._sampleLength = sampleLength
|
||||
self._sampleEnd = None
|
||||
self._checksum = 0
|
||||
self._bytes = 0 # number of bytes received
|
||||
self._first = None
|
||||
self._last = None
|
||||
self._adapter = gst.Adapter()
|
||||
|
||||
self.checksum = None # result
|
||||
|
||||
cgstreamer.removeAudioParsers()
|
||||
|
||||
### gstreamer.GstPipelineTask implementations
|
||||
|
||||
def getPipelineDesc(self):
|
||||
return '''
|
||||
filesrc location="%s" !
|
||||
decodebin name=decode ! audio/x-raw-int !
|
||||
appsink name=sink sync=False emit-signals=True
|
||||
''' % gstreamer.quoteParse(self._path).encode('utf-8')
|
||||
|
||||
def _getSampleLength(self):
|
||||
# get length in samples of file
|
||||
sink = self.pipeline.get_by_name('sink')
|
||||
|
||||
logger.debug('query duration')
|
||||
try:
|
||||
length, qformat = sink.query_duration(gst.FORMAT_DEFAULT)
|
||||
except gst.QueryError, e:
|
||||
self.setException(e)
|
||||
return None
|
||||
|
||||
# wavparse 0.10.14 returns in bytes
|
||||
if qformat == gst.FORMAT_BYTES:
|
||||
logger.debug('query returned in BYTES format')
|
||||
length /= 4
|
||||
logger.debug('total sample length of file: %r', length)
|
||||
|
||||
return length
|
||||
|
||||
|
||||
def paused(self):
|
||||
sink = self.pipeline.get_by_name('sink')
|
||||
|
||||
length = self._getSampleLength()
|
||||
if length is None:
|
||||
return
|
||||
|
||||
if self._sampleLength < 0:
|
||||
self._sampleLength = length - self._sampleStart
|
||||
logger.debug('sampleLength is queried as %d samples',
|
||||
self._sampleLength)
|
||||
else:
|
||||
logger.debug('sampleLength is known, and is %d samples' %
|
||||
self._sampleLength)
|
||||
|
||||
self._sampleEnd = self._sampleStart + self._sampleLength - 1
|
||||
logger.debug('sampleEnd is sample %d' % self._sampleEnd)
|
||||
|
||||
logger.debug('event')
|
||||
|
||||
|
||||
if self._sampleStart == 0 and self._sampleEnd + 1 == length:
|
||||
logger.debug('No need to seek, crcing full file')
|
||||
else:
|
||||
# the segment end only is respected since -good 0.10.14.1
|
||||
event = gst.event_new_seek(1.0, gst.FORMAT_DEFAULT,
|
||||
gst.SEEK_FLAG_FLUSH,
|
||||
gst.SEEK_TYPE_SET, self._sampleStart,
|
||||
gst.SEEK_TYPE_SET, self._sampleEnd + 1) # half-inclusive
|
||||
logger.debug('CRCing %r from frame %d to frame %d (excluded)' % (
|
||||
self._path,
|
||||
self._sampleStart / common.SAMPLES_PER_FRAME,
|
||||
(self._sampleEnd + 1) / common.SAMPLES_PER_FRAME))
|
||||
# FIXME: sending it with sampleEnd set screws up the seek, we
|
||||
# don't get # everything for flac; fixed in recent -good
|
||||
result = sink.send_event(event)
|
||||
logger.debug('event sent, result %r', result)
|
||||
if not result:
|
||||
msg = 'Failed to select samples with GStreamer seek event'
|
||||
logger.critical(msg)
|
||||
raise Exception(msg)
|
||||
sink.connect('new-buffer', self._new_buffer_cb)
|
||||
sink.connect('eos', self._eos_cb)
|
||||
|
||||
logger.debug('scheduling setting to play')
|
||||
# since set_state returns non-False, adding it as timeout_add
|
||||
# will repeatedly call it, and block the main loop; so
|
||||
# gobject.timeout_add(0L, self.pipeline.set_state, gst.STATE_PLAYING)
|
||||
# would not work.
|
||||
|
||||
def play():
|
||||
self.pipeline.set_state(gst.STATE_PLAYING)
|
||||
return False
|
||||
self.schedule(0, play)
|
||||
|
||||
#self.pipeline.set_state(gst.STATE_PLAYING)
|
||||
logger.debug('scheduled setting to play')
|
||||
|
||||
def stopped(self):
|
||||
logger.debug('stopped')
|
||||
if not self._last:
|
||||
# see http://bugzilla.gnome.org/show_bug.cgi?id=578612
|
||||
logger.debug(
|
||||
'not a single buffer gotten, setting exception EmptyError')
|
||||
self.setException(common.EmptyError('not a single buffer gotten'))
|
||||
return
|
||||
else:
|
||||
self._checksum = self._checksum % 2 ** 32
|
||||
logger.debug("last buffer's sample offset %r", self._last.offset)
|
||||
logger.debug("last buffer's sample size %r", len(self._last) / 4)
|
||||
last = self._last.offset + len(self._last) / 4 - 1
|
||||
logger.debug("last sample offset in buffer: %r", last)
|
||||
logger.debug("requested sample end: %r", self._sampleEnd)
|
||||
logger.debug("requested sample length: %r", self._sampleLength)
|
||||
logger.debug("checksum: %08X", self._checksum)
|
||||
logger.debug("bytes: %d", self._bytes)
|
||||
if self._sampleEnd != last:
|
||||
msg = 'did not get all samples, %d of %d missing' % (
|
||||
self._sampleEnd - last, self._sampleEnd)
|
||||
logger.warning(msg)
|
||||
self.setExceptionAndTraceback(common.MissingFrames(msg))
|
||||
return
|
||||
|
||||
self.checksum = self._checksum
|
||||
|
||||
### subclass methods
|
||||
|
||||
def do_checksum_buffer(self, buf, checksum):
|
||||
"""
|
||||
Subclasses should implement this.
|
||||
|
||||
@param buf: a byte buffer containing two 16-bit samples per
|
||||
channel.
|
||||
@type buf: C{str}
|
||||
@param checksum: the checksum so far, as returned by the
|
||||
previous call.
|
||||
@type checksum: C{int}
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
### private methods
|
||||
|
||||
def _new_buffer_cb(self, sink):
|
||||
buf = sink.emit('pull-buffer')
|
||||
gst.log('received new buffer at offset %r with length %r' % (
|
||||
buf.offset, buf.size))
|
||||
if self._first is None:
|
||||
self._first = buf.offset
|
||||
logger.debug('first sample is sample offset %r', self._first)
|
||||
self._last = buf
|
||||
|
||||
assert len(buf) % 4 == 0, "buffer is not a multiple of 4 bytes"
|
||||
|
||||
# FIXME: gst-python 0.10.14.1 doesn't have adapter_peek/_take wrapped
|
||||
# see http://bugzilla.gnome.org/show_bug.cgi?id=576505
|
||||
self._adapter.push(buf)
|
||||
|
||||
while self._adapter.available() >= common.BYTES_PER_FRAME:
|
||||
# FIXME: in 0.10.14.1, take_buffer leaks a ref
|
||||
buf = self._adapter.take_buffer(common.BYTES_PER_FRAME)
|
||||
|
||||
self._checksum = self.do_checksum_buffer(buf, self._checksum)
|
||||
self._bytes += len(buf)
|
||||
|
||||
# update progress
|
||||
sample = self._first + self._bytes / 4
|
||||
samplesDone = sample - self._sampleStart
|
||||
progress = float(samplesDone) / float((self._sampleLength))
|
||||
# marshal to the main thread
|
||||
self.schedule(0, self.setProgress, progress)
|
||||
|
||||
def _eos_cb(self, sink):
|
||||
# get the last one; FIXME: why does this not get to us before ?
|
||||
#self._new_buffer_cb(sink)
|
||||
logger.debug('eos, scheduling stop')
|
||||
self.schedule(0, self.stop)
|
||||
|
||||
class CRC32TaskOld(ChecksumTask):
|
||||
"""
|
||||
I do a simple CRC32 check.
|
||||
"""
|
||||
|
||||
description = 'Calculating CRC'
|
||||
|
||||
def do_checksum_buffer(self, buf, checksum):
|
||||
return zlib.crc32(buf, 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
|
||||
@@ -314,143 +78,3 @@ class FastAccurateRipChecksumTask(etask.Task):
|
||||
self.checksum = arc
|
||||
|
||||
self.stop()
|
||||
|
||||
|
||||
class AccurateRipChecksumTask(ChecksumTask):
|
||||
"""
|
||||
I implement the AccurateRip checksum.
|
||||
|
||||
See http://www.accuraterip.com/
|
||||
"""
|
||||
|
||||
description = 'Calculating AccurateRip checksum'
|
||||
|
||||
def __init__(self, path, trackNumber, trackCount, sampleStart=0,
|
||||
sampleLength=-1):
|
||||
ChecksumTask.__init__(self, path, sampleStart, sampleLength)
|
||||
self._trackNumber = trackNumber
|
||||
self._trackCount = trackCount
|
||||
self._discFrameCounter = 0 # 1-based
|
||||
|
||||
def __repr__(self):
|
||||
return "<AccurateRipCheckSumTask of track %d in %r>" % (
|
||||
self._trackNumber, self._path)
|
||||
|
||||
def do_checksum_buffer(self, buf, checksum):
|
||||
self._discFrameCounter += 1
|
||||
|
||||
# on first track ...
|
||||
if self._trackNumber == 1:
|
||||
# ... skip first 4 CD frames
|
||||
if self._discFrameCounter <= 4:
|
||||
gst.debug('skipping frame %d' % self._discFrameCounter)
|
||||
return checksum
|
||||
# ... on 5th frame, only use last value
|
||||
elif self._discFrameCounter == 5:
|
||||
values = struct.unpack("<I", buf[-4:])
|
||||
checksum += common.SAMPLES_PER_FRAME * 5 * values[0]
|
||||
checksum &= 0xFFFFFFFF
|
||||
return checksum
|
||||
|
||||
# on last track, skip last 5 CD frames
|
||||
if self._trackNumber == self._trackCount:
|
||||
discFrameLength = self._sampleLength / common.SAMPLES_PER_FRAME
|
||||
if self._discFrameCounter > discFrameLength - 5:
|
||||
logger.debug('skipping frame %d', self._discFrameCounter)
|
||||
return checksum
|
||||
|
||||
# self._bytes is updated after do_checksum_buffer
|
||||
factor = self._bytes / 4 + 1
|
||||
values = struct.unpack("<%dI" % (len(buf) / 4), buf)
|
||||
for value in values:
|
||||
checksum += factor * value
|
||||
factor += 1
|
||||
# offset = self._bytes / 4 + i + 1
|
||||
# if offset % common.SAMPLES_PER_FRAME == 0:
|
||||
# print 'frame %d, ends before %d, last value %08x, CRC %08x' % (
|
||||
# offset / common.SAMPLES_PER_FRAME, offset, value, sum)
|
||||
|
||||
checksum &= 0xFFFFFFFF
|
||||
return checksum
|
||||
|
||||
|
||||
class TRMTask(task.GstPipelineTask):
|
||||
"""
|
||||
I calculate a MusicBrainz TRM fingerprint.
|
||||
|
||||
@ivar trm: the resulting trm
|
||||
"""
|
||||
|
||||
trm = None
|
||||
description = 'Calculating fingerprint'
|
||||
|
||||
def __init__(self, path):
|
||||
if not os.path.exists(path):
|
||||
raise IndexError('%s does not exist' % path)
|
||||
|
||||
self.path = path
|
||||
self._trm = None
|
||||
self._bus = None
|
||||
|
||||
def getPipelineDesc(self):
|
||||
return '''
|
||||
filesrc location="%s" !
|
||||
decodebin ! audioconvert ! audio/x-raw-int !
|
||||
trm name=trm !
|
||||
appsink name=sink sync=False emit-signals=True''' % self.path
|
||||
|
||||
def parsed(self):
|
||||
sink = self.pipeline.get_by_name('sink')
|
||||
sink.connect('new-buffer', self._new_buffer_cb)
|
||||
|
||||
def paused(self):
|
||||
gst.debug('query duration')
|
||||
|
||||
self._length, qformat = self.pipeline.query_duration(gst.FORMAT_TIME)
|
||||
gst.debug('total length: %r' % self._length)
|
||||
gst.debug('scheduling setting to play')
|
||||
# since set_state returns non-False, adding it as timeout_add
|
||||
# will repeatedly call it, and block the main loop; so
|
||||
# gobject.timeout_add(0L, self.pipeline.set_state, gst.STATE_PLAYING)
|
||||
# would not work.
|
||||
|
||||
|
||||
# FIXME: can't move this to base class because it triggers too soon
|
||||
# in the case of checksum
|
||||
|
||||
def bus_eos_cb(self, bus, message):
|
||||
gst.debug('eos, scheduling stop')
|
||||
self.schedule(0, self.stop)
|
||||
|
||||
def bus_tag_cb(self, bus, message):
|
||||
taglist = message.parse_tag()
|
||||
if 'musicbrainz-trmid' in taglist.keys():
|
||||
self._trm = taglist['musicbrainz-trmid']
|
||||
|
||||
def _new_buffer_cb(self, sink):
|
||||
# this is just for counting progress
|
||||
buf = sink.emit('pull-buffer')
|
||||
position = buf.timestamp
|
||||
if buf.duration != gst.CLOCK_TIME_NONE:
|
||||
position += buf.duration
|
||||
self.setProgress(float(position) / self._length)
|
||||
|
||||
def stopped(self):
|
||||
self.trm = self._trm
|
||||
|
||||
class MaxSampleTask(ChecksumTask):
|
||||
"""
|
||||
I check for the biggest sample value.
|
||||
"""
|
||||
|
||||
description = 'Finding highest sample value'
|
||||
|
||||
def do_checksum_buffer(self, buf, checksum):
|
||||
values = struct.unpack("<%dh" % (len(buf) / 2), buf)
|
||||
absvalues = [abs(v) for v in values]
|
||||
m = max(absvalues)
|
||||
if checksum < m:
|
||||
checksum = m
|
||||
|
||||
return checksum
|
||||
|
||||
|
||||
@@ -135,47 +135,6 @@ def formatTime(seconds, fractional=3):
|
||||
|
||||
return " ".join(chunks)
|
||||
|
||||
|
||||
def tagListToDict(tl):
|
||||
"""
|
||||
Converts gst.TagList to dict.
|
||||
Also strips it of tags that are not writable.
|
||||
"""
|
||||
import gst
|
||||
|
||||
d = {}
|
||||
for key in tl.keys():
|
||||
if key == gst.TAG_DATE:
|
||||
date = tl[key]
|
||||
d[key] = "%4d-%2d-%2d" % (date.year, date.month, date.day)
|
||||
elif key in [
|
||||
gst.TAG_AUDIO_CODEC,
|
||||
gst.TAG_VIDEO_CODEC,
|
||||
gst.TAG_MINIMUM_BITRATE,
|
||||
gst.TAG_BITRATE,
|
||||
gst.TAG_MAXIMUM_BITRATE,
|
||||
]:
|
||||
pass
|
||||
else:
|
||||
d[key] = tl[key]
|
||||
return d
|
||||
|
||||
|
||||
def tagListEquals(tl1, tl2):
|
||||
d1 = tagListToDict(tl1)
|
||||
d2 = tagListToDict(tl2)
|
||||
|
||||
return d1 == d2
|
||||
|
||||
|
||||
def tagListDifference(tl1, tl2):
|
||||
d1 = tagListToDict(tl1)
|
||||
d2 = tagListToDict(tl2)
|
||||
return set(d1.keys()) - set(d2.keys())
|
||||
|
||||
return d1 == d2
|
||||
|
||||
|
||||
class MissingDependencyException(Exception):
|
||||
dependency = None
|
||||
|
||||
|
||||
@@ -28,148 +28,15 @@ import tempfile
|
||||
from mutagen.flac import FLAC
|
||||
|
||||
from morituri.common import common
|
||||
from morituri.common import gstreamer as cgstreamer
|
||||
from morituri.common import task as ctask
|
||||
from morituri.extern.task import task
|
||||
|
||||
from morituri.extern.task import task, gstreamer
|
||||
from morituri.program import sox
|
||||
from morituri.program import flac
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Profile:
|
||||
|
||||
name = None
|
||||
extension = None
|
||||
pipeline = None
|
||||
losless = None
|
||||
|
||||
def test(self):
|
||||
"""
|
||||
Test if this profile will work.
|
||||
Can check for elements, ...
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class FlacProfile(Profile):
|
||||
name = 'flac'
|
||||
extension = 'flac'
|
||||
pipeline = 'flacenc name=tagger quality=8'
|
||||
lossless = True
|
||||
|
||||
# FIXME: we should do something better than just printing ERRORS
|
||||
|
||||
def test(self):
|
||||
|
||||
# here to avoid import gst eating our options
|
||||
import gst
|
||||
|
||||
plugin = gst.registry_get_default().find_plugin('flac')
|
||||
if not plugin:
|
||||
print 'ERROR: cannot find flac plugin'
|
||||
return False
|
||||
|
||||
versionTuple = tuple([int(x) for x in plugin.get_version().split('.')])
|
||||
if len(versionTuple) < 4:
|
||||
versionTuple = versionTuple + (0, )
|
||||
if versionTuple > (0, 10, 9, 0) and versionTuple <= (0, 10, 15, 0):
|
||||
print 'ERROR: flacenc between 0.10.9 and 0.10.15 has a bug'
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# FIXME: ffenc_alac does not have merge_tags
|
||||
|
||||
|
||||
class AlacProfile(Profile):
|
||||
name = 'alac'
|
||||
extension = 'alac'
|
||||
pipeline = 'ffenc_alac'
|
||||
lossless = True
|
||||
|
||||
# FIXME: wavenc does not have merge_tags
|
||||
|
||||
|
||||
class WavProfile(Profile):
|
||||
name = 'wav'
|
||||
extension = 'wav'
|
||||
pipeline = 'wavenc'
|
||||
lossless = True
|
||||
|
||||
|
||||
class WavpackProfile(Profile):
|
||||
name = 'wavpack'
|
||||
extension = 'wv'
|
||||
pipeline = 'wavpackenc bitrate=0 name=tagger'
|
||||
lossless = True
|
||||
|
||||
|
||||
class _LameProfile(Profile):
|
||||
extension = 'mp3'
|
||||
lossless = False
|
||||
|
||||
def test(self):
|
||||
version = cgstreamer.elementFactoryVersion('lamemp3enc')
|
||||
logger.debug('lamemp3enc version: %r', version)
|
||||
if version:
|
||||
t = tuple([int(s) for s in version.split('.')])
|
||||
if t >= (0, 10, 19):
|
||||
self.pipeline = self._lamemp3enc_pipeline
|
||||
return True
|
||||
|
||||
version = cgstreamer.elementFactoryVersion('lame')
|
||||
logger.debug('lame version: %r', version)
|
||||
if version:
|
||||
self.pipeline = self._lame_pipeline
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class MP3Profile(_LameProfile):
|
||||
name = 'mp3'
|
||||
|
||||
_lame_pipeline = 'lame name=tagger quality=0 ! id3v2mux'
|
||||
_lamemp3enc_pipeline = \
|
||||
'lamemp3enc name=tagger target=bitrate cbr=true bitrate=320 ! ' \
|
||||
'xingmux ! id3v2mux'
|
||||
|
||||
|
||||
class MP3VBRProfile(_LameProfile):
|
||||
name = 'mp3vbr'
|
||||
|
||||
_lame_pipeline = 'lame name=tagger ' \
|
||||
'vbr-quality=0 vbr=new vbr-mean-bitrate=192 ! ' \
|
||||
'id3v2mux'
|
||||
_lamemp3enc_pipeline = 'lamemp3enc name=tagger quality=0 ' \
|
||||
'! xingmux ! id3v2mux'
|
||||
|
||||
|
||||
class VorbisProfile(Profile):
|
||||
name = 'vorbis'
|
||||
extension = 'oga'
|
||||
pipeline = 'audioconvert ! vorbisenc name=tagger ! oggmux'
|
||||
lossless = False
|
||||
|
||||
|
||||
PROFILES = {
|
||||
'wav': WavProfile,
|
||||
'flac': FlacProfile,
|
||||
'alac': AlacProfile,
|
||||
'wavpack': WavpackProfile,
|
||||
}
|
||||
|
||||
LOSSY_PROFILES = {
|
||||
'mp3': MP3Profile,
|
||||
'mp3vbr': MP3VBRProfile,
|
||||
'vorbis': VorbisProfile,
|
||||
}
|
||||
|
||||
ALL_PROFILES = PROFILES.copy()
|
||||
ALL_PROFILES.update(LOSSY_PROFILES)
|
||||
|
||||
class SoxPeakTask(task.Task):
|
||||
description = 'Calculating peak level'
|
||||
|
||||
@@ -226,380 +93,3 @@ class TaggingTask(task.Task):
|
||||
w.save()
|
||||
|
||||
self.stop()
|
||||
|
||||
class EncodeTask(ctask.GstPipelineTask):
|
||||
"""
|
||||
I am a task that encodes a .wav file.
|
||||
I set tags too.
|
||||
I also calculate the peak level of the track.
|
||||
|
||||
@param peak: the peak volume, from 0.0 to 1.0. This is the sqrt of the
|
||||
peak power.
|
||||
@type peak: float
|
||||
"""
|
||||
|
||||
logCategory = 'EncodeTask'
|
||||
|
||||
description = 'Encoding'
|
||||
peak = None
|
||||
|
||||
def __init__(self, inpath, outpath, profile, taglist=None, what="track"):
|
||||
"""
|
||||
@param profile: encoding profile
|
||||
@type profile: L{Profile}
|
||||
"""
|
||||
assert type(inpath) is unicode, "inpath %r is not unicode" % inpath
|
||||
assert type(outpath) is unicode, \
|
||||
"outpath %r is not unicode" % outpath
|
||||
|
||||
self._inpath = inpath
|
||||
self._outpath = outpath
|
||||
self._taglist = taglist
|
||||
self._length = 0 # in samples
|
||||
|
||||
self._level = None
|
||||
self._peakdB = None
|
||||
self._profile = profile
|
||||
|
||||
self.description = "Encoding %s" % what
|
||||
self._profile.test()
|
||||
|
||||
cgstreamer.removeAudioParsers()
|
||||
|
||||
def getPipelineDesc(self):
|
||||
# start with an emit interval of one frame, because we end up setting
|
||||
# the final interval after paused and after processing some samples
|
||||
# already, which is too late
|
||||
interval = int(self.gst.SECOND / 75.0)
|
||||
return '''
|
||||
filesrc location="%s" !
|
||||
decodebin name=decoder !
|
||||
audio/x-raw-int,width=16,depth=16,channels=2 !
|
||||
level name=level interval=%d !
|
||||
%s ! identity name=identity !
|
||||
filesink location="%s" name=sink''' % (
|
||||
gstreamer.quoteParse(self._inpath).encode('utf-8'),
|
||||
interval,
|
||||
self._profile.pipeline,
|
||||
gstreamer.quoteParse(self._outpath).encode('utf-8'))
|
||||
|
||||
def parsed(self):
|
||||
tagger = self.pipeline.get_by_name('tagger')
|
||||
|
||||
# set tags
|
||||
if tagger and self._taglist:
|
||||
# FIXME: under which conditions do we not have merge_tags ?
|
||||
# See for example comment saying wavenc did not have it.
|
||||
try:
|
||||
tagger.merge_tags(self._taglist, self.gst.TAG_MERGE_APPEND)
|
||||
except AttributeError, e:
|
||||
logger.warning('Could not merge tags: %r', str(e))
|
||||
|
||||
def paused(self):
|
||||
# get length
|
||||
identity = self.pipeline.get_by_name('identity')
|
||||
logger.debug('query duration')
|
||||
try:
|
||||
length, qformat = identity.query_duration(self.gst.FORMAT_DEFAULT)
|
||||
except self.gst.QueryError, e:
|
||||
self.setException(e)
|
||||
self.stop()
|
||||
return
|
||||
|
||||
|
||||
# wavparse 0.10.14 returns in bytes
|
||||
if qformat == self.gst.FORMAT_BYTES:
|
||||
logger.debug('query returned in BYTES format')
|
||||
length /= 4
|
||||
logger.debug('total length: %r', length)
|
||||
self._length = length
|
||||
|
||||
duration = None
|
||||
try:
|
||||
duration, qformat = identity.query_duration(self.gst.FORMAT_TIME)
|
||||
except self.gst.QueryError, e:
|
||||
logger.debug('Could not query duration')
|
||||
self._duration = duration
|
||||
|
||||
# set up level callbacks
|
||||
# FIXME: publicize bus and reuse it instead of regetting and adding ?
|
||||
bus = self.pipeline.get_bus()
|
||||
bus.add_signal_watch()
|
||||
|
||||
bus.connect('message::element', self._message_element_cb)
|
||||
self._level = self.pipeline.get_by_name('level')
|
||||
|
||||
# set an interval that is smaller than the duration
|
||||
# FIXME: check level and make sure it emits level up to the last
|
||||
# sample, even if input is small
|
||||
interval = self.gst.SECOND
|
||||
if interval > duration:
|
||||
interval = duration / 2
|
||||
logger.debug('Setting level interval to %s, duration %s',
|
||||
self.gst.TIME_ARGS(interval), self.gst.TIME_ARGS(duration))
|
||||
self._level.set_property('interval', interval)
|
||||
# add a probe so we can track progress
|
||||
# we connect to level because this gives us offset in samples
|
||||
srcpad = self._level.get_static_pad('src')
|
||||
self.gst.debug('adding srcpad buffer probe to %r' % srcpad)
|
||||
ret = srcpad.add_buffer_probe(self._probe_handler)
|
||||
self.gst.debug('added srcpad buffer probe to %r: %r' % (srcpad, ret))
|
||||
|
||||
def _probe_handler(self, pad, buffer):
|
||||
# update progress based on buffer offset (expected to be in samples)
|
||||
# versus length in samples
|
||||
# marshal to main thread
|
||||
self.schedule(0, self.setProgress,
|
||||
float(buffer.offset) / self._length)
|
||||
|
||||
# don't drop the buffer
|
||||
return True
|
||||
|
||||
def bus_eos_cb(self, bus, message):
|
||||
logger.debug('eos, scheduling stop')
|
||||
self.schedule(0, self.stop)
|
||||
|
||||
def _message_element_cb(self, bus, message):
|
||||
if message.src != self._level:
|
||||
return
|
||||
|
||||
s = message.structure
|
||||
if s.get_name() != 'level':
|
||||
return
|
||||
|
||||
|
||||
if self._peakdB is None:
|
||||
self._peakdB = s['peak'][0]
|
||||
|
||||
for p in s['peak']:
|
||||
if self._peakdB < p:
|
||||
logger.debug('higher peakdB found, now %r', self._peakdB)
|
||||
self._peakdB = p
|
||||
|
||||
# FIXME: works around a bug on F-15 where buffer probes don't seem
|
||||
# to get triggered to update progress
|
||||
if self._duration is not None:
|
||||
self.schedule(0, self.setProgress,
|
||||
float(s['stream-time'] + s['duration']) / self._duration)
|
||||
|
||||
def stopped(self):
|
||||
if self._peakdB is not None:
|
||||
logger.debug('peakdB %r', self._peakdB)
|
||||
self.peak = math.sqrt(math.pow(10, self._peakdB / 10.0))
|
||||
return
|
||||
|
||||
logger.warning('No peak found.')
|
||||
|
||||
self.peak = 0.0
|
||||
|
||||
if self._duration:
|
||||
logger.warning('GStreamer level element did not send messages.')
|
||||
# workaround for when the file is too short to have volume ?
|
||||
if self._length == common.SAMPLES_PER_FRAME:
|
||||
logger.warning('only one frame of audio, setting peak to 0.0')
|
||||
self.peak = 0.0
|
||||
|
||||
class TagReadTask(ctask.GstPipelineTask):
|
||||
"""
|
||||
I am a task that reads tags.
|
||||
|
||||
@ivar taglist: the tag list read from the file.
|
||||
@type taglist: L{gst.TagList}
|
||||
"""
|
||||
|
||||
logCategory = 'TagReadTask'
|
||||
|
||||
description = 'Reading tags'
|
||||
|
||||
taglist = None
|
||||
|
||||
def __init__(self, path):
|
||||
"""
|
||||
"""
|
||||
assert type(path) is unicode, "path %r is not unicode" % path
|
||||
|
||||
self._path = path
|
||||
|
||||
def getPipelineDesc(self):
|
||||
return '''
|
||||
filesrc location="%s" !
|
||||
decodebin name=decoder !
|
||||
fakesink''' % (
|
||||
gstreamer.quoteParse(self._path).encode('utf-8'))
|
||||
|
||||
def bus_eos_cb(self, bus, message):
|
||||
logger.debug('eos, scheduling stop')
|
||||
self.schedule(0, self.stop)
|
||||
|
||||
def bus_tag_cb(self, bus, message):
|
||||
taglist = message.parse_tag()
|
||||
logger.debug('tag_cb, %d tags' % len(taglist.keys()))
|
||||
if not self.taglist:
|
||||
self.taglist = taglist
|
||||
else:
|
||||
import gst
|
||||
self.taglist = self.taglist.merge(taglist, gst.TAG_MERGE_REPLACE)
|
||||
|
||||
|
||||
class TagWriteTask(ctask.LoggableTask):
|
||||
"""
|
||||
I am a task that retags an encoded file.
|
||||
"""
|
||||
|
||||
logCategory = 'TagWriteTask'
|
||||
|
||||
description = 'Writing tags'
|
||||
|
||||
def __init__(self, inpath, outpath, taglist=None):
|
||||
"""
|
||||
"""
|
||||
assert type(inpath) is unicode, "inpath %r is not unicode" % inpath
|
||||
assert type(outpath) is unicode, "outpath %r is not unicode" % outpath
|
||||
|
||||
self._inpath = inpath
|
||||
self._outpath = outpath
|
||||
self._taglist = taglist
|
||||
|
||||
def start(self, runner):
|
||||
task.Task.start(self, runner)
|
||||
|
||||
# here to avoid import gst eating our options
|
||||
import gst
|
||||
|
||||
# FIXME: this hardcodes flac; we should be using the correct
|
||||
# tag element instead
|
||||
self._pipeline = gst.parse_launch('''
|
||||
filesrc location="%s" !
|
||||
flactag name=tagger !
|
||||
filesink location="%s"''' % (
|
||||
gstreamer.quoteParse(self._inpath).encode('utf-8'),
|
||||
gstreamer.quoteParse(self._outpath).encode('utf-8')))
|
||||
|
||||
# set tags
|
||||
tagger = self._pipeline.get_by_name('tagger')
|
||||
if self._taglist:
|
||||
tagger.merge_tags(self._taglist, gst.TAG_MERGE_APPEND)
|
||||
|
||||
logger.debug('pausing pipeline')
|
||||
self._pipeline.set_state(gst.STATE_PAUSED)
|
||||
self._pipeline.get_state()
|
||||
logger.debug('paused pipeline')
|
||||
|
||||
# add eos handling
|
||||
bus = self._pipeline.get_bus()
|
||||
bus.add_signal_watch()
|
||||
bus.connect('message::eos', self._message_eos_cb)
|
||||
|
||||
logger.debug('scheduling setting to play')
|
||||
# since set_state returns non-False, adding it as timeout_add
|
||||
# will repeatedly call it, and block the main loop; so
|
||||
# gobject.timeout_add(0L, self._pipeline.set_state,
|
||||
# gst.STATE_PLAYING)
|
||||
# would not work.
|
||||
|
||||
def play():
|
||||
self._pipeline.set_state(gst.STATE_PLAYING)
|
||||
return False
|
||||
self.schedule(0, play)
|
||||
|
||||
#self._pipeline.set_state(gst.STATE_PLAYING)
|
||||
logger.debug('scheduled setting to play')
|
||||
|
||||
def _message_eos_cb(self, bus, message):
|
||||
logger.debug('eos, scheduling stop')
|
||||
self.schedule(0, self.stop)
|
||||
|
||||
def stop(self):
|
||||
# here to avoid import gst eating our options
|
||||
import gst
|
||||
|
||||
logger.debug('stopping')
|
||||
logger.debug('setting state to NULL')
|
||||
self._pipeline.set_state(gst.STATE_NULL)
|
||||
logger.debug('set state to NULL')
|
||||
task.Task.stop(self)
|
||||
|
||||
|
||||
class SafeRetagTask(ctask.LoggableMultiSeparateTask):
|
||||
"""
|
||||
I am a task that retags an encoded file safely in place.
|
||||
First of all, if the new tags are the same as the old ones, it doesn't
|
||||
do anything.
|
||||
If the tags are not the same, then the file gets retagged, but only
|
||||
if the decodes of the original and retagged file checksum the same.
|
||||
|
||||
@ivar changed: True if the tags have changed (and hence an output file is
|
||||
generated)
|
||||
"""
|
||||
|
||||
logCategory = 'SafeRetagTask'
|
||||
|
||||
description = 'Retagging'
|
||||
|
||||
changed = False
|
||||
|
||||
def __init__(self, path, taglist=None):
|
||||
"""
|
||||
"""
|
||||
assert type(path) is unicode, "path %r is not unicode" % path
|
||||
|
||||
task.MultiSeparateTask.__init__(self)
|
||||
|
||||
self._path = path
|
||||
self._taglist = taglist.copy()
|
||||
|
||||
self.tasks = [TagReadTask(path), ]
|
||||
|
||||
def stopped(self, taskk):
|
||||
from morituri.common import checksum
|
||||
|
||||
if not taskk.exception:
|
||||
# Check if the tags are different or not
|
||||
if taskk == self.tasks[0]:
|
||||
taglist = taskk.taglist.copy()
|
||||
if common.tagListEquals(taglist, self._taglist):
|
||||
logger.debug('tags are already fine: %r',
|
||||
common.tagListToDict(taglist))
|
||||
else:
|
||||
# need to retag
|
||||
logger.debug('tags need to be rewritten')
|
||||
logger.debug('Current tags: %r, new tags: %r',
|
||||
common.tagListToDict(taglist),
|
||||
common.tagListToDict(self._taglist))
|
||||
assert common.tagListToDict(taglist) \
|
||||
!= common.tagListToDict(self._taglist)
|
||||
self.tasks.append(checksum.CRC32Task(self._path))
|
||||
self._fd, self._tmppath = tempfile.mkstemp(
|
||||
dir=os.path.dirname(self._path), suffix=u'.morituri')
|
||||
self.tasks.append(TagWriteTask(self._path,
|
||||
self._tmppath, self._taglist))
|
||||
self.tasks.append(checksum.CRC32Task(self._tmppath))
|
||||
self.tasks.append(TagReadTask(self._tmppath))
|
||||
elif len(self.tasks) > 1 and taskk == self.tasks[4]:
|
||||
if common.tagListEquals(self.tasks[4].taglist, self._taglist):
|
||||
logger.debug('tags written successfully')
|
||||
c1 = self.tasks[1].checksum
|
||||
c2 = self.tasks[3].checksum
|
||||
logger.debug('comparing checksums %08x and %08x' % (c1, c2))
|
||||
if c1 == c2:
|
||||
# data is fine, so we can now move
|
||||
# but first, copy original mode to our temporary file
|
||||
shutil.copymode(self._path, self._tmppath)
|
||||
logger.debug('moving temporary file to %r' % self._path)
|
||||
os.rename(self._tmppath, self._path)
|
||||
self.changed = True
|
||||
else:
|
||||
# FIXME: don't raise TypeError
|
||||
e = TypeError("Checksums failed")
|
||||
self.setAndRaiseException(e)
|
||||
else:
|
||||
logger.debug('failed to update tags, only have %r',
|
||||
common.tagListToDict(self.tasks[4].taglist))
|
||||
logger.debug('difference: %r',
|
||||
common.tagListDifference(self.tasks[4].taglist,
|
||||
self._taglist))
|
||||
os.unlink(self._tmppath)
|
||||
e = TypeError("Tags not written")
|
||||
self.setAndRaiseException(e)
|
||||
|
||||
task.MultiSeparateTask.stopped(self, taskk)
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
# -*- Mode: Python; test-case-name: morituri.test.test_common_gstreamer -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
# Morituri - for those about to RIP
|
||||
|
||||
# Copyright (C) 2009 Thomas Vander Stichele
|
||||
|
||||
# This file is part of morituri.
|
||||
#
|
||||
# morituri is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# morituri is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with morituri. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
import commands
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# workaround for issue #64
|
||||
|
||||
|
||||
def removeAudioParsers():
|
||||
logger.debug('Removing buggy audioparsers plugin if needed')
|
||||
|
||||
import gst
|
||||
registry = gst.registry_get_default()
|
||||
|
||||
plugin = registry.find_plugin("audioparsersbad")
|
||||
if plugin:
|
||||
# always remove from bad
|
||||
logger.debug('removing audioparsersbad plugin from registry')
|
||||
registry.remove_plugin(plugin)
|
||||
|
||||
plugin = registry.find_plugin("audioparsers")
|
||||
if plugin:
|
||||
logger.debug('removing audioparsers plugin from %s %s',
|
||||
plugin.get_source(), plugin.get_version())
|
||||
|
||||
# the query bug was fixed after 0.10.30 and before 0.10.31
|
||||
# the seek bug is still there though
|
||||
# if plugin.get_source() == 'gst-plugins-good' \
|
||||
# and plugin.get_version() > '0.10.30.1':
|
||||
# return
|
||||
|
||||
registry.remove_plugin(plugin)
|
||||
|
||||
def gstreamerVersion():
|
||||
import gst
|
||||
return _versionify(gst.version())
|
||||
|
||||
def gstPythonVersion():
|
||||
import gst
|
||||
return _versionify(gst.pygst_version)
|
||||
|
||||
_VERSION_RE = re.compile(
|
||||
"Version:\s*(?P<version>[\d.]+)")
|
||||
|
||||
def elementFactoryVersion(name):
|
||||
# surprisingly, there is no python way to get from an element factory
|
||||
# to its plugin and its version directly; you can only compare
|
||||
# with required versions
|
||||
# Let's use gst-inspect-0.10 and wave hands and assume it points to the
|
||||
# same version that python uses
|
||||
output = commands.getoutput('gst-inspect-0.10 %s | grep Version' % name)
|
||||
m = _VERSION_RE.search(output)
|
||||
if not m:
|
||||
return None
|
||||
return m.group('version')
|
||||
|
||||
|
||||
def _versionify(tup):
|
||||
l = list(tup)
|
||||
if len(l) == 4 and l[3] == 0:
|
||||
l = l[:3]
|
||||
v = [str(n) for n in l]
|
||||
return ".".join(v)
|
||||
@@ -30,9 +30,10 @@ import sys
|
||||
import time
|
||||
|
||||
from morituri.common import common, mbngs, cache, path
|
||||
from morituri.common import checksum
|
||||
from morituri.program import cdrdao, cdparanoia
|
||||
from morituri.image import image
|
||||
from morituri.extern.task import task, gstreamer
|
||||
from morituri.extern.task import task
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -172,8 +173,7 @@ class Program:
|
||||
def saveRipResult(self):
|
||||
self._presult.persist()
|
||||
|
||||
def getPath(self, outdir, template, mbdiscid, i, profile=None,
|
||||
disambiguate=False):
|
||||
def getPath(self, outdir, template, mbdiscid, i, disambiguate=False):
|
||||
"""
|
||||
Based on the template, get a complete path for the given track,
|
||||
minus extension.
|
||||
@@ -185,7 +185,6 @@ class Program:
|
||||
@type template: unicode
|
||||
@param i: track number (0 for HTOA, or for disc)
|
||||
@type i: int
|
||||
@type profile: L{morituri.common.encode.Profile}
|
||||
|
||||
@rtype: unicode
|
||||
"""
|
||||
@@ -208,7 +207,7 @@ class Program:
|
||||
v['R'] = 'Unknown'
|
||||
v['B'] = '' # barcode
|
||||
v['C'] = '' # catalog number
|
||||
v['x'] = profile and profile.extension or 'unknown'
|
||||
v['x'] = 'flac'
|
||||
v['X'] = v['x'].upper()
|
||||
v['y'] = '0000'
|
||||
|
||||
@@ -416,12 +415,12 @@ class Program:
|
||||
|
||||
def getTagList(self, number):
|
||||
"""
|
||||
Based on the metadata, get a gst.TagList for the given track.
|
||||
Based on the metadata, get a dict of tags for the given track.
|
||||
|
||||
@param number: track number (0 for HTOA)
|
||||
@type number: int
|
||||
|
||||
@rtype: L{gst.TagList}
|
||||
@rtype: dict
|
||||
"""
|
||||
trackArtist = u'Unknown Artist'
|
||||
albumArtist = u'Unknown Artist'
|
||||
@@ -491,8 +490,6 @@ class Program:
|
||||
return (start, stop)
|
||||
|
||||
def verifyTrack(self, runner, trackResult):
|
||||
# here to avoid import gst eating our options
|
||||
from morituri.common import checksum
|
||||
|
||||
t = checksum.CRC32Task(trackResult.filename)
|
||||
|
||||
@@ -502,9 +499,6 @@ class Program:
|
||||
if isinstance(e.exception, common.MissingFrames):
|
||||
logger.warning('missing frames for %r' % trackResult.filename)
|
||||
return False
|
||||
elif isinstance(e.exception, gstreamer.GstException):
|
||||
logger.warning('GstException %r' % (e.exception, ))
|
||||
return False
|
||||
else:
|
||||
raise
|
||||
|
||||
@@ -513,7 +507,7 @@ class Program:
|
||||
trackResult.testcrc, t.checksum, ret)
|
||||
return ret
|
||||
|
||||
def ripTrack(self, runner, trackResult, offset, device, profile, taglist,
|
||||
def ripTrack(self, runner, trackResult, offset, device, taglist,
|
||||
overread, what=None):
|
||||
"""
|
||||
Ripping the track may change the track's filename as stored in
|
||||
@@ -541,7 +535,6 @@ class Program:
|
||||
self.result.table, start, stop, overread,
|
||||
offset=offset,
|
||||
device=device,
|
||||
profile=profile,
|
||||
taglist=taglist,
|
||||
what=what)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import signal
|
||||
import subprocess
|
||||
|
||||
from morituri.extern import asyncsub
|
||||
from morituri.extern.task import task, gstreamer
|
||||
from morituri.extern.task import task
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -24,10 +24,6 @@ class LoggableMultiSeparateTask(task.MultiSeparateTask):
|
||||
pass
|
||||
|
||||
|
||||
class GstPipelineTask(gstreamer.GstPipelineTask):
|
||||
pass
|
||||
|
||||
|
||||
class PopenTask(task.Task):
|
||||
"""
|
||||
I am a task that runs a command using Popen.
|
||||
|
||||
272
morituri/extern/task/gstreamer.py
vendored
272
morituri/extern/task/gstreamer.py
vendored
@@ -1,272 +0,0 @@
|
||||
# -*- Mode: Python; test-case-name: test_gstreamer -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
# Morituri - for those about to RIP
|
||||
|
||||
# Copyright (C) 2009 Thomas Vander Stichele
|
||||
|
||||
# This file is part of morituri.
|
||||
#
|
||||
# morituri is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# morituri is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with morituri. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import task
|
||||
|
||||
def quoteParse(path):
|
||||
"""
|
||||
Quote a path for use in gst.parse_launch.
|
||||
"""
|
||||
# Make sure double quotes and backslashes are escaped. See
|
||||
# morituri.test.test_common_checksum.NormalPathTestCase
|
||||
|
||||
return path.replace('\\', '\\\\').replace('"', '\\"')
|
||||
|
||||
|
||||
class GstException(Exception):
|
||||
def __init__(self, gerror, debug):
|
||||
self.args = (gerror, debug, )
|
||||
self.gerror = gerror
|
||||
self.debug = debug
|
||||
|
||||
def __repr__(self):
|
||||
return '<GstException: GError %r, debug %r>' % (
|
||||
self.gerror.message, self.debug)
|
||||
|
||||
class GstPipelineTask(task.Task):
|
||||
"""
|
||||
I am a base class for tasks that use a GStreamer pipeline.
|
||||
|
||||
I handle errors and raise them appropriately.
|
||||
|
||||
@cvar gst: the GStreamer module, so code does not have to import gst
|
||||
as a module in code everywhere to avoid option stealing.
|
||||
@cvar playing: whether the pipeline should be set to playing after
|
||||
paused. Some pipelines don't need to play for a task
|
||||
to be done (for example, querying length)
|
||||
@type playing: bool
|
||||
@type pipeline: L{gst.Pipeline}
|
||||
@type bus: L{gst.Bus}
|
||||
"""
|
||||
|
||||
gst = None
|
||||
playing = True
|
||||
pipeline = None
|
||||
bus = None
|
||||
|
||||
### task.Task implementations
|
||||
def start(self, runner):
|
||||
import gst
|
||||
self.gst = gst
|
||||
|
||||
task.Task.start(self, runner)
|
||||
|
||||
self.getPipeline()
|
||||
|
||||
self.bus = self.pipeline.get_bus()
|
||||
# FIXME: remove this
|
||||
self._bus = self.bus
|
||||
self.gst.debug('got bus %r' % self.bus)
|
||||
|
||||
# a signal watch calls callbacks from an idle loop
|
||||
# self.bus.add_signal_watch()
|
||||
|
||||
# sync emission triggers sync-message signals which calls callbacks
|
||||
# from the thread that signals, but happens immediately
|
||||
self.bus.enable_sync_message_emission()
|
||||
self.bus.connect('sync-message::eos', self.bus_eos_cb)
|
||||
self.bus.connect('sync-message::tag', self.bus_tag_cb)
|
||||
self.bus.connect('sync-message::error', self.bus_error_cb)
|
||||
|
||||
self.parsed()
|
||||
|
||||
self.debug('setting pipeline to PAUSED')
|
||||
self.pipeline.set_state(gst.STATE_PAUSED)
|
||||
self.debug('set pipeline to PAUSED')
|
||||
# FIXME: this can block
|
||||
ret = self.pipeline.get_state()
|
||||
self.debug('got pipeline to PAUSED: %r', ret)
|
||||
|
||||
# GStreamer tasks could already be done in paused, and not
|
||||
# need playing.
|
||||
if self.exception:
|
||||
raise self.exception
|
||||
|
||||
done = self.paused()
|
||||
|
||||
if done:
|
||||
self.debug('paused() is done')
|
||||
else:
|
||||
self.debug('paused() wants more')
|
||||
self.play()
|
||||
|
||||
def play(self):
|
||||
# since set_state returns non-False, adding it as timeout_add
|
||||
# will repeatedly call it, and block the main loop; so
|
||||
# gobject.timeout_add(0L, self._pipeline.set_state,
|
||||
# gst.STATE_PLAYING)
|
||||
# would not work.
|
||||
def playLater():
|
||||
if self.exception:
|
||||
self.debug('playLater: exception was raised, not playing')
|
||||
self.stop()
|
||||
return False
|
||||
|
||||
self.debug('setting pipeline to PLAYING')
|
||||
self.pipeline.set_state(self.gst.STATE_PLAYING)
|
||||
self.debug('set pipeline to PLAYING')
|
||||
return False
|
||||
|
||||
if self.playing:
|
||||
self.debug('schedule playLater()')
|
||||
self.schedule(0, playLater)
|
||||
|
||||
def stop(self):
|
||||
self.debug('stopping')
|
||||
|
||||
|
||||
# FIXME: in theory this should help clean up properly,
|
||||
# but in practice we can still get
|
||||
# python: /builddir/build/BUILD/Python-2.7/Python/pystate.c:595: PyGILState_Ensure: Assertion `autoInterpreterState' failed.
|
||||
|
||||
self.pipeline.set_state(self.gst.STATE_READY)
|
||||
self.debug('set pipeline to READY')
|
||||
# FIXME: this can block
|
||||
ret = self.pipeline.get_state()
|
||||
self.debug('got pipeline to READY: %r', ret)
|
||||
|
||||
self.debug('setting state to NULL')
|
||||
self.pipeline.set_state(self.gst.STATE_NULL)
|
||||
self.debug('set state to NULL')
|
||||
self.stopped()
|
||||
task.Task.stop(self)
|
||||
|
||||
### subclass optional implementations
|
||||
def getPipeline(self):
|
||||
desc = self.getPipelineDesc()
|
||||
|
||||
self.debug('creating pipeline %r', desc)
|
||||
self.pipeline = self.gst.parse_launch(desc)
|
||||
|
||||
def getPipelineDesc(self):
|
||||
"""
|
||||
subclasses should implement this to provide a pipeline description.
|
||||
|
||||
@rtype: str
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def parsed(self):
|
||||
"""
|
||||
Called after parsing/getting the pipeline but before setting it to
|
||||
paused.
|
||||
"""
|
||||
pass
|
||||
|
||||
def paused(self):
|
||||
"""
|
||||
Called after pipeline is paused.
|
||||
|
||||
If this returns True, the task is done and
|
||||
should not continue going to PLAYING.
|
||||
"""
|
||||
pass
|
||||
|
||||
def stopped(self):
|
||||
"""
|
||||
Called after pipeline is set back to NULL but before chaining up to
|
||||
stop()
|
||||
"""
|
||||
pass
|
||||
|
||||
def bus_eos_cb(self, bus, message):
|
||||
"""
|
||||
Called synchronously (ie from messaging thread) on eos message.
|
||||
|
||||
Override me to handle eos
|
||||
"""
|
||||
pass
|
||||
|
||||
def bus_tag_cb(self, bus, message):
|
||||
"""
|
||||
Called synchronously (ie from messaging thread) on tag message.
|
||||
|
||||
Override me to handle tags.
|
||||
"""
|
||||
pass
|
||||
|
||||
def bus_error_cb(self, bus, message):
|
||||
"""
|
||||
Called synchronously (ie from messaging thread) on error message.
|
||||
"""
|
||||
self.debug('bus_error_cb: bus %r, message %r' % (bus, message))
|
||||
if self.exception:
|
||||
self.debug('bus_error_cb: already got an exception, ignoring')
|
||||
return
|
||||
|
||||
exc = GstException(*message.parse_error())
|
||||
self.setAndRaiseException(exc)
|
||||
self.debug('error, scheduling stop')
|
||||
self.schedule(0, self.stop)
|
||||
|
||||
def query_length(self, element):
|
||||
"""
|
||||
Query the length of the pipeline in samples, for progress updates.
|
||||
To be called from paused()
|
||||
"""
|
||||
# get duration
|
||||
self.debug('query duration')
|
||||
try:
|
||||
duration, qformat = element.query_duration(self.gst.FORMAT_DEFAULT)
|
||||
except self.gst.QueryError, e:
|
||||
# Fall back to time; for example, oggdemux/vorbisdec only supports
|
||||
# TIME
|
||||
try:
|
||||
duration, qformat = element.query_duration(self.gst.FORMAT_TIME)
|
||||
except self.gst.QueryError, e:
|
||||
self.setException(e)
|
||||
# schedule it, otherwise runner can get set to None before
|
||||
# we're done starting
|
||||
self.schedule(0, self.stop)
|
||||
return
|
||||
|
||||
# wavparse 0.10.14 returns in bytes
|
||||
if qformat == self.gst.FORMAT_BYTES:
|
||||
self.debug('query returned in BYTES format')
|
||||
duration /= 4
|
||||
|
||||
if qformat == self.gst.FORMAT_TIME:
|
||||
rate = None
|
||||
self.debug('query returned in TIME format')
|
||||
# we need sample rate
|
||||
pads = list(element.pads())
|
||||
sink = element.get_by_name('sink')
|
||||
pads += list(sink.pads())
|
||||
|
||||
for pad in pads:
|
||||
caps = pad.get_negotiated_caps()
|
||||
print caps[0].keys()
|
||||
if 'rate' in caps[0].keys():
|
||||
rate = caps[0]['rate']
|
||||
self.debug('Sample rate: %d Hz', rate)
|
||||
|
||||
if not rate:
|
||||
raise KeyError(
|
||||
'Cannot find sample rate, cannot convert to samples')
|
||||
|
||||
duration = int(float(rate) * (float(duration) / self.gst.SECOND))
|
||||
|
||||
self.debug('total duration: %r', duration)
|
||||
|
||||
return duration
|
||||
|
||||
|
||||
@@ -26,7 +26,9 @@ Wrap on-disk CD images based on the .cue file.
|
||||
|
||||
import os
|
||||
|
||||
from morituri.common import encode
|
||||
from morituri.common import common
|
||||
from morituri.common import checksum
|
||||
from morituri.image import cue, table
|
||||
from morituri.extern.task import task
|
||||
from morituri.program.soxi import AudioLengthTask
|
||||
@@ -135,8 +137,6 @@ class AccurateRipChecksumTask(task.MultiSeparateTask):
|
||||
|
||||
path = image.getRealPath(index.path)
|
||||
|
||||
# here to avoid import gst eating our options
|
||||
from morituri.common import checksum
|
||||
|
||||
checksumTask = checksum.FastAccurateRipChecksumTask(path,
|
||||
trackNumber=trackIndex + 1, trackCount=len(cue.table.tracks),
|
||||
@@ -221,27 +221,24 @@ class ImageEncodeTask(task.MultiSeparateTask):
|
||||
|
||||
description = "Encoding tracks"
|
||||
|
||||
def __init__(self, image, profile, outdir):
|
||||
def __init__(self, image, outdir):
|
||||
task.MultiSeparateTask.__init__(self)
|
||||
|
||||
self._image = image
|
||||
self._profile = profile
|
||||
cue = image.cue
|
||||
self._tasks = []
|
||||
self.lengths = {}
|
||||
|
||||
def add(index):
|
||||
# here to avoid import gst eating our options
|
||||
from morituri.common import encode
|
||||
|
||||
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 + '.' + profile.extension)
|
||||
outpath = os.path.join(outdir, root + '.' + 'flac')
|
||||
logger.debug('schedule encode to %r', outpath)
|
||||
taskk = encode.EncodeTaskFlac(path, os.path.join(outdir,
|
||||
root + '.' + profile.extension))
|
||||
root + '.' + 'flac'))
|
||||
self.addTask(taskk)
|
||||
|
||||
try:
|
||||
|
||||
@@ -430,7 +430,7 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
|
||||
_tmppath = None
|
||||
|
||||
def __init__(self, path, table, start, stop, overread, offset=0,
|
||||
device=None, profile=None, taglist=None, what="track"):
|
||||
device=None, taglist=None, what="track"):
|
||||
"""
|
||||
@param path: where to store the ripped track
|
||||
@type path: str
|
||||
@@ -444,10 +444,8 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
|
||||
@type offset: int
|
||||
@param device: the device to rip from
|
||||
@type device: str
|
||||
@param profile: the encoding profile
|
||||
@type profile: L{encode.Profile}
|
||||
@param taglist: a list of tags
|
||||
@param taglist: L{gst.TagList}
|
||||
@param taglist: a dict of tags
|
||||
@type taglist: dict
|
||||
"""
|
||||
task.MultiSeparateTask.__init__(self)
|
||||
|
||||
@@ -461,7 +459,6 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
|
||||
os.close(fd)
|
||||
self._tmpwavpath = tmppath
|
||||
|
||||
# here to avoid import gst eating our options
|
||||
from morituri.common import checksum
|
||||
|
||||
self.tasks = []
|
||||
@@ -487,7 +484,6 @@ class ReadVerifyTrackTask(task.MultiSeparateTask):
|
||||
self._tmppath = tmpoutpath
|
||||
self.path = path
|
||||
|
||||
# here to avoid import gst eating our options
|
||||
from morituri.common import encode
|
||||
|
||||
self.tasks.append(encode.FlacEncodeTask(tmppath, tmpoutpath))
|
||||
|
||||
@@ -56,17 +56,6 @@ class MorituriLogger(result.Logger):
|
||||
lines.append(" Gap detection: cdrdao %s" % ripResult.cdrdaoVersion)
|
||||
lines.append("")
|
||||
|
||||
# Rip encoding settings
|
||||
lines.append("Encoding phase information:")
|
||||
lines.append(" Used output format: %s" % ripResult.profileName)
|
||||
lines.append(" GStreamer:")
|
||||
lines.append(" Pipeline: %s" % ripResult.profilePipeline)
|
||||
lines.append(" Version: %s" % ripResult.gstreamerVersion)
|
||||
lines.append(" Python version: %s" % ripResult.gstPythonVersion)
|
||||
lines.append(" Encoder plugin version: %s" %
|
||||
ripResult.encoderVersion)
|
||||
lines.append("")
|
||||
|
||||
# CD metadata
|
||||
lines.append("CD metadata:")
|
||||
lines.append(" Album: %s - %s" % (ripResult.artist, ripResult.title))
|
||||
|
||||
@@ -108,13 +108,6 @@ class RipResult:
|
||||
cdparanoiaVersion = None
|
||||
cdparanoiaDefeatsCache = None
|
||||
|
||||
gstreamerVersion = None
|
||||
gstPythonVersion = None
|
||||
encoderVersion = None
|
||||
|
||||
profileName = None
|
||||
profilePipeline = None
|
||||
|
||||
classVersion = 3
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
# -*- Mode: Python; test-case-name: morituri.test.test_common_checksum -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import gobject
|
||||
gobject.threads_init()
|
||||
|
||||
from morituri.common import checksum, task as ctask
|
||||
|
||||
from morituri.extern.task import task, gstreamer
|
||||
|
||||
from morituri.test import common as tcommon
|
||||
|
||||
|
||||
def h(i):
|
||||
return "0x%08x" % i
|
||||
|
||||
|
||||
class EmptyTestCase(tcommon.TestCase):
|
||||
|
||||
def testEmpty(self):
|
||||
# this test makes sure that checksumming empty files doesn't hang
|
||||
self.runner = ctask.SyncRunner(verbose=False)
|
||||
fd, path = tempfile.mkstemp(suffix=u'morituri.test.empty')
|
||||
checksumtask = checksum.ChecksumTask(path)
|
||||
# FIXME: do we want a specific error for this ?
|
||||
e = self.assertRaises(task.TaskException, self.runner.run,
|
||||
checksumtask, verbose=False)
|
||||
self.failUnless(isinstance(e.exception, gstreamer.GstException))
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
class PathTestCase(tcommon.TestCase):
|
||||
|
||||
def _testSuffix(self, suffix):
|
||||
self.runner = ctask.SyncRunner(verbose=False)
|
||||
fd, path = tempfile.mkstemp(suffix=suffix)
|
||||
checksumtask = checksum.ChecksumTask(path)
|
||||
e = self.assertRaises(task.TaskException, self.runner.run,
|
||||
checksumtask, verbose=False)
|
||||
self.failUnless(isinstance(e.exception, gstreamer.GstException))
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
class UnicodePathTestCase(PathTestCase, tcommon.UnicodeTestMixin):
|
||||
|
||||
def testUnicodePath(self):
|
||||
# this test makes sure we can checksum a unicode path
|
||||
self._testSuffix(u'morituri.test.B\xeate Noire.empty')
|
||||
|
||||
|
||||
class NormalPathTestCase(PathTestCase):
|
||||
|
||||
def testSingleQuote(self):
|
||||
self._testSuffix(u"morituri.test.Guns 'N Roses")
|
||||
|
||||
def testDoubleQuote(self):
|
||||
# This test makes sure we can checksum files with double quote in
|
||||
# their name
|
||||
self._testSuffix(u'morituri.test.12" edit')
|
||||
|
||||
def testBackSlash(self):
|
||||
# This test makes sure we can checksum files with a backslash in
|
||||
# their name
|
||||
self._testSuffix(u'morituri.test.40 Years Back\\Come')
|
||||
@@ -1,146 +0,0 @@
|
||||
# -*- Mode: Python; test-case-name: morituri.test.test_common_encode -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import gobject
|
||||
gobject.threads_init()
|
||||
|
||||
import gst
|
||||
|
||||
from morituri.common import encode
|
||||
|
||||
from morituri.extern.task import task, gstreamer
|
||||
|
||||
from morituri.test import common
|
||||
|
||||
|
||||
class PathTestCase(common.TestCase):
|
||||
|
||||
def _testSuffix(self, suffix):
|
||||
# because of https://bugzilla.gnome.org/show_bug.cgi?id=688625
|
||||
# we first create the file with a 'normal' filename, then rename
|
||||
self.runner = task.SyncRunner(verbose=False)
|
||||
fd, path = tempfile.mkstemp()
|
||||
|
||||
cmd = "gst-launch " \
|
||||
"audiotestsrc num-buffers=100 samplesperbuffer=1024 ! " \
|
||||
"audioconvert ! audio/x-raw-int,width=16,depth=16,channels =2 ! " \
|
||||
"wavenc ! " \
|
||||
"filesink location=\"%s\" > /dev/null 2>&1" % (
|
||||
gstreamer.quoteParse(path).encode('utf-8'), )
|
||||
self.debug('Running cmd %r' % cmd)
|
||||
os.system(cmd)
|
||||
self.failUnless(os.path.exists(path))
|
||||
os.close(fd)
|
||||
|
||||
fd, newpath = tempfile.mkstemp(suffix=suffix)
|
||||
os.rename(path, newpath)
|
||||
|
||||
encodetask = encode.EncodeTask(newpath, newpath + '.out',
|
||||
encode.WavProfile())
|
||||
self.runner.run(encodetask, verbose=False)
|
||||
os.close(fd)
|
||||
os.unlink(newpath)
|
||||
os.unlink(newpath + '.out')
|
||||
|
||||
|
||||
# class UnicodePathTestCase(PathTestCase, common.UnicodeTestMixin):
|
||||
|
||||
# def testUnicodePath(self):
|
||||
# # this test makes sure we can checksum a unicode path
|
||||
# self._testSuffix(u'.morituri.test_encode.B\xeate Noire')
|
||||
|
||||
|
||||
# class NormalPathTestCase(PathTestCase):
|
||||
|
||||
# def testSingleQuote(self):
|
||||
# self._testSuffix(u".morituri.test_encode.Guns 'N Roses")
|
||||
|
||||
# def testDoubleQuote(self):
|
||||
# self._testSuffix(u'.morituri.test_encode.12" edit')
|
||||
|
||||
|
||||
class TagReadTestCase(common.TestCase):
|
||||
|
||||
def testRead(self):
|
||||
path = os.path.join(os.path.dirname(__file__), u'track.flac')
|
||||
self.runner = task.SyncRunner(verbose=False)
|
||||
t = encode.TagReadTask(path)
|
||||
self.runner.run(t)
|
||||
self.failUnless(t.taglist)
|
||||
self.assertEquals(t.taglist['audio-codec'], 'FLAC')
|
||||
self.assertEquals(t.taglist['description'], 'audiotest wave')
|
||||
|
||||
|
||||
# class TagWriteTestCase(common.TestCase):
|
||||
|
||||
# def testWrite(self):
|
||||
# fd, inpath = tempfile.mkstemp(suffix=u'.morituri.tagwrite.flac')
|
||||
|
||||
# # wave is pink-noise because a pure sine is encoded too efficiently
|
||||
# # by flacenc and triggers not enough frames in parsing
|
||||
# # FIXME: file a bug for this in GStreamer
|
||||
# os.system('gst-launch '
|
||||
# 'audiotestsrc '
|
||||
# 'wave=pink-noise num-buffers=10 samplesperbuffer=588 ! '
|
||||
# 'audioconvert ! '
|
||||
# 'audio/x-raw-int,channels=2,width=16,height=16,rate=44100 ! '
|
||||
# 'flacenc ! filesink location=%s > /dev/null 2>&1' % inpath)
|
||||
# os.close(fd)
|
||||
|
||||
# fd, outpath = tempfile.mkstemp(suffix=u'.morituri.tagwrite.flac')
|
||||
# self.runner = task.SyncRunner(verbose=False)
|
||||
# taglist = gst.TagList()
|
||||
# taglist[gst.TAG_ARTIST] = 'Artist'
|
||||
# taglist[gst.TAG_TITLE] = 'Title'
|
||||
|
||||
# t = encode.TagWriteTask(inpath, outpath, taglist)
|
||||
# self.runner.run(t)
|
||||
|
||||
# t = encode.TagReadTask(outpath)
|
||||
# self.runner.run(t)
|
||||
# self.failUnless(t.taglist)
|
||||
# self.assertEquals(t.taglist['audio-codec'], 'FLAC')
|
||||
# self.assertEquals(t.taglist['description'], 'audiotest wave')
|
||||
# self.assertEquals(t.taglist[gst.TAG_ARTIST], 'Artist')
|
||||
# self.assertEquals(t.taglist[gst.TAG_TITLE], 'Title')
|
||||
|
||||
# os.unlink(inpath)
|
||||
# os.unlink(outpath)
|
||||
|
||||
|
||||
class SafeRetagTestCase(common.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self._fd, self._path = tempfile.mkstemp(suffix=u'.morituri.retag.flac')
|
||||
|
||||
os.system('gst-launch '
|
||||
'audiotestsrc '
|
||||
'num-buffers=40 samplesperbuffer=588 wave=pink-noise ! '
|
||||
'audioconvert ! '
|
||||
'audio/x-raw-int,channels=2,width=16,height=16,rate=44100 ! '
|
||||
'flacenc ! filesink location=%s > /dev/null 2>&1' % self._path)
|
||||
os.close(self._fd)
|
||||
self.runner = task.SyncRunner(verbose=False)
|
||||
|
||||
def tearDown(self):
|
||||
os.unlink(self._path)
|
||||
|
||||
# def testNoChange(self):
|
||||
# taglist = gst.TagList()
|
||||
# taglist[gst.TAG_DESCRIPTION] = 'audiotest wave'
|
||||
# taglist[gst.TAG_AUDIO_CODEC] = 'FLAC'
|
||||
|
||||
# t = encode.SafeRetagTask(self._path, taglist)
|
||||
# self.runner.run(t)
|
||||
|
||||
# def testChange(self):
|
||||
# taglist = gst.TagList()
|
||||
# taglist[gst.TAG_DESCRIPTION] = 'audiotest retagged'
|
||||
# taglist[gst.TAG_AUDIO_CODEC] = 'FLAC'
|
||||
# taglist[gst.TAG_ARTIST] = 'Artist'
|
||||
|
||||
# t = encode.SafeRetagTask(self._path, taglist)
|
||||
# self.runner.run(t)
|
||||
@@ -1,21 +0,0 @@
|
||||
# -*- Mode: Python -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
from morituri.common import gstreamer
|
||||
|
||||
from morituri.test import common
|
||||
|
||||
|
||||
class VersionTestCase(common.TestCase):
|
||||
|
||||
def testGStreamer(self):
|
||||
version = gstreamer.gstreamerVersion()
|
||||
self.failUnless(version.startswith('0.'))
|
||||
|
||||
def testGSTPython(self):
|
||||
version = gstreamer.gstPythonVersion()
|
||||
self.failUnless(version.startswith('0.'))
|
||||
|
||||
def testFlacEnc(self):
|
||||
version = gstreamer.elementFactoryVersion('flacenc')
|
||||
self.failUnless(version.startswith('0.'))
|
||||
@@ -1,85 +0,0 @@
|
||||
# -*- Mode: Python; test-case-name: morituri.test.test_image_image -*-
|
||||
# vi:si:et:sw=4:sts=4:ts=4
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import gobject
|
||||
gobject.threads_init()
|
||||
|
||||
import gst
|
||||
|
||||
from morituri.image import image
|
||||
from morituri.common import common
|
||||
|
||||
from morituri.extern.task import task, gstreamer
|
||||
|
||||
from morituri.test import common as tcommon
|
||||
|
||||
|
||||
def h(i):
|
||||
return "0x%08x" % i
|
||||
|
||||
|
||||
class TrackSingleTestCase(tcommon.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.image = image.Image(os.path.join(os.path.dirname(__file__),
|
||||
u'track-single.cue'))
|
||||
self.runner = task.SyncRunner(verbose=False)
|
||||
self.image.setup(self.runner)
|
||||
|
||||
def testAccurateRipChecksum(self):
|
||||
checksumtask = image.AccurateRipChecksumTask(self.image)
|
||||
self.runner.run(checksumtask, verbose=False)
|
||||
|
||||
self.assertEquals(len(checksumtask.checksums), 4)
|
||||
# self.assertEquals(h(checksumtask.checksums[0]), '0x00000000')
|
||||
# self.assertEquals(h(checksumtask.checksums[1]), '0x793fa868')
|
||||
# self.assertEquals(h(checksumtask.checksums[2]), '0x8dd37c26')
|
||||
# self.assertEquals(h(checksumtask.checksums[3]), '0x00000000')
|
||||
|
||||
def testLength(self):
|
||||
self.assertEquals(self.image.table.getTrackLength(1), 2)
|
||||
self.assertEquals(self.image.table.getTrackLength(2), 2)
|
||||
self.assertEquals(self.image.table.getTrackLength(3), 2)
|
||||
self.assertEquals(self.image.table.getTrackLength(4), 4)
|
||||
|
||||
def testCDDB(self):
|
||||
self.assertEquals(self.image.table.getCDDBDiscId(), "08000004")
|
||||
|
||||
def testAccurateRip(self):
|
||||
self.assertEquals(self.image.table.getAccurateRipIds(), (
|
||||
"00000016", "0000005b"))
|
||||
|
||||
|
||||
class TrackSeparateTestCase(tcommon.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.image = image.Image(os.path.join(os.path.dirname(__file__),
|
||||
u'track-separate.cue'))
|
||||
self.runner = task.SyncRunner(verbose=False)
|
||||
self.image.setup(self.runner)
|
||||
|
||||
def testAccurateRipChecksum(self):
|
||||
checksumtask = image.AccurateRipChecksumTask(self.image)
|
||||
self.runner.run(checksumtask, verbose=False)
|
||||
|
||||
self.assertEquals(len(checksumtask.checksums), 4)
|
||||
self.assertEquals(h(checksumtask.checksums[0]), '0xd60e55e1')
|
||||
self.assertEquals(h(checksumtask.checksums[1]), '0xd63dc2d2')
|
||||
self.assertEquals(h(checksumtask.checksums[2]), '0xd63dc2d2')
|
||||
self.assertEquals(h(checksumtask.checksums[3]), '0x7271db39')
|
||||
|
||||
def testLength(self):
|
||||
self.assertEquals(self.image.table.getTrackLength(1), 10)
|
||||
self.assertEquals(self.image.table.getTrackLength(2), 10)
|
||||
self.assertEquals(self.image.table.getTrackLength(3), 10)
|
||||
self.assertEquals(self.image.table.getTrackLength(4), 10)
|
||||
|
||||
def testCDDB(self):
|
||||
self.assertEquals(self.image.table.getCDDBDiscId(), "08000004")
|
||||
|
||||
def testAccurateRip(self):
|
||||
self.assertEquals(self.image.table.getAccurateRipIds(), (
|
||||
"00000064", "00000191"))
|
||||
Reference in New Issue
Block a user