#!/usr/bin/ruby
#
###########################################################################
#
# = Remote Control Interface to XMMS
#
# == Description
# the X Multi Media System
# The library works by connecting to XMMS's controlsocket, usually called 
# word /tmp/xmms_*. For information about XMMS itself see http://www.xmms.org/
#
# (c) Oliver M. Bolzer <oliver@fakeroot.net>, 2002
#
# == Revision History
#
# v0.1.1 (Feb 7, 2002)
# - clean up documentation for use with RDoc http://rdoc.sourceforge.net/
#
# v0.1 (Feb 4, 2002)
# - Initial Public Release 
# - supports most "fire-and-forget" commands as well as
#   volume control
#
# ==TODO
#
# - add support for other remote-controllable functions
# - check protocol versions on reply packets
# 
# == Licence
#
# (c) Oliver M. Bolzer <oliver@fakeroot.net>, 2002
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. Neither the name of the author nor the names of its contributors
#    may be used to endorse or promote products derived from this software
#    without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
###########################################################################

require "etc"      # for Etc.getpwuid
require "socket"   # for UNIX domain sockets

# Remote-Control Interface for XMMS
class XMMS

  ## supported protocol version
  PROTOCOL_VERSION  = 1

  ## remote control commands 
  CMD_GET_VERSION      =  0
  CMD_PLAYLIST_ADD     =  1
  CMD_PLAY             =  2
  CMD_PAUSE            =  3
  CMD_STOP             =  4
  CMD_IS_PLAYING       =  5
  CMD_IS_PAUSED        =  6
  CMD_GET_PLAYLIST_POS =  7
  CMD_SET_PLAYLIST_POS =  8
  CMD_GET_PLAYLIST_LENGTH = 9
  CMD_PLAYLIST_CLEAR      = 10
  CMD_GET_OUTPUT_TIME  = 11
  CMD_JUMP_TO_TIME     = 12
  CMD_GET_VOLUME       = 13
  CMD_SET_VOLUME       = 14

  CMD_SHOW_PREFS_BOX   = 25
  CMD_TOGGLE_AOT       = 26
  CMD_SHOW_ABOUT_BOX   = 27
  CMD_EJECT            = 28
  CMD_PLAYLIST_PREV    = 29 
  CMD_PLAYLIST_NEXT    = 30
  CMD_PING             = 31 

  CMD_QUIT             = 49 

  CMD_PLAY_PAUSE       = 52   # XMMS claims command unknown  
  
  ## volume, both elements are Integers
  Volume = Struct.new( :left, :right )

  ##
  # connect to a running XMMS session,
  # raises Socket-releated I/O exceptions if no XMMS is running 
  def initialize( session = 0 )
    tmpdir = ENV['TMPDIR'] || ENV['TMP'] || ENV['TEMP'] || '/tmp'
    username = Etc.getpwuid.name
    @socketpath = tmpdir + '/xmms_' + username + '.0' # socket of XMMS

    socket = self.connect 
    send_cmd( socket, CMD_PING )
    read_ack( socket )
    socket.close
  end # initialize()

  ##
  # start playing
  def play
    remote_cmd( CMD_PLAY )
  end # play()

  ##
  # stop playing
  def stop
    remote_cmd( CMD_STOP )
  end # stop()

  ##
  # pause current track / resume if paused
  def pause
    remote_cmd( CMD_PAUSE )
  end #pause()

  ##
  # quit XMMS, no gurantee what happens with this object afterwards
  def quit
    remote_cmd( CMD_QUIT )
  end # quit()

  ##
  # remove all entries of playlist
  def clear_playlist
    remote_cmd( CMD_PLAYLIST_CLEAR );
  end # clar_playlist()

  ##
  # disply preferences dialog
  def show_prefs_box
    remote_cmd( CMD_SHOW_PREFS_BOX );
  end # # show_prefs_box()

  ##
  # show "about" dialog
  def show_about_box
    remote_cmd( CMD_SHOW_ABOUT_BOX )
  end # show_about_box()
  
  ##
  # activate "eject" button on XMMS (usually File->Read dialog)
  def eject
    remote_cmd( CMD_EJECT )
  end # eject()

  ##
  # skipt to next track
  def next_track
    remote_cmd( CMD_PLAYLIST_NEXT )
  end # next_track()

  ##
  # skip to previous track
  def prev_track
    remote_cmd( CMD_PLAYLIST_PREV )
  end # next_track()

  ##
  # current PCM-channel volume, returns Struct.new( :left, :right ) 
  def volume
    socket = self.connect
    send_cmd( socket, CMD_GET_VOLUME )
    data = read_reply( socket ).unpack("II")
    volume = Volume.new
    volume.left = data[0]
    volume.right = data[1]
    read_ack( socket )
    socket.close
    return volume
  end # volume()

  ##
  # set PCM-channel volume
  # +vol+ is of Struct Volume
  def volume=( vol )
    lvol = vol.left
    rvol = vol.right

    lvol =   0 if lvol < 0
    lvol = 100 if lvol > 100
    rvol =   0 if rvol < 0
    rvol = 100 if rvol > 100

    volume = [ lvol, rvol ].pack("II")
    socket = self.connect
    send_cmd( socket, CMD_SET_VOLUME, volume )
    read_ack( socket )
    socket.close
  end # volume=()

  ######### private helper-functions #########
  :private
  ## get UNIX domain socket to XMMS
  def connect
    UNIXSocket.open( @socketpath )
  end
  
  ## send command and data to XMMS
  def send_cmd( socket, cmd, data = '' )
    socket.write  [PROTOCOL_VERSION, cmd, data.size].pack("SSI" )
    if !data.empty? then
      socket.write data
    end
  end # send_cmd()

  ## send command with no reply
  def remote_cmd( cmd )
    socket = self.connect
    send_cmd( socket, cmd )
    read_ack( socket )
    socket.close
  end # remote_cmd

  ## read reply from XMMS
  def read_reply( socket )
    reply = socket.read( 8 ).unpack("SSI")
    if reply[2] > 0 then
      data = socket.read( reply[2] )
    end
    return data
  end # read_reply()

  ## receive ack (empty) packet from XMMS
  def read_ack(socket)
    read_reply( socket )
    nil
  end # read_ack

end

if $0 == __FILE__ then

  xmms = XMMS.new

end # if $0 == __FILE__
