;;; workbone.el --- Workbone interface for the Emacs editor

;; Copyright (C) 2000 by Benjamin Drieu

;; Author: Benjamin Drieu <bdrieu@april.org>
;; Keywords: tools

;; This file is NOT part of GNU Emacs.

;; This program as GNU Emacs are free software; you can redistribute
;; them and/or modify them 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.

;; They are distributed in the hope that they 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 them; see the file COPYING.  If not, write to the Free
;; Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
;; 02111-1307, USA.

;;; Commentary:

;;  $Id: workbone.el,v 1.1.1.1 2002/07/01 17:04:37 benj Exp $

;;  You need workbone to use this program.  You also need the volume
;;  program to be able to change volume with keystrokes.  If you have
;;  a Internet connection and cddb.pl, you will be able to have tracks
;;  titles on the mode libe (which is *cool*).
;;
;;  Workbone : /ftp@sunsite.unc.edu:/pub/Linux/apps/sound/cdrom/curses
;;  Volume :   /ftp@sunsite.unc.edu:/pub/Linux/apps/sound/soundcard
;;  cddb.pl :  http://armin.emx.at/cddb/

;;  Usage is basicly the same than the traditionnal workbone, which is
;;  driven by the numeric pad.  The only difference is that all
;;  commands are prefixed by C-x w (w stands for workbone).
;;
;;  +----| number pad |----+
;;  |                      |
;;  |    []    ||    =>    |    7	 	8		9
;;  |                      |	^ Stop		^ Pause /Resume	^ Play
;;  |    <     ^^     >    |	4		5		6
;;  |                      |	^ Play previous	^ Restart	^ Play next
;;  |    <<    ..    >>    |	1 		2 		3
;;  |                      |	^ Back 15 '	^ Eject		^ Forward 15 '
;;  |    quit         ?    |	0   	  	 		.
;;  |                      |
;;  +----------------------+
;;                              C-x w + :   Set the sound up
;;                              C-x w - :   Set the sound down
;;				C-x w p n : Play track number n

