gremlin/gremlin.in: Fix things for new GI-based GStreamer etc. bindings.
[autoys] / gremlin / gremlin.in
old mode 100755 (executable)
new mode 100644 (file)
index c029bc3..f3a870a
@@ -7,18 +7,20 @@
 
 ###----- Licensing notice ---------------------------------------------------
 ###
-### This program is free software; you can redistribute it and/or modify
+### This file is part of the `autoys' audio tools collection.
+###
+### `autoys' 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 2 of the License, or
 ### (at your option) any later version.
 ###
-### This program is distributed in the hope that it will be useful,
+### `autoys' 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 this program; if not, write to the Free Software Foundation,
+### along with `autoys'; if not, write to the Free Software Foundation,
 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 
 ###--------------------------------------------------------------------------
@@ -39,20 +41,18 @@ import shutil as SH
 import optparse as OP
 import threading as TH
 import shlex as L
-from math import sqrt
+from math import sqrt, ceil
 from contextlib import contextmanager
 
 ## eyeD3 tag fettling.
-import eyeD3 as E3
+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
@@ -245,7 +245,7 @@ class ProgressEyecandy (object):
     ## Work out -- well, guess -- the time remaining.
     if cur:
       t = T.time()
-      eta = me._fmt_time((t - me._start)*(max - cur)/cur)
+      eta = me._fmt_time(ceil((t - me._start)*(max - cur)/cur))
     else:
       eta = '???'
 
@@ -409,6 +409,8 @@ class FormatParser (P.ParserElement):
   named format and its superclasses.
   """
 
+  name = 'format-spec'
+
   ## We cache the parser elements we generate to avoid enormous consing.
   CACHE = {}
 
@@ -706,10 +708,28 @@ Policy << (And | Or | Accept | Convert)
 
 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.
@@ -750,12 +770,10 @@ class GStreamerProgressEyecandy (ProgressEyecandy):
     ## 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."
@@ -809,7 +827,6 @@ class AudioIdentifier (object):
     me._pipe = GS.Pipeline()
     me._file = file
     bus = me._pipe.get_bus()
-    bus.add_signal_watch()
     loop = G.MainLoop()
 
     ## The basic recognition kit is based around `decodebin'.  We must keep
@@ -819,27 +836,31 @@ class AudioIdentifier (object):
     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)
+    for i in [source, decoder, sink]: me._pipe.add(i)
+    link_elements([source, decoder])
 
     ## 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 \
+      elif ty == GS.MessageType.STATE_CHANGED:
+        if s['new-state'] == GS.State.PAUSED and \
                msg.src == me._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
@@ -851,7 +872,7 @@ class AudioIdentifier (object):
     ## 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
@@ -860,12 +881,14 @@ class AudioIdentifier (object):
 
     ## 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()
+    me._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)
+      me._pipe.set_state(GS.State.NULL)
       raise fail[0], fail[1], fail[2]
 
     ## Store the collected tags.
@@ -873,9 +896,9 @@ class AudioIdentifier (object):
 
     ## 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.
@@ -894,7 +917,7 @@ class AudioIdentifier (object):
 
   def __del__(me):
     "Close the pipeline down so we don't leak file descriptors."
-    me._pipe.set_state(GS.STATE_NULL)
+    me._pipe.set_state(GS.State.NULL)
 
   @property
   def bitrate(me):
@@ -915,28 +938,32 @@ class AudioIdentifier (object):
     ## 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()
     bmid = bus.connect('message', bus_message)
 
     ## Get everything moving, and keep the user amused while we work.
-    me._pipe.set_state(GS.STATE_PLAYING)
+    bus.add_signal_watch()
+    me._pipe.set_state(GS.State.PLAYING)
     with GStreamerProgressEyecandy(filestatus(file, 'measure bitrate') %
                                    me._pipe,
                                    silentp = True):
       loop.run()
+    bus.remove_signal_watch()
     bus.disconnect(bmid)
     if fail:
-      me._pipe.set_state(GS.STATE_NULL)
+      me._pipe.set_state(GS.State.NULL)
       raise fail[0], fail[1], fail[2]
 
     ## 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)
+    ok, t = pipe.query_position(GS.Format.TIME)
+    assert ok, 'failed to discover bitrate'
     me._bitrate = int(8*me._bytes*1e6/t)
 
     ## Done.
@@ -986,10 +1013,10 @@ class AudioFormat (BaseFormat):
     """
     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):
@@ -1002,7 +1029,6 @@ class AudioFormat (BaseFormat):
     ## 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
@@ -1023,9 +1049,9 @@ class AudioFormat (BaseFormat):
     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
@@ -1053,7 +1079,7 @@ class AudioFormat (BaseFormat):
     ## 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:
@@ -1063,20 +1089,23 @@ class AudioFormat (BaseFormat):
     ## 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]
 
@@ -1100,13 +1129,15 @@ class OggVorbisFormat (AudioFormat):
   EXT = 'ogg'
 
   def encoder_chain(me):
-    for q, br in me.QMAP:
-      if br >= me.bitrate:
-        break
-    else:
-      raise ValueError, 'no suitable quality setting found'
-    return [make_element('vorbisenc',
-                         quality = q/10.0),
+    encprops = {}
+    if me.bitrate is not None:
+      for q, br in me.QMAP:
+        if br >= me.bitrate:
+          break
+      else:
+        raise ValueError, 'no suitable quality setting found'
+      encprops['quality'] = q/10.0
+    return [make_element('vorbisenc', **encprops),
             make_element('oggmux')]
 
 defformat('ogg-vorbis', OggVorbisFormat)
@@ -1119,9 +1150,14 @@ class MP3Format (AudioFormat):
   EXT = 'mp3'
 
   def encoder_chain(me):
-    return [make_element('lame',
-                         vbr_mean_bitrate = me.bitrate,
-                         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')]
 
@@ -1132,13 +1168,16 @@ class MP3Format (AudioFormat):
     GStreamer produces ID3v2 tags, but not ID3v1.  This seems unnecessarily
     unkind to stupid players.
     """
-    tag = E3.Tag()
-    tag.link(path)
-    tag.setTextEncoding(E3.UTF_8_ENCODING)
-    try:
-      tag.update(E3.ID3_V1_1)
-    except (UnicodeEncodeError, E3.tag.GenreException):
-      pass
+    f = E3.load(path)
+    if f is None: return
+    t = f.tag
+    if t is None: return
+    for v in [E3.id3.ID3_V2_3, E3.id3.ID3_V1]:
+      try: f.tag.save(version = v)
+      except (UnicodeEncodeError,
+              E3.id3.GenreException,
+              E3.id3.TagException):
+        pass
 
 defformat('mp3', MP3Format)
 
@@ -1236,7 +1275,7 @@ class JPEGFormat (ImageFormat):
   optimize
           If present, take a second pass to select optimal encoder settings.
 
-  progression
+  progressive
           If present, make a progressive file.
 
   quality Integer from 1--100 (worst to best); default is 75.
@@ -1496,8 +1535,9 @@ def grobble(master, targets, noact = False):
           ## 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)