Ejemplo n.º 1
0
  def __init__(self,conf,directory):
    """ Create the audiobook playing abstraction.
    
    Arguments:
      conf        A configuration object like that from the result of the
                  parser in pstorytime.coreparser.
      directory   Directory of the audiobook to play.
    """

    gobject.GObject.__init__(self)
    self._lock = threading.RLock()
    with self._lock:
      self._playing = False
      self.notify("playing")

      self._conf = conf
      self._directory = normcase(expanduser(directory))
      
      self._player = pstorytime.player.Player(self,self._directory)
      self._player.connect("notify::eos",self._on_eos)

      self._log = Log(self,
                      self._player,
                      self._directory,
                      self._conf)
      self._log.connect("notify::playlog",self._on_playlog)

      self._filename = None
      self._eob = False

      # Try to load last entry from play log.
      if len(self._log.playlog)>0:
        start_file = self._log.playlog[-1].filename
        start_pos = self._log.playlog[-1].position
        self._play(start_file, start_pos, log=False, seek=True)
      else:
        # Otherwise use first file in directory.
        dirlist = self.list_files()
        if len(dirlist)>0:
          start_file = dirlist[0]
          self._play(start_file, log=False, seek=True)
        else:
          # Nothing to play!
          self.emit("error","No valid files in audiobook directory.")