;; $Log: workbone.el,v $
;; Revision 1.1.1.1  2002/07/01 17:04:37  benj
;; - version initiale
;;
;; Revision 1.11  2002/05/18 21:34:43  benj
;; - now (provide 'workbone)
;;
;; Revision 1.10  2002/05/05 13:26:40  benj
;; - fix stuff tu make it work with emacs21
;;
;; Revision 1.9  2000/11/20 10:46:24  benj
;; - did some cleanup
;;
;; Revision 1.8  2000/06/29 12:38:45  drieu
;;  - fiabilisation de workbone-init-cddb (si on ne trouve pas le titre,
;;    on ne va pas scanner la base de donnees a nouveau).
;;
;; Revision 1.7  1999/11/06 14:10:10  benj
;; - Fix a bug with workbone-eject
;;
;; Revision 1.6  1999/11/06 13:58:55  benj
;; - now use global-mode-string instead of an ugly dance
;; - fix soem bugs
;; - workbone-mode now knows how to behave when its workbone process is
;;   killed
;; - English typos fixed
;;

;;; Code:

;;  Custom variables (may be set by the user)

(defgroup workbone nil 
  "CD player for GNU Emacs."
  :tag "Workbone"
  :group 'applications)

(defcustom workbone-command "workbone"
  "*Program that is lauched to act with the audio cd"
  :group 'workbone
  :type '(string))

(defcustom workbone-options "-aq"
  "*Command added when the workbone command is launched"
  :group 'workbone
  :type '(string))

(defcustom workbone-volume-command "volume"
  "*Program that is lauched when the user wants to change volume"
  :group 'workbone
  :type '(string))

(defcustom workbone-cddb.pl "cddb.pl"
  "Program used to extract track names"
  :group 'workbone
  :type '(string))

(defcustom workbone-volume-string-to-match "Current volume: R = \\([0-9]*\\), L = \\([0-9]*\\)"
  "String to match to extract volume"
  :group 'workbone
  :type '(string))

(defcustom workbone-stop-string-to-match "stopped\|quit\|1A"
  "String to match when workbone says he stop"
  :group 'workbone
  :type '(string))

(defcustom workbone-track-number-to-match "[a-z]* #\\([0-9]*\\)"
  "*String that is matched when guessing track number
You may change it if workbone is improved by its author"
  :group 'workbone
  :type '(string))

(defcustom workbone-volume-number 40 "Volume used"
  :group 'workbone
  :type '(integer))

(defcustom workbone-volume-step 5
  "*Amount of steps that are added or substracted each time the volume is changed"
  :group 'workbone
  :type '(integer))

(defcustom workbone-stop-string "7" 
  "*String used to stop the program"
  :group 'workbone
  :type '(string))

(defcustom workbone-pause-string "8" 
  "*String used to pause the audio playing"
  :group 'workbone
  :type '(string))

(defcustom workbone-resume-string "8"
  "*String used to resume the ausin playing (same as workbone-pause-string)"
  :group 'workbone
  :type '(string))

(defcustom workbone-eject-string "2" 
  "*String used to eject the CD"
  :group 'workbone
  :type '(string))

(defcustom workbone-backward-string "1" 
  "*String used to go back 15'"
  :group 'workbone
  :type '(string))

(defcustom workbone-forward-string "3" 
  "*String used to go forward 15'"
  :group 'workbone
  :type '(string))

(defcustom workbone-play-string "9" 
  "*String used to start playing"
  :group 'workbone
  :type '(string))

(defcustom workbone-play-next-string "6" 
  "*String used to play next track"
  :group 'workbone
  :type '(string))

(defcustom workbone-play-previous-string "4"
  "*String used to play previous track"
  :group 'workbone
  :type '(string))

(defcustom workbone-restart-string "5"
  "*String used to restart playing one track"
  :group 'workbone
  :type '(string))

(defcustom workbone-quit-string "0"
  "*String used to quit the interface without stopping the music"
  :group 'workbone
  :type '(string))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;  Variables used by workbone (believe me, you don't want to know what they do !)

(defvar workbone-track-number 0 "Track number")
(defvar workbone-process nil "Workbone process")
(defvar workbone-track-list nil)
(defvar workbone-mode nil)
(defvar workbone-string "")
(defvar workbone-paused nil)
(defvar workbone-volume-buffer "*Temporary volume buffer*")

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;  Keystrokes used by the workbone interface (hmmm... seems ugly to
;;  me, need more work)

(define-key (current-global-map) "\C-xw+" 'workbone-volume-up)
(define-key (current-global-map) "\C-xw-" 'workbone-volume-down)
(define-key (current-global-map) "\C-xw7" 'workbone-stop)
(define-key (current-global-map) "\C-xw8" 'workbone-pause-or-resume)
(define-key (current-global-map) "\C-xw9" 'workbone-play)
(define-key (current-global-map) "\C-xw5" 'workbone-restart)
(define-key (current-global-map) "\C-xw4" 'workbone-play-previous)
(define-key (current-global-map) "\C-xw6" 'workbone-play-next)
(define-key (current-global-map) "\C-xw1" 'workbone-backward)
(define-key (current-global-map) "\C-xw2" 'workbone-eject)
(define-key (current-global-map) "\C-xw3" 'workbone-forward)
(define-key (current-global-map) "\C-xw0" 'workbone-quit)
(define-key (current-global-map) "\C-xwp" 'workbone-play-nth)
(define-key (current-global-map) "\C-xwv" 'workbone-set-volume)


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;  Fun stuff : all the code

;;;###autoload
(defun workbone ()
  "Play an audio cd using workbone."
  (interactive)
  (workbone-mode 1))


(defun workbone-mode (&optional arg)
  "Toggle reading of an audio cd using workbone-mode.

With a numeric argument, enable it if arg is positive."
  (interactive)
  (if
      (if (null arg) (not workbone-mode)
	(> (prefix-numeric-value arg) 0))
      (workbone-enable)
    (workbone-disable)))


(defun workbone-enable ()
  "Enable reading of an audio cd using workbone-mode."
  (interactive)
  (setq workbone-mode t)
  (or (assq 'workbone-mode minor-mode-alist)
      (setq minor-mode-alist
	    (cons minor-mode-alist '((workbone-mode workbone-string)))))
  (if (workbone-not-running)
      (if (not (workbone-start-process))
	  (progn
	    (princ "Workbone is not installed")
	    nil)
	(setq workbone-track-number 0)
	(workbone-set-volume workbone-volume-number)))
  (workbone-play))


(defun workbone-disable ()
  "Disable reading of an audio cd using workbone-mode."
  (interactive)
  (setq workbone-mode nil)
  (workbone-quit)
  (setq global-mode-string (delete 'workbone-string global-mode-string)))


(defun workbone-start-process ()
  "Launch the workbone process"
  (interactive)
  (setq workbone-process (start-process workbone-command nil 
					workbone-command workbone-options))
  (set-process-filter workbone-process 'workbone-filter))


(defun workbone-not-running ()
  "State if workbone is already running"
  (equal nil (get-process workbone-command)))


(defun workbone-send-request (request &optional n)
  "Send a request to the workbone process"
  (interactive "sRequest to send: ")
  (process-send-string (process-name workbone-process) request)
  (if (and n (> n 1))
      (workbone-send-request request (1- n))))


(defun workbone-change-status (string)
  "Change process status variable"
  (setq workbone-status string))


(defun workbone-play ()
  "Start playing"
  (interactive)
  (when workbone-mode
    (if (workbone-not-running)
	(workbone-start-process))
    (workbone-change-mode-line "Playing")
    (workbone-send-request workbone-play-string))
  (workbone-init-cddb))


(defun workbone-restart ()
  "Restart playing a track"
  (interactive)
  (workbone-init-cddb)
  (when workbone-mode
    (if (workbone-not-running)
	(workbone-start-process))
    (workbone-change-mode-line "Playing")
    (workbone-send-request workbone-restart-string)))


(defun workbone-play-next (&optional n)
  "Play next track"
  (interactive "P")
  (when workbone-mode
    (if (workbone-not-running)
	(workbone-start-process))
    (workbone-change-mode-line)
    (workbone-send-request workbone-play-next-string n)))


(defun workbone-play-previous (&optional n)
  "Play previous track"
  (interactive "P")
  (when workbone-mode
    (if (workbone-not-running)
	(workbone-start-process))
    (workbone-change-mode-line)
    (workbone-send-request workbone-play-previous-string n)))


(defun workbone-quit ()
  "Quit the workbone interface, and kill some stuff"
  (interactive)
  (setq workbone-track-list nil)
  (when workbone-mode
    (if (not (workbone-not-running))
	(and
	 (workbone-send-request workbone-stop-string)
	 (workbone-send-request workbone-quit-string)))))


(defun workbone-stop ()
  "Stop playing"
  (interactive)
  (when workbone-mode
    (when (workbone-not-running)
      (workbone-start-process))
    (setq workbone-track-list nil)
    (workbone-change-mode-line "Stopped")
    (workbone-send-request workbone-stop-string)))


(defun workbone-pause-or-resume ()
  "Pause or resume reading"
  (interactive)
  (when workbone-mode
    (if (workbone-not-running)
	(workbone-start-process))
    (if workbone-paused
	(progn
	  (setq workbone-paused nil)
	  (workbone-change-mode-line "Playing")
	  (workbone-send-request workbone-resume-string))
      (setq workbone-paused t)
      (workbone-change-mode-line "Pause")
      (workbone-send-request workbone-pause-string))))


(defun workbone-backward (&optional n)
  "Go backward 15 ' (or more)"
  (interactive "P")
  (when workbone-mode
    (if (workbone-not-running)
	(workbone-start-process))
    (workbone-send-request workbone-backward-string n)))
   

(defun workbone-eject ()
  "Eject the audio cd"
  (interactive)
  (when workbone-mode
    (if (workbone-not-running)
	(workbone-start-process))
    (workbone-send-request workbone-eject-string)
    (setq workbone-track-number 0)
    (setq workbone-track-list nil)
    (workbone-change-mode-line "Stopped")))


(defun workbone-forward (&optional n)
  "Go forward 15'' (or more)"
  (interactive "P")
  (when workbone-mode
    (if (workbone-not-running)
	(workbone-start-process))
    (workbone-send-request workbone-forward-string n)))


(defun workbone-play-nth (&optional n)
  "Play Nth track"
  (interactive "nPlay track: ")
  (when workbone-mode
    (if (> workbone-track-number n)
	(workbone-play-previous (- workbone-track-number n))
      (workbone-play-next (- n workbone-track-number)))))


(defun workbone-track-format ()
  (format "%d %s" workbone-track-number workbone-status))


(defun workbone-filter (proc string)
  "Filter of the workbone processus"
  (cond
   ((string-match workbone-track-number-to-match string)
    (let ((track-string (match-string 1 string)))
      (if (null track-string)
	  nil
	(setq local-track-number (string-to-number track-string))
	(if (not (equal local-track-number workbone-track-number))
	    (progn
	      (setq workbone-track-number local-track-number)
	      (workbone-change-mode-line))
	  nil))))
   ((string-match workbone-stop-string-to-match string))))


(defun workbone-change-mode-line (&optional arg)
  (when arg
    (setq workbone-status arg))
  (setq workbone-string (format " [ %s | %s ] " (workbone-make-title workbone-track-number) workbone-status))
  (force-mode-line-update))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;; CDDB interface - you need cddb.pl to do that

(defun workbone-init-cddb ()
  (interactive)
  (unless (not (null workbone-track-list))
    (let ((result (shell-command-to-string workbone-cddb.pl)))
      (setq workbone-track-list
	    (if (string-match "no cddb entry found" result)
		'(("artist" . "Unknown artist"))
	      (workbone-parse-result result 0))))))


(defun workbone-parse-result (string start)
  (if (string-match "\\(track \\([0-9]\+\\)\\|artist\\|title\\): \\([^\n]+\\)" string start)
      (let ((name (or (match-string 2 string) (match-string 1 string))))
	(cons (cons name (match-string 3 string)) (workbone-parse-result string (match-end 3))))
    nil))


(defun workbone-make-title (&optional n)
  (format "%s: %s"
	  (or (cdr (assoc "artist" workbone-track-list))
	      "Unknown artist")
	  ;; (cdr (assoc "title" workbone-track-list))
	  (or (cdr (assoc (format "%d" n) workbone-track-list))
	      "Unknown title")))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;; Volume control - you need volume to do that

(defun workbone-guess-volume ()
  "Guess the current volume"
  (interactive)
  ;;; Why not (start-process) ?
  (call-process workbone-volume-command nil 
		(get-buffer-create workbone-volume-buffer) nil)
  (set-buffer (get-buffer workbone-volume-buffer))
  (string-match workbone-volume-string-to-match (buffer-string))
  (setq workbone-volume-number (string-to-number 
				(match-string 1 (buffer-string))))
  (kill-buffer (get-buffer workbone-volume-buffer)))


(defun workbone-prompt-for-volume ()
  "Read volume from input"
  (let ((n (string-to-int 
	    (read-input 
	     (format "Set volume to (default %d): " workbone-volume-number)))))
    (if n n workbone-volume-number)))
      

(defun workbone-set-volume (&optional n)
  "Set volume"
  (interactive (list (workbone-prompt-for-volume)))
  (when (not n)
    (setq n 1))
  (if (and 
       (> n 0)
       (< n 100))
      (setq workbone-volume-number n))
  (workbone-update-volume))


(defun workbone-volume-down (&optional n)
  "Set volume higher"
  (interactive "P")
	(workbone-guess-volume)
  (when (not n)
    (setq n 1))
  (if (> workbone-volume-number 0)
      (setq workbone-volume-number (- workbone-volume-number 
				      (* n workbone-volume-step))))
  (workbone-update-volume))


(defun workbone-volume-up (&optional n)
  "Set volume lower"
  (interactive "P")
  (workbone-guess-volume)
  (when (not n)
    (setq n 1))
  (if (< workbone-volume-number 100)
      (setq workbone-volume-number (+ workbone-volume-number 
				      (* n workbone-volume-step))))
  (workbone-update-volume))


(defun workbone-update-volume ()
  (start-process workbone-volume-command nil workbone-volume-command
		 (number-to-string workbone-volume-number)))

(provide 'workbone)

;;; workbone.el ends here
