# xmms.py --- Python interface to the libxmms, plus some higher-level functions
# Copyright (c) 2002 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; either version 2, or (at your option)
# any later version.
#
# 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.

This module provides a Python interface to XMMS (the X MultiMedia
System), an audio (and video) player currently (13 March 2002) running
on Unix 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

  res = xmms.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.py, 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

     [...]

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


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

* From libxmms

  playlist
  play_files
  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

* 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
  
"""

import re, os, os.path, string, math, time
from _xmms import *      # Import all the direct bindings to libxmms functions


class PyXMMSGenericException(Exception):
    """Generic PyXMMS exception.

    This class is meant to be derived for each specific exception.

    """
    def __init__(self, message):
        self.message = message
    def __str__(self):
        return "<%s: %s>" % (self.__class__.__name__, self.message)
    def complete_message(self):
        return "%s: %s." % (self.ExceptionPrettyIdentifier, self.message)
    ExceptionPrettyIdentifier = "PyXMMS generic exception"


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


class InvalidFadeOutAction(PyXMMSGenericException):
    """Exception raised when fade_out is given an invalid action."""
    ExceptionPrettyIdentifier = "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):
    """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

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

    Return None.

    Notable exception: xmms.ExecutableNotFound is raised if
    `xmms_prg' can't be found, read and executed.

    """
    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 MUSTN'T trigger any exception (look at
            #_spawnvef in Python'd os.py).
            try:
                os.execvp(abs_prog_name, (abs_prog_name, "-p") + tuple(seq))
            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:
        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.InvalidFadeOutAction is raised if
    `action' is invalid.

    """

    vol = orig_volume = get_main_volume(session)
    # math.floor() to be safe with Python >= 3.0 while still working before 2.2
    # (first Python version with the // floor division)
    decr = math.floor(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)
