# control.py --- Python interface to libxmms -- control module.
# Copyright (c) 2002, 2003, 2005 Florent Rougon
#
# This file is part of PyXMMS.
#
# PyXMMS 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; version 2 dated June, 1991.
#
# PyXMMS 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; see the file COPYING. If not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
# MA 02111-1307, USA.

"""Python interface to XMMS --- control module.

This module provides a Python interface to control XMMS (the X
MultiMedia System), an audio and video player for Unix-like
platforms.

This module provides bindings for all the xmms_remote_* functions
accessible through the libxmms library (which comes with XMMS), plus
a few higher-level functions that I (Florent Rougon) find useful.

The function names and mappings between the calling syntax of the C
functions from libxmms and that of their Python bindings are meant to
be mechanical (see below).

Note: I use the expression "Python binding for foo()" for a Python
function that wraps (thus calls) directly, not doing additional work,
the C function foo() defined in libxmms.


Function names
--------------

The binding for the libxmms function xmms_remote_foo() will be called
foo; therefore, you will probably use it in this way:

  import xmms.control

  res = xmms.control.foo(arg, ...)


Calling syntax -- passing arguments and getting results
-------------------------------------------------------

Each xmms_remote_* function from libxmms takes as its first argument
the XMMS session to control. With xmms.control, this argument is
optional (defaulting to 0, which is generally what you want if you
don't launch multiple XMMS sessions at once) and comes last.

For the other arguments:
 - the type mapping should be obvious (if the C function expects a
   gint, the Python binding expects a Python integer, same for char *
   and strings, gfloats and floats, etc.); when the C function
   expects a list (such as a GList *), the Python binding expects a
   sequence (like a list or a tuple). gboolean types are mapped to
   Python integers (FALSE is mapped to 0 and TRUE to 1).

   Except for 'session', which has to come last so as to be optional,
   the order of the arguments is always preserved.

   Example:

     void xmms_remote_set_volume(gint session, gint vl, gint vr)

   is mapped to:

     set_volume(vl, vr, session)

   where 'vl' and 'vr' have to be Python integers and 'session' (also
   an integer), is optional and defaults to 0.
   Note: 'vl' and 'vr' stand for left and right volume, respectively.
   
 - if the C function returns values through the use of pointers,
   these are returned by the corresponding Python binding and
   therefore are removed from the argument list of the Python
   binding. The type/structure of the return value is the most
   obvious one I could think of (e.g. a single integer if the
   function returns a gboolean). The most non-obvious example is
   indeed quite simple, as you can see:

     void xmms_remote_get_eq(gint session, gfloat *preamp,
                             gfloat **bands)

   which returns a global preamp gain (in dB) and a list of gains for
   10 frequeny bands. It is called like this in Python:

     (preamp, bands) = get_eq(session)

   where 'session' is optional (see above), 'preamp' is a float and
   'bands' is a 10-tuple of floats.

   Note: this is written from this module's scope, but in real life,
   you would of course probably have:

     import xmms.control

     [...]

     (preamp, bands) = xmms.control.get_eq(session)


Functions exported by this module
---------------------------------

* From libxmms

  playlist
  playlist_add
  playlist_delete
  playlist_clear
  playlist_add_url_string
  playlist_ins_url_string

  get_playlist_length
  get_playlist_pos
  set_playlist_pos

  get_playlist_file
  get_playlist_title
  get_playlist_time

  play
  pause
  play_pause
  stop
  eject
  playlist_prev
  playlist_next
  jump_to_time

  is_running
  is_playing
  is_paused

  get_output_time
  get_info

  get_volume
  set_volume
  get_main_volume
  set_main_volume

  get_balance
  set_balance

  get_eq
  set_eq
  get_eq_preamp
  set_eq_preamp
  get_eq_band
  set_eq_band

  get_skin
  set_skin

  main_win_toggle
  pl_win_toggle
  eq_win_toggle

  is_main_win
  is_pl_win
  is_eq_win

  show_prefs_box
  show_about_box

  toggle_aot

  toggle_repeat
  toggle_shuffle

  is_repeat
  is_shuffle

  get_version

  quit

  play_files (deprecated in libxmms)


* Specific to this module (no direct binding in libxmms)

  playlist_add_allow_relative
  enqueue_and_play
  enqueue_and_play_launch_if_session_not_started
  fade_out


Exceptions specific to this module
----------------------------------

ExecutableNotFound
RequestedSessionDoesNotComeUp
InvalidFadeOutAction

They are all subclasses of xmms.error.

"""

import re, os, os.path, string, time
import common
from _xmmscontrol import *


class ExecutableNotFound(common.error):
    """Exception raised when the XMMS executable can't be found."""
    ExceptionShortDescription = "Executable not found"

class RequestedSessionDoesNotComeUp(common.error):
    """Exception raised when a started XMMS session still doesn't answer after a specified timeout."""
    ExceptionShortDescription = "Requested session doesn't come up"

class InvalidFadeOutAction(common.error):
    """Exception raised when fade_out is given an invalid action."""
    ExceptionShortDescription = "Invalid action"


def _find_in_path(prog_name):
    """Search an executable in the PATH, like the exec*p functions do.

    If PATH is not defined, the default path ":/bin:/usr/bin" is
    used, as with the C library exec*p functions.

    Return the absolute file name or None if no readable and
    executable file is found.

    """
    PATH = os.getenv("PATH", ":/bin:/usr/bin") # see the execvp(3) man page
    for dir in string.split(PATH, ":"):
        full_path = os.path.join(dir, prog_name)
        if os.path.isfile(full_path) \
           and os.access(full_path, os.R_OK | os.X_OK):
            return full_path
    return None