Ejemplo n.º 2
0
class AudioBook(gobject.GObject):
  """Audiobook-playing abstraction for gstreamer.

  The same callback system as in gstreamer (and also GTK) is used.  Read up on
  python gobject (or GTK if appropriate) bindings if unsure on how to use them.
  
  Signals:
    error             Contains error messages as strings.

    position          Contains no additional information, but signals that it
                      is appropriate to update position information. This is
                      done when playing, pausing and seeking (also when
                      paused.)

    notify::eob       eob property updated.

    notify::filename  filename property updated.

    notify::playing   playing property updated.

    notify::playlog   playlog property updated.

  """

  SECOND = pstorytime.player.Player.SECOND
  """Time unit of a second according to gstreamer.
  """

  __gsignals__ = {
    'error' : ( gobject.SIGNAL_RUN_LAST,
                gobject.TYPE_NONE,
                (gobject.TYPE_STRING,)),
    'position' : (gobject.SIGNAL_RUN_LAST,
                  gobject.TYPE_NONE,
                  tuple())
  }

  core_extensions = ["m4b"]
  """Extensions to treat as audio files in addition to those registered as such in the system mime type database."""

  @withdoc(gobject.property)
  def playing(self):
    """True if the audiobook is playing. """
    with self._lock:
      return self._playing

  @withdoc(gobject.property)
  def eob(self):
    """True if the audiobook player is currently at the end of the book. """
    with self._lock:
      return self._eob

  @withdoc(gobject.property)
  def filename(self):
    """The currently loaded filename. """
    with self._lock:
      return self._filename

  @withdoc(gobject.property)
  def playlog(self):
    """The current playlog containing walltime, event type, filename and
    position. """
    with self._lock:
      return self._log.playlog

  def __init__(self,conf,directory):
    """ Create the audiobook playing abstraction.
    
    Arguments:
      conf        A configuration object like that from the result of the
                  parser in pstorytime.coreparser.
      directory   Directory of the audiobook to play.
    """

    gobject.GObject.__init__(self)
    self._lock = threading.RLock()
    with self._lock:
      self._playing = False
      self.notify("playing")

      self._conf = conf
      self._directory = normcase(expanduser(directory))
      
      self._player = pstorytime.player.Player(self,self._directory)
      self._player.connect("notify::eos",self._on_eos)

      self._log = Log(self,
                      self._player,
                      self._directory,
                      self._conf)
      self._log.connect("notify::playlog",self._on_playlog)

      self._filename = None
      self._eob = False

      # Try to load last entry from play log.
      if len(self._log.playlog)>0:
        start_file = self._log.playlog[-1].filename
        start_pos = self._log.playlog[-1].position
        self._play(start_file, start_pos, log=False, seek=True)
      else:
        # Otherwise use first file in directory.
        dirlist = self.list_files()
        if len(dirlist)>0:
          start_file = dirlist[0]
          self._play(start_file, log=False, seek=True)
        else:
          # Nothing to play!
          self.emit("error","No valid files in audiobook directory.")

  def _on_playlog(self,log,property):
    """The playlog was updated.
    
    Arguments:
      log       The logger object.
      property  The property object that was updated.
    """
    with self._lock:
      self.notify("playlog")

  def _on_eos(self,player,property):
    """Gstreamer reached the end of a file.
    
    Arguments:
      player    The player that reached the end of a stream.
      property  The property that was updated.
    """
    with self._lock:
      if player.eos:
        # The player reported an end of stream, go to next file.
        next_file = self.get_file(1)
        if next_file != None:
          self._play(next_file)
        else:
          # No next file, we are at the end of the book.
          self._eob = True
          self.notify("eob")
          self._playing = False
          self.notify("playing")
          self.emit("position")
          self._log.lognow("eob")
          self._log.stop()

  def mark(self, name):
    """Manually add an event in the playlog.
    
    Arguments:
      name  Name of the event.
    """
    with self._lock:
      self._log.lognow(name)

  def _play(self, start_file=None, start_pos=None, pos_relative_end=False, log=False, seek=False):
    """Internal general play abstraction.
    
    Arguments:
      start_file        The file to start playing at, use None to use the
                        current file. (Optional, defaults to None.)

      start_pos         The position to start playing at in ns, use None to use
                        the current position. (Or beginning of file if
                        start_file was given.) (Optional, defaults to None.)

      pos_relative_end  True if the length of the track should be added to
                        start_pos.  (Optional, defaults to False.)

      log               True if this should be logged as an event. (Optional,
                        defaults to False.)

      seek              True if this is a seeking operation. (Optional,
                        defaults to False.)

    Returns:  True if successfull.
    """
    with self._lock:
      self._eob = False
      self.notify("eob")

      # Is this a seek while the player is paused?
      paused_seek = seek and (not self._playing)

      # Make sure we are not playing anything.
      self._pause(log=log, seek=seek)

      if start_file != None and start_file != self._filename:
        # Try to load new file.
        self._filename = start_file
        self.notify("filename")
        if not self._player.load(start_file):
          # Failed to load file.
          self._log.lognow("loadfail")
          self._log.stop()
          self._playing = False
          self.notify("playing")
          return False

      if start_pos != None:
        duration = self.duration()
        if pos_relative_end:
          start_pos += duration
        if start_pos < 0:
          # Position in an earlier file.
          prev_file = self.get_file(-1)
          if prev_file == None:
            # Already in first book!
            start_pos = 0
            pos_relative_end = False
            self._player.seek(start_pos)
          else:
            self._play(prev_file, start_pos, pos_relative_end=True, seek=True)
        elif start_pos < duration:
          # Position in this file.
          self._player.seek(start_pos)
        else:
          # Position in a later file.
          next_file = self.get_file(1)
          if next_file == None:
            # Already in last book!
            self._player.seek(duration)
          else:
            self._play(next_file, start_pos-duration, seek=True)

      if log:
        if seek:
          self._log.lognow("seekto")
        else:
          self._log.lognow("start")

        if not paused_seek:
          self._log.start()

      if not paused_seek:
        self._playing = True
        self._player.play()
        self.notify("playing")

      self.emit("position")
      return True

  def _pause(self, log=False, seek=False):
    """Internal general pause abstraction.
    
    Arguments:
      log   True if this should be logged as an event.
      seek  True if this is part of a seek operation.
    """
    with self._lock: 
      if self._playing:
        self._playing = False
        self.notify("playing")
        self._player.pause()
        if log:
          if seek:
            self._log.lognow("seekfrom")
          else:
            self._log.lognow("stop")
          self._log.stop()
        self.emit("position")

  def play(self, start_file=None, start_pos=None):
    """Start playing the audiobook at current location, unless given another
    location.

    Arguments:
      start_file  The file to start playing at, use None to use the current
                  file. (Optional, defaults to None.)

      start_pos   The position to start playing at in ns, use None to use the
                  current position. (Or beginning of file if start_file was
                  given.) (Optional, defaults to None.)
    """
    with self._lock:
      # Only propagate if we are not playing, or we have given a position to
      # start playing at.
      if (not self._playing) or start_file != None or start_pos != None:
        return self._play(start_file, start_pos, log=True)

  def seek(self, start_file=None, start_pos=None):
    """Seek to the given position, otherwise works the same as play.

    Arguments:
      start_file  The file to seek to, use None to indicate the current file.
                  (Optional, defaults to None.)

      start_pos   The position to seek to in ns, use None to indicatethe
                  current position. (Or beginning of file if start_file was
                  given.) (Optional, defaults to None.)
    """
    with self._lock:
      if start_file!=None or start_pos!=None:
        return self._play(start_file, start_pos, log=True, seek=True)

  def dseek(self, delta):
    """Seek relative to the current position.

    Arguments:
      delta   Positive or negative distance to seek in ns.
    """
    with self._lock:
      (filename,pos,_) = self.position()
      return self._play(filename, pos+delta, log=True, seek=True)

  def pause(self):
    """Pause audiobook now. """
    with self._lock:
      if self._playing:
        self._pause(log=True)
        backtrack = self._conf.backtrack
        if backtrack!=None and backtrack>0:
          self.dseek(-backtrack*self.SECOND)

  def play_pause(self):
    """Toggle play/pause. """
    with self._lock:
      if self._playing:
        self.pause()
      else:
        self.play()

  def position(self):
    """Get current filename, position and duration (in ns) as a tuple.
    
    Returns: (filename,position,duration)
    """
    with self._lock:
      return self._player.position()

  def duration(self):
    """Get the duration of the current file.

    Returns:  Duration in ns.
    """
    with self._lock:
      return self._player.duration()

  def list_files(self):
    """List all audio files in audiobook directory.

    Returns:  List of filenames as strings.
    """
    with self._lock:
      entries = os.listdir(self._directory)
      entries.sort()
      return filter(self._is_audio_file, entries)
  
  def _is_audio_file(self,filename):
    """Internal function to check if a file is to be considered an audiobook.

    Returns: True if so.
    """
    with self._lock:
      if not isfile(join(self._directory,filename)):
        return False

      # Check if the filename ends with any of the given extensions.
      exts = AudioBook.core_extensions + self._conf.extensions
      exts = tuple(map(lambda e: '.'+e, exts))
      if filename.endswith(exts):
        return True

      # Check if the mimetype database indicates sais the extension
      # corresponds to an audio file.
      (mime, _) = mimetypes.guess_type(filename)
      if mime != None:
        data = mime.split("/",1)
        return len(data)==2 and data[0] == "audio"
      else:
        return False

  def get_file(self,delta):
    """Get a file relative to the current one.

    Argements:
      delta   Adds this number to the position of the current file in the
              audiobook directory, to get the new file.

    Returns:  New filename.
    """
    try:
      files = self.list_files()
      i = files.index(self._filename)
      if 0 <= i+delta < len(files):
        return files[i+delta]
      else:
        return None
    except ValueError:
      return None

  def gst(self):
    """Get the gstreamer playbin2 object.

    Please do not touch play/pause/seek functionality, or it will seriously
    mess things up. Though feel free to adjust volume/playback speed etc.

    Returns:  Gstreamer playbin2 object.
    """
    return self._player.gst

  def quit(self):
    """Shut down the audiobook player.
    """
    with self._lock:
      self.pause()
      self._player.quit()