from __future__ import with_statement
## Standard Python libraries.
-import sys as SYS
-import os as OS
import errno as E
-import time as T
-import unicodedata as UD
import fnmatch as FN
+import locale as LC
+import optparse as OP
+import os as OS
import re as RX
+import sys as SYS
+import time as T
+import shlex as L
import shutil as SH
-import optparse as OP
import threading as TH
-import shlex as L
+import unicodedata as UD
from math import sqrt, ceil
from contextlib import contextmanager
## eyeD3 tag fettling.
import eyed3 as E3
-## Gstreamer. It picks up command-line arguments -- most notably `--help' --
-## and processes them itself. Of course, its help is completely wrong. This
-## kludge is due to Jonas Wagner.
-_argv, SYS.argv = SYS.argv, []
-import gobject as G
-import gio as GIO
-import gst as GS
-SYS.argv = _argv
+## Gstreamer.
+import gi
+gi.require_version('GLib', '2.0'); from gi.repository import GLib as G
+gi.require_version('Gio', '2.0'); from gi.repository import Gio as GIO
+gi.require_version('Gst', '1.0'); from gi.repository import Gst as GS
+GS.init([])
## Python Imaging.
from PIL import Image as I
###--------------------------------------------------------------------------
### Eyecandy progress reports.
+DEFAULT_ENCODING = None
+
def charwidth(s):
"""
Return the width of S, in characters.
None of this handles tab characters in any kind of useful way. Sorry.
"""
- ## If there's no encoding for stdout then we're doing something stupid.
- if SYS.stdout.encoding is None: return len(s)
+ global DEFAULT_ENCODING
+
+ ## Figure out the default encoding.
+ if DEFAULT_ENCODING is None: DEFAULT_ENCODING = LC.getpreferredencoding()
## Turn the string into Unicode so we can hack on it properly. Maybe that
## won't work out, in which case fall back to being stupid.
- try: u = s.decode(SYS.stdout.encoding)
+ try: u = s.decode(DEFAULT_ENCODING)
except UnicodeError: return len(s)
## Our main problem is combining characters, but we should also try to
def make_element(factory, name = None, **props):
"Return a new element from the FACTORY with the given NAME and PROPS."
- elt = GS.element_factory_make(factory, name)
+ elt = GS.ElementFactory.make(factory, name)
+ if elt is None: raise ValueError, 'failed to make `%s\' element' % factory
elt.set_properties(**props)
return elt
+def link_elements(elts):
+ "Link the elements ELTS together, in order."
+ e0 = None
+ for e1 in elts:
+ if e0 is not None: e0.link(e1)
+ e0 = e1
+
+def bin_children(bin):
+ "Iterate over the (direct) children of a BIN."
+ iter = bin.iterate_elements()
+ while True:
+ rc, elt = iter.next()
+ if rc == GS.IteratorResult.DONE: break
+ elif rc != GS.IteratorResult.OK:
+ raise ValueError, 'iteration failed (%s)' % rc
+ else: yield elt
+
class GStreamerProgressEyecandy (ProgressEyecandy):
"""
Provide amusement while GStreamer is busy doing something.
## time, because (particularly with VBR-encoded MP3 inputs) the estimated
## duration can change as we progress. Hopefully it settles down fairly
## soon.
- try:
- t, hunoz = me._elt.query_position(GS.FORMAT_TIME)
- end, hukairz = me._elt.query_duration(GS.FORMAT_TIME)
- return t, end
- except GS.QueryError:
- return None, None
+ ok, t = me._elt.query_position(GS.Format.TIME)
+ if ok: ok, end = me._elt.query_duration(GS.Format.TIME)
+ if ok: return t, end
+ else: return None, None
def __enter__(me):
"Enter context: attach progress meter display."
return
## Update regularly. The pipeline runs asynchronously.
- me._id = G.timeout_add(200, me._update)
+ me._id = G.timeout_add(100, me._update)
def __exit__(me, ty, val, tb):
"Leave context: remove display and report completion or failure."
demand.
"""
- def __init__(me, file, mime):
- "Initialize the object suitably for identifying FILE."
-
- ## Make some initial GStreamer objects. We'll want the pipeline later if
- ## we need to analyse a poorly tagged MP3 stream, so save it away.
- me._pipe = GS.Pipeline()
- me._file = file
- bus = me._pipe.get_bus()
- bus.add_signal_watch()
- loop = G.MainLoop()
+ def _prepare_pipeline(me):
+ pipe = GS.Pipeline()
+ bus = pipe.get_bus()
## The basic recognition kit is based around `decodebin'. We must keep
## it happy by giving it sinks for the streams it's found, which it
## announces asynchronously.
- source = make_element('filesrc', 'file', location = file)
+ source = make_element('filesrc', 'file', location = me._file)
decoder = make_element('decodebin', 'decode')
sink = make_element('fakesink')
def decoder_pad_arrived(elt, pad):
- if pad.get_caps()[0].get_name().startswith('audio/'):
+ if pad.get_current_caps()[0].get_name().startswith('audio/'):
elt.link_pads(pad.get_name(), sink, 'sink')
- dpaid = decoder.connect('pad-added', decoder_pad_arrived)
- me._pipe.add(source, decoder, sink)
- GS.element_link_many(source, decoder)
+ decoder.connect('pad-added', decoder_pad_arrived)
+ for i in [source, decoder, sink]: pipe.add(i)
+ link_elements([source, decoder])
+
+ ## Done.
+ return pipe, bus, decoder, sink
+
+ def __init__(me, file, mime):
+ "Initialize the object suitably for identifying FILE."
+
+ me._file = file
+ pipe, bus, decoder, sink = me._prepare_pipeline()
+
+ ## Make some initial GStreamer objects. We'll want the pipeline later if
+ ## we need to analyse a poorly tagged MP3 stream, so save it away.
+ loop = G.MainLoop()
## Arrange to collect tags from the pipeline's bus as they're reported.
- ## If we reuse the pipeline later, we'll want different bus-message
- ## handling, so make sure we can take the signal handler away.
tags = {}
fail = []
def bus_message(bus, msg):
- if msg.type == GS.MESSAGE_ERROR:
- fail[:] = (ValueError, msg.structure['debug'], None)
+ ty, s = msg.type, msg.get_structure()
+ if ty == GS.MessageType.ERROR:
+ fail[:] = (ValueError, s['debug'], None)
loop.quit()
- elif msg.type == GS.MESSAGE_STATE_CHANGED:
- if msg.structure['new-state'] == GS.STATE_PAUSED and \
- msg.src == me._pipe:
+ elif ty == GS.MessageType.STATE_CHANGED:
+ if s['new-state'] == GS.State.PAUSED and \
+ msg.src == pipe:
loop.quit()
- elif msg.type == GS.MESSAGE_TAG:
- tags.update(msg.structure)
+ elif ty == GS.MessageType.TAG:
+ tt = s['taglist']
+ for i in xrange(tt.n_tags()):
+ t = tt.nth_tag_name(i)
+ if tt.get_tag_size(t) != 1: continue
+ v = tt.get_value_index(t, 0)
+ tags[t] = v
bmid = bus.connect('message', bus_message)
## We want to identify the kind of stream this is. (Hmm. The MIME type
## things from being too awful.)
me.cap = None
me.dcap = None
- for e in decoder.elements():
+ for e in bin_children(decoder):
if e.get_factory().get_name() == 'typefind':
tfelt = e
break
## Crank up most of the heavy machinery. The message handler will stop
## the loop when things seem to be sufficiently well underway.
- me._pipe.set_state(GS.STATE_PAUSED)
+ bus.add_signal_watch()
+ pipe.set_state(GS.State.PAUSED)
loop.run()
bus.disconnect(bmid)
- decoder.disconnect(dpaid)
+ bus.remove_signal_watch()
if fail:
- me._pipe.set_state(GS.STATE_NULL)
+ pipe.set_state(GS.State.NULL)
raise fail[0], fail[1], fail[2]
## Store the collected tags.
## Gather the capabilities. The `typefind' element knows the input data
## type. The 'decodebin' knows the raw data type.
- me.cap = tfelt.get_pad('src').get_negotiated_caps()[0]
+ me.cap = tfelt.get_static_pad('src').get_allowed_caps()[0]
me.mime = set([mime, me.cap.get_name()])
- me.dcap = sink.get_pad('sink').get_negotiated_caps()[0]
+ me.dcap = sink.get_static_pad('sink').get_allowed_caps()[0]
## If we found a plausible bitrate then stash it. Otherwise note that we
## failed. If anybody asks then we'll work it out then.
elif 'bitrate' in tags and tags['bitrate'] >= 80000:
me._bitrate = tags['bitrate']/1000
else:
- me._bitrate = None
-
- ## The bitrate computation wants the file size. Ideally we'd want the
- ## total size of the frames' contents, but that seems hard to dredge
- ## out. If the framing overhead is small, this should be close enough
- ## for our purposes.
- me._bytes = OS.stat(file).st_size
-
- def __del__(me):
- "Close the pipeline down so we don't leak file descriptors."
- me._pipe.set_state(GS.STATE_NULL)
+ ok, n = pipe.query_duration(GS.Format.BYTES)
+ if ok: ok, t = pipe.query_duration(GS.Format.TIME)
+ if ok: me._bitrate = int((8e6*n)/t)
+ else: me._bitrate = None
+ pipe.set_state(GS.State.NULL)
@property
def bitrate(me):
if me._bitrate is not None:
return me._bitrate
- ## Make up a new main loop.
+ ## Make up a new pipeline and main loop.
+ pipe, bus, _, _ = me._prepare_pipeline()
loop = G.MainLoop()
## Watch for bus messages. We'll stop when we reach the end of the
## stream: then we'll have a clear idea of how long the track was.
fail = []
def bus_message(bus, msg):
- if msg.type == GS.MESSAGE_ERROR:
- fail[:] = (ValueError, msg.structure['debug'], None)
+ ty, s = msg.type, msg.get_structure()
+ if ty == GS.MessageType.ERROR:
+ fail[:] = (ValueError, s['debug'], None)
loop.quit()
- elif msg.type == GS.MESSAGE_EOS:
+ elif ty == GS.MessageType.EOS:
loop.quit()
- bus = me._pipe.get_bus()
+ bus = pipe.get_bus()
bmid = bus.connect('message', bus_message)
## Get everything moving, and keep the user amused while we work.
- me._pipe.set_state(GS.STATE_PLAYING)
- with GStreamerProgressEyecandy(filestatus(file, 'measure bitrate') %
- me._pipe,
- silentp = True):
+ bus.add_signal_watch()
+ pipe.set_state(GS.State.PLAYING)
+ with GStreamerProgressEyecandy(filestatus(me._file, 'measure bitrate'),
+ pipe, silentp = True):
loop.run()
+ bus.remove_signal_watch()
bus.disconnect(bmid)
if fail:
- me._pipe.set_state(GS.STATE_NULL)
+ pipe.set_state(GS.State.NULL)
raise fail[0], fail[1], fail[2]
+ STATUS.clear()
+
+ ## The bitrate computation wants the file size. Ideally we'd want the
+ ## total size of the frames' contents, but that seems hard to dredge
+ ## out. If the framing overhead is small, this should be close enough
+ ## for our purposes.
+ bytes = OS.stat(me._file).st_size
## Now we should be able to find out our position accurately and work out
## a bitrate. Cache it in case anybody asks again.
- t, hukairz = me._pipe.query_position(GS.FORMAT_TIME)
- me._bitrate = int(8*me._bytes*1e6/t)
+ ok, t = pipe.query_position(GS.Format.TIME)
+ assert ok, 'failed to discover bitrate'
+ me._bitrate = int(8*bytes*1e6/t)
+ pipe.set_state(GS.State.NULL)
## Done.
return me._bitrate
"""
elts = me.encoder_chain()
bin = GS.Bin()
- bin.add(*elts)
- GS.element_link_many(*elts)
- bin.add_pad(GS.GhostPad('sink', elts[0].get_pad('sink')))
- bin.add_pad(GS.GhostPad('src', elts[-1].get_pad('src')))
+ for i in elts: bin.add(i)
+ link_elements(elts)
+ bin.add_pad(GS.GhostPad('sink', elts[0].get_static_pad('sink')))
+ bin.add_pad(GS.GhostPad('src', elts[-1].get_static_pad('src')))
return bin
def convert(me, master, id, target):
## Construct the necessary equipment.
pipe = GS.Pipeline()
bus = pipe.get_bus()
- bus.add_signal_watch()
loop = G.MainLoop()
## Make sure that there isn't anything in the way of our output. We're
convert = make_element('audioconvert', 'convert')
encoder = me.encoder()
sink = make_element('filesink', 'sink', location = new)
- pipe.add(source, decoder, convert, encoder, sink)
- GS.element_link_many(source, decoder)
- GS.element_link_many(convert, encoder, sink)
+ for i in [source, decoder, convert, encoder, sink]: pipe.add(i)
+ link_elements([source, decoder])
+ link_elements([convert, encoder, sink])
## Some decoders (e.g., the AC3 decoder) include channel-position
## indicators in their output caps. The Vorbis encoder interferes with
## our encoding chain. For now, we'll hope that there's only one audio
## stream in there, and just throw everything else away.
def decoder_pad_arrived(elt, pad):
- if pad.get_caps()[0].get_name().startswith('audio/'):
+ if pad.get_current_caps()[0].get_name().startswith('audio/'):
if dcap:
elt.link_pads_filtered(pad.get_name(), convert, 'sink', dcap)
else:
## Watch the bus for completion messages.
fail = []
def bus_message(bus, msg):
- if msg.type == GS.MESSAGE_ERROR:
- fail[:] = (ValueError, msg.structure['debug'], None)
+ if msg.type == GS.MessageType.ERROR:
+ fail[:] = (ValueError, msg.get_structure()['debug'], None)
loop.quit()
- elif msg.type == GS.MESSAGE_EOS:
+ elif msg.type == GS.MessageType.EOS:
loop.quit()
bmid = bus.connect('message', bus_message)
## Get everything ready and let it go.
- pipe.set_state(GS.STATE_PLAYING)
+ bus.add_signal_watch()
+ pipe.set_state(GS.State.PLAYING)
with GStreamerProgressEyecandy(filestatus(master,
'convert to %s' % me.NAME),
pipe):
loop.run()
- pipe.set_state(GS.STATE_NULL)
+ pipe.set_state(GS.State.NULL)
+ bus.remove_signal_watch()
+ bus.disconnect(bmid)
if fail:
raise fail[0], fail[1], fail[2]
def encoder_chain(me):
encprops = {}
- if me.bitrate is not None: encprops['vbr_mean_bitrate'] = me.bitrate
- return [make_element('lame', vbr = 4, **encprops),
+ if me.bitrate is not None:
+ encprops['bitrate'] = me.bitrate
+ encprops['target'] = 'bitrate'
+ else:
+ encprops['quality'] = 4
+ encprops['target'] = 'quality'
+ return [make_element('lamemp3enc', quality = 4, **encprops),
make_element('xingmux'),
make_element('id3v2mux')]
## the appropriate categories. Later, we'll apply policy to the
## files, by category, and work out what to do with them all.
else:
- gf = GIO.File(masterfile)
- mime = gf.query_info('standard::content-type').get_content_type()
+ mime = GIO.file_new_for_path(masterfile) \
+ .query_info('standard::content-type', 0) \
+ .get_content_type()
cats = []
for cat in pmap.iterkeys():
id = cat.identify(masterfile, mime)