def _find_and_check_executable(prog_name):
    """Return the absolute file name if OK, None otherwise."""
    if prog_name[0:2] == "./" or prog_name[0:3] == "../":
        abs_file_name = os.path.join(os.getcwd(), prog_name)
    elif os.path.isabs(prog_name):
        abs_file_name = prog_name
    else:
        # This checks the r and x bits
        return _find_in_path(prog_name)
        
    if os.path.isfile(abs_file_name) and \
       os.access(abs_file_name, os.R_OK | os.X_OK):
        return abs_file_name
    else:
        return None


# Same as playlist_add but converts all relative paths to absolute
def playlist_add_allow_relative(seq, session=0):
    """Add files/URLs to the playlist, allowing relative file names.

    seq     -- a sequence of files/URLs
    session -- the XMMS session to act on

    Return None.

    """
    # Regexp matching absolute paths and strings containing "://"
    abs_re = re.compile(r"/|.*://")
    abs_seq = list(seq)
    for i in range(len(seq)):
        if not (abs_re.match(seq[i])):
            abs_seq[i] = os.path.join(os.getcwd(), seq[i])
    playlist_add(abs_seq, session)

def enqueue_and_play(seq, session=0):
    """Add files/URLs to the playlist and start playing from the first one.

    seq     -- a sequence of files/URLs
    session -- the XMMS session to act on

    The files/URLs in seq are added to the playlist and XMMS is asked
    to play starting at the first element of seq.

    Return None.

    """
    pl = get_playlist_length(session)
    playlist_add_allow_relative(seq, session)
    set_playlist_pos(pl, session)
    play()


def enqueue_and_play_launch_if_session_not_started(seq, xmms_prg="xmms",
                                                   session=0,
                                                   poll_delay=0.1,
                                                   timeout=10.0):
    """Add files/URLs to the playlist and start playing from the first one.

    seq        -- a sequence of files/URLs
    xmms_prg   -- the name (absolute or looked up in the PATH)
                  of an XMMS binary to invoke in case the specified
                  session is not running
    session    -- the XMMS session to act on
    poll_delay -- poll delay, in seconds (float, see below)
    timeout    -- timeout while polling, in seconds (float, see below)

    This function is identical to enqueue_and_play except that it
    spawns an XMMS process if the requested session is not running.

    When it does spawn an XMMS process, it has to wait until XMMS is
    ready to handle requests (here, the first request will be
    enqueue_and_play). It will therefore check every 'poll_delay'
    seconds whether the requested session is ready, and abort after
    'timeout' seconds of unsuccessful checks, raising
    xmms.control.RequestedSessionDoesNotComeUp. If we get that far,
    it may be that your system is very slow, or more likely that the
    XMMS session that was started by this function is not the one
    numbered 'session': there is currently no way to start an XMMS
    session for a chosen number; we have to guess the number of the
    session that will be started...

    Return None.

    Notable exceptions:
        - xmms.control.ExecutableNotFound is raised if 'xmms_prg'
          can't be found, read and executed.
        - xmms.control.RequestedSessionDoesNotComeUp is raised if the
          requested session is still unable to handle requests
          'timeout' seconds after the XMMS process was started by
          this function.

    """
    if not is_running(session):
        # We want to warn the user if the XMMS executable can't be
        # found or executed, so we have to check *before* forking.
        abs_prog_name = _find_and_check_executable(xmms_prg)
        if not abs_prog_name:
            raise ExecutableNotFound("can't find XMMS executable")
            
        child_pid = os.fork()
        if child_pid == 0:
            # We are in the child, me MUST NOT trigger any exception (look at
            # _spawnvef in Python's os.py).
            try:
                os.execvp(abs_prog_name, (abs_prog_name,))
            except:
                # We cannot know in the father process if the child's execvp
                # failed, but AFAIK, there is no simple solution since I don't
                # want the father to wait for the child's completion. init
                # will be a good father. :-)
                os._exit(127)

        else:                           # We are in the father
            start_time = time.time()
            while not is_running(session):
                if time.time() - start_time >= timeout:
                    raise RequestedSessionDoesNotComeUp(
                        "session %u still unavailable after %.2f seconds "
                        "timeout" % (session, timeout))
                time.sleep(poll_delay)

    enqueue_and_play(seq, session)


def fade_out(action="stop", nb_steps=20, step_duration=0.5, restore_volume=1,
             session=0):
    """Fade out the volume to stop or pause the playback.

    Progressively decrease the main volume, then stop or pause
    (depending on the 'action' argument), then optionally restore
    the original main volume setting.

    action         -- a string, either "stop" or "pause"
    nb_steps       -- number of decrease-volume steps to use
    step_duration  -- duration of a step (float, in seconds)
    restore_volume -- boolean (0 = false, 1 = true) telling whether
                      to restore the original main volume setting
                      after the fade out
    session        -- the XMMS session to act on

    Return None.

    Notable exception: xmms.control.InvalidFadeOutAction is raised if
    'action' is invalid.

    """

    vol = orig_volume = get_main_volume(session)
    # int() to be safe with Python >= 3.0 while still working before 2.2
    # (first Python version with the // floor division)
    # Also, int() returns an integer, contrary to math.floor().
    decr = int(orig_volume / nb_steps)
        
    for i in range(nb_steps):
        vol = vol - decr
        set_main_volume(vol, session)
        time.sleep(step_duration)

    # Stop/pause the playback and restore the volume as it was before the fade
    # out.
    if action == "stop":
        stop(session)
    elif action == "pause":
        pause(session)
    else:
        raise InvalidFadeOutAction("invalid action for fade_out: %s" % action)
                                   
    if restore_volume:
        set_main_volume(orig_volume)
