xchat 2.x python plugin example

overview

You can think of this as a very short 'howto' on xchat scripting in python, or as a short mostly code-based tutorial. Below is a very simple but non-trivial xchat script written in python. If you know python already, looking at the source should be enough to get a hold for most of the xchat API that you will need to write scripts. If you don't know python, then the below should give you a good idea on how to structure your xchat script and some insights into some of the more popular paradigms in the python programming language. Unfortunately, though, teaching python is outside of the scope of this page and I'll only explain what I feel might not be totally self explanatory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#!/usr/bin/env python

import sys, re
import os
import xchat
from subprocess import *

__module_name__ = "banshee announce"
__module_version__ = "0.1"
__module_description__ = "banshee announce"

def get_output(runstr):
	return Popen(runstr.split(), stdout=PIPE).communicate()[0]

def is_running():
	p1 = Popen(["ps", "-ef"], stdout=PIPE)
	p2 = Popen(["grep", "banshee"], stdin=p1.stdout, stdout=PIPE)
	output = p2.communicate()[0]
	if output.rfind("banshee.exe") == -1: return False
	return True

def banshee_title(): return get_output("banshee --hide-field --query-title").strip()
def banshee_artist(): return get_output("banshee --hide-field --query-artist").strip()
def banshee_position(): return get_output("banshee --hide-field --query-position").strip()
def banshee_album(): return get_output("banshee --hide-field --query-album").strip()
def banshee_duration(): return get_output("banshee --hide-field --query-duration").strip()

def banshee_stop(): get_output("banshee --pause")
def banshee_play(): get_output("banshee --play")
def banshee_pause(): banshee_stop()
def banshee_next(): get_output("banshee --next")
def banshee_prev(): get_output("banshee --previous")
def banshee_eject(): get_output("banshee --show")

class mp3:
	def __init__(self):
		self.title = banshee_title()
		self.artist = banshee_artist()
		self.album = banshee_album()
		self.elapsed = int(banshee_position())
		self.time = "%2d:%02d" % (self.elapsed/60, self.elapsed % 60)
		self.ulen = int(banshee_duration())
		self.len = "%2d:%02d" % (self.ulen/60, self.ulen % 60)

	def __str__(self):
		return "%s - [%s] - %s ~ [%s] of [%s]" % (self.artist, self.album, self.title, self.time, self.len)


def announce():
	current = mp3()
	xchat.command('me is listening to: %s' % (current))

def help():
	print "\nCommands:"
	print "   \002/mp3\002         : announce the currently playing mp3"
	print "   \002/mp3\002     \00303stop\003 : stop playing"
	print "   \002/mp3\002     \00303play\003 : start playing"
	print "   \002/mp3\002    \00303pause\003 : pause playback"
	print "   \002/mp3\002 \00303next [#]\003 : skip to next (# of) track(s)"
	print "   \002/mp3\002 \00303prev [#]\003 : skip to prev (# of) track(s)"
	print "   \002/mp3\002     \00303open\003 : open files"
	print ""

def xchat_stop(): banshee_stop()
def xchat_play(): banshee_play()
def xchat_pause(): banshee_pause()
def xchat_next(): banshee_next()
def xchat_prev(): banshee_pref()
def xchat_eject(): banshee_eject()

def dispatch(argv, arg_to_eol, c):
	if len(argv) == 1:
		announce()
		return xchat.EAT_XCHAT
	try:
		{
		"help" : help,
		"stop" : xchat_stop,
		"play" : xchat_play,
		"pause" : xchat_pause,
		"next" : xchat_next,
		"prev" : xchat_prev,
		"eject" : xchat_eject,
		"open" : xchat_eject,
	}[argv[1]]()
	except:
		usage()
	return xchat.EAT_XCHAT

__unhook__ = xchat.hook_command("mp3", dispatch, help="/mp3 help for commands.")

xchat API

module globals [ lines 8-10 ]

These lines define global private variables __module_name__, __module_version__, __module_description__. When you look at the scripts window in xchat, your script will appear with this name, version, and description. Xchat actually expects these to be defined, and will error if they aren't. Some of the reloading functionality works off of the 'name', so making the name something simple is probably a good idea.

the hook [ line 90 ]

xchat.hook_command() is how you set a new 'slash' command in xchat. In this case, we are setting a new command called 'mp3' which, when used, will call the function dispatch, and has a help string "/mp3 help for commands." To use this command, a user would type something like: "/mp3 pause" into xchat.

xchat.EAT_XCHAT [ lines 74, 88 ]

Callbacks for commands registered with xchat.hook_command can return different constants depending on what you want to happen with the command. If you return xchat.EAT_XCHAT, you are telling xchat that you have already done all of the processing on this command and you do not want anything further to happen to this event. This is good for when you are registering your own command, but when you are capturing events (such as DCC events, say), you will want xchat to carry on handling the event after you've done your processing.

functions

is_running() [ lines 15-20 ]

The is_running function checks to see if banshee is running. Normally, you would want to filter out the "grep banshee" result, but in this case, since banshee actually runs as "banshee.exe", it isn't important. This function is not used in the code for simplicity, mostly because the banshee --option commands will not choke even if banshee is not running. However, you would probably want to use this to make sure that, if banshee is not running, you do not try to run the announce function.

xchat_*

The purpose of these seemingly useless functions is to provide a mechanism for enhancement in the future. Right now they simply call banshee functions, but in the future this architecture could be easily expanded in order to support multiple players (and in fact, that is what I eventually did.

dispatch(argv, arg_to_eol, c) [ lines 71-88 ]

In most of my xchat modules, I use something similar to this to dispatch command arguments out to different functions. This isolates dispatching errors and command logic errors from eachother, and also lets me use virtually the same code for dispatching across multiple scripts.

Lines 75-85 might seem a little strange to a python beginner (or even python intermediates), but it is a quasi-popular method of getting switch statement behavior out of python. It uses the built in dictionary literal syntax to build a dictionary of function objects, and then executes an a function with the subsequent arguments based on the command key. In english, suppose the following is executed by the user:

    /mp3 next

This will call the dispatch function with argv = ['mp3', 'next']. If the length of this list is 1 (ie; we have just called '/mp3'), then we automatically run the announce function. Since the length in this case would be 2, we go into the try block. argv[1] in this case is our "subcommand"; that is, /mp3 takes a number of arguments that themselves are commands, and the one we are using is 'next'. Since, in this dictionary, the key "next" maps to the function object xchat_next, {}[argv[1]] is actually going to be the 'xchat_next' function reference, and {..}[argv[1]]() is going to execute that function in response. If the subcommand is not in the dictionary, we'll get an invalid key exception, and print out the usage.

This is a generally useful organization to dispatch subcommands off of a single hooked command and allows you to cleanly separate the logic of actually carrying out the command and figuring out which one it is the user has requested. Also note that, by passing 'argv', we can let the subcommands parse their own arguments.