root/xchat/xdccq.py

Revision 37, 12.6 kB (checked in by jmoiron, 1 year ago)
  • fix stupid bug in xdccq
  • replace 'which' command in mp3.py w/ one that is 5.88x faster (remove more forks)
Line 
1 #!/usr/bin/env python
2
3 # written by jmoiron, jmoiron.net
4 # licensed under the GNU GPL v2
5 # a copy of the license can be found:
6 #   http://www.gnu.org/licenses/gpl.txt
7
8 import xchat
9 import sys
10 import re
11 from exceptions import ValueError
12
13 __module_name__ = "xdccq"
14 __module_version__ = "0.4"
15 __module_description__ = "Xdcc Queue"
16
17 # to enable some debugging output, set to True
18 __debugging__ = False
19
20 if __debugging__:
21     import traceback
22
23 def print_debug(string):
24     global __debugging__
25     if __debugging__:
26         string = str(string)
27         print "\00302" + string + "\003"
28
29 def print_error(string):
30     string = str(string)
31     print "\00304" + string + "\003"
32
33 def print_help(string):
34     string = str(string)
35     print "\00302" + string + "\003"
36
37 def print_success(string):
38     string = str(string)
39     print "\00303" + string + "\003"
40
41 def echo(string):
42     string = str(string)
43     print "\00300" + string + "\003"
44
45 print_info = print_success
46
47 cmd_help = {}
48 gen_help = [
49     "\037[help, ?]\037 <cmd>    - prints this help or detailed help for 'cmd'\n",
50     "\037[ls, list]\037         - lists files in queue\n",
51     "\037get\037 [bot] [#, #-#] - adds 'send' cmds to queue\n",
52     "\037rm\037 [bot] <#, #-#>  - removes 'send' cmds from queue\n",
53     "\037[cancel,stop]\037      - cancles current transfer",
54 ]
55 cmd_help['msg'] = "   " + "   ".join(gen_help)
56 cmd_help['?'] = """\n\002/xdccq\002 \037[help, ?]\037 <cmd>\n   \00303By itself, help/? prints out the available commands along withshort descriptions of each.  If you supply a command \002cmd\002, a longer help description of that command and its usage is given.\003"""
57 cmd_help['ls'] = """\n\002/xdccq\002 \037[ls, list]\037\n   \00303Lists the file gets currently in the queue.\00303"""
58 cmd_help['get'] = """\n\002/xdccq\002 \037get\037 [bot] [#, #-#]\n   \00303Adds commands to the local queue "/ctcp \037bot\037 xdcc send \037#\037".  A number ["7"], a number range ["1-5"], or a comma separated list ["7, 8-11, 15"] may be given.\003"""
59 cmd_help['rm'] = """\n\003/xdccq\002 \037rm\037 [bot] <#, #-#>\n   \00303Removes commands from local queue.  If only \037bot\037 is supplied, it removes all commands dealing with \037bot\037.  If it is supplied with a number or number range, any commands in that range are removed.   If you had numbers "8, 9, 11, 12, 14" queued, and removed "8-14", it would remove all of those package numbers for that bot without resulting in an error for the missing "10" and "13".\003\n   \00303New in 0.3: rm will now also call 'cancel' if the active transfer is from [bot] and the optional range argument was not given.\003"""
60 cmd_help['cancel'] = """\n\003/xdccq\002 \037cancel\037\n   \00303If a DCC handled by xdccq is currently active, it cancels that transfer and starts the next one on the queue.  If the 'active' transfer is remotely queued, xdccq attempts to unqueue it by issuing a '/msg <bot> xdcc remove' command.  If this command fails, this transfer will continue to remotely pend; take notice!\003"""
61
62 # converts a string like "3,5,7-9,14" into a list
63 # raises InvalidRange
64 # raises InvalidInteger
65 def numToList(string):
66     ret = []
67     numsplit = string.split(",")
68     # the following code makes nums into a list of all integers
69     for n in numsplit:
70         nr = n.split('-')
71         # handle the case of a single number
72         if len(nr) == 1:
73             try: ret.append(int(n))
74             except: raise ValueError("number")
75         # handle the case of a range
76         elif len(nr) == 2:
77             try:
78                 low = int(nr[0])
79                 high = int(nr[1]) + 1
80                 if low > high: raise ValueError("number")
81                 ret += range(low, high)
82             except ValueError: raise ValueError("number")
83         else: raise ValueError("range")
84     return ret
85
86 class Command:
87     def __init__(self, bot, num):
88         self.bot = str(bot)
89         self.num = int(num)
90         self.channel = xchat.get_info("channel")
91         self.network = xchat.get_info("network")
92         self.context = xchat.get_context()
93         self.file = ""
94         self.retries = 0
95         self.queue_position = 0
96         self.queued = False
97         self.transfering = False
98         self.dead = False
99         self.s = "ctcp %s xdcc send #%d" % (str(bot), int(num))
100
101     def __str__(self):
102         return "#%d on %s (%s)" % (self.num, self.bot, self.channel)
103
104 class Queue:
105     def __init__(self):
106         self.data = []
107    
108     def put(self, item):
109         self.data.append(item)
110    
111     def get(self):
112         tmp = self.data[0]
113         del self.data[0]
114         return tmp
115    
116     def __getitem__(self, key):
117         return self.data[key]
118    
119     def __delitem__(self, key):
120         del self.data[key]
121    
122     def __len__(self):
123         return len(self.data)
124
125     def __iter__(self):
126         return self.data.__iter__()
127
128 class cmdQueue(Queue):
129     """Utility functions for dealing with a queue of Commands"""
130
131     def getBotSet(self, botname, r=[]):
132         packs = [cmd for cmd in self.data if cmd.bot == botname]
133         # if supplied a range, cut list to those in the range
134         if r: packs = [cmd for cmd in packs if cmd.num in r]
135         return packs
136
137     def removeBot(self, botname, r=[]):
138         packs = self.getBotSet(botname, r)
139         for cmd in packs:
140             self.data.remove(cmd)
141         # this might be better as the actual list someday?
142         return len(packs)
143        
144
145 CommandQueue = cmdQueue()
146 Active = False
147 Watchdog = False
148
149 # help, ?  -- print out explanations of commands
150 def help(a):
151     global cmd_help
152     if len(a) == 3:
153         if cmd_help.has_key(a[2]):
154             print_help(cmd_help[a[2]])
155             return
156     usage()
157     print cmd_help['msg']
158
159 # ls, list -- print out a list of files in the queue
160 def ls(a):
161     global CommandQueue, Active
162     s = "No files being transfered."
163     if Active:
164         if Active.transfering:
165             dcc_list = xchat.get_list("dcc")
166             cps = "Unknown CPS"
167             for dcc_item in dcc_list:
168                 if str(dcc_item.nick).lower() == Active.bot.lower():
169                     cps = "%s CPS" % (str(dcc_item.cps))
170             s = "Pack #%d from %s on %s@%s being transfered at %s." % (Active.num, Active.bot, Active.channel, Active.network, cps)
171         elif Active.queued: s = "Pack #%d from %s on %s@%s queued remotely [position %d]." % (Active.num, Active.bot, Active.channel, Active.network, Active.queue_position)
172     if len(CommandQueue) == 0:
173         print_info("No files in the queue.  " + s)
174     else:
175         print_info(s)
176         for f in CommandQueue:
177             print_info(f)
178
179 # get -- add commands to the queue
180 def get(a):
181     if len(a) != 4:
182         print_error("Error: invalid arguments for get.  /xdccq get [bot] [#, #-#]")
183         return
184     bot = str(a[2])
185     try: nums = numToList(a[3])
186     except ValueError, exc:
187         # exc.args[0] is in the set ['number', 'range']
188         print_error("Error: %s contains invalid %s." % (a[3], exc.args[0]))
189         return
190     print_info("adding packs: %s" % (str(nums)))
191     for num in nums:
192         CommandQueue.put(Command(bot, num))
193     # if we actually added something, try to start this party
194     if len(nums):
195         run()
196
197 # rm -- remove commands from the queue 
198 def rm(a):
199     global CommandQueue, Active
200     if len(a) not in (3, 4):
201         print_error("Error: invalid arguments for 'rm'.  /xdccq rm [bot] <#, #-#>")
202     items_to_delete = []
203     if len(a) == 4:
204         try: items_to_delete = numToList(a[3])
205         except ValueError, exc:
206             # exc.args[0] is in the set ['number', 'range']
207             print_error("Error: %s contains invalid %s." % (a[3], exc.args[0]))
208             return
209     # if we removed all from bot, and the bot is currently transfering...
210     removed = CommandQueue.removeBot(a[2], items_to_delete)
211     if Active and Active.bot == a[2] and len(a) == 3: cancel()
212     print_info("deleted %d commands from the queue." % removed)
213
214 # we won't be needing a, and it'l clean the call elsewhere
215 def cancel(a=[]):
216     global Active
217     if not Active:
218         print_error("No currently transfering packages.")
219         return
220     if Active.transfering:
221         s = "dcc close get %s %s" % (Active.bot, Active.file)
222         print_debug("/%s" % (s))
223         Active.context.command(s)
224     elif Active.queued:
225         s = "msg %s xdcc remove" % (Active.bot)
226         print_debug("/%s" % (s))
227         Active.context.command(s)
228     Active = False
229     run()
230
231 USAGE_STR = "Usage: \002/xdccq\002 [cmd] [args], \002/xdccq\002 \037help\037 for commands."
232
233 def usage():
234     print USAGE_STR
235
236 def dispatch(argv, arg_to_eol, c):
237     print_debug(argv)
238     echo("/" + str(arg_to_eol[0]))
239     try:
240         {
241         "help"    : help,
242         "?"       : help,
243         "ls"      : ls,
244         "list"    : ls,
245         "get"     : get,
246         "rm"      : rm,
247         "cancel"  : cancel,
248         "stop"    : cancel,
249     }[argv[1]](argv)
250     except:
251         if __debugging__: traceback.print_exc(sys.stdout)
252         usage()
253     return xchat.EAT_XCHAT
254
255 def retryTransfer():
256     if Active.retries < 3:
257         Active.context.command(Active.s)
258         Active.retries += 1
259         return True
260     return False
261
262 # watchdog callback
263 def transferCheck(data):
264     global Active
265     # if active has already finished, stop the timer
266     if not Active:
267         return False
268     elif Active.transfering or Active.queued:
269         return False
270     elif not Active.dead:
271         Active.dead = True
272         return True
273     else:
274         ret = retryTransfer()
275         if ret:
276             print_error("Previous attempt to get file (#%d from %s) seems to have failed.  Repeating (%d of 3 retries)" % (Active.num, Active.bot, Active.retries))
277         return ret
278        
279 def run():
280     global CommandQueue, Active, Watchdog
281     if Active: return
282     if len(CommandQueue):
283         cmd = CommandQueue.get()
284         Active = cmd
285         print_debug("/%s on %s@%s" % (cmd.s, cmd.channel, cmd.network))
286         Active.context.command(Active.s)
287         # set up a watchdog every 45 seconds
288         Watchdog = xchat.hook_timer(45000, transferCheck)
289
290 """ some other messages are possible:
291 ** Closing Connection: Unable to transfer data (Broken pipe)
292 ** Closing Connection: DCC Timeout (180 Sec Timeout)
293 """
294 def notice(split, full, data):
295     global CommandQueue, Active
296     # ['jonas|srvr', 'Total Offered: 0.3 MB  Total Transferred: 0.30 MB']
297     botname = split[0]
298     message = split[1]
299     if not Active: return
300     if Active.queued: return
301     if Active.bot == botname:
302         re_queued = re.compile(r"queue")
303         re_pos = re.compile(r"position [0-9]+")
304         if re_queued.search(message.lower()):
305             Active.queued = True
306             print_info("xdccq detected the active file being placed on a remote queue")
307         if re_pos.search(message.lower()):
308             try:
309                 res = re_pos.search(message.lower())
310                 postr = message[res.start() : res.end()]
311                 pos = int(postr.split()[1])
312                 Active.queue_position = pos
313             except:
314                 Active.queue_position = 0
315                 print_error("an error occured parsing the remote queue position")
316        
317 def dccComplete(split, full, data):
318     # ['just4u.txt', '/home/jmoiron/.xchat2/downloads/just4u.txt.1', 'just|4|u', '38373']
319     global CommandQueue, Active
320     if not Active: return
321     #print_debug(str(split))
322     if Active.bot == str(split[2]):
323         # we assume it was our file
324         print_info("Received file \"%s\" from %s on %s@%s." % (Active.file, Active.bot, Active.channel, Active.network))
325         Active = False
326     run()
327
328 def dccConnect(split, full, data):
329     global CommandQueue, Active
330     if not Active: return
331     #print_debug(str(split))
332     botname = split[0]
333     if Active.bot == str(split[0]):
334         print_info("Requested file \"%s\" is being sent." % (split[2]))
335         Active.file = split[2]
336         Active.queued = False
337         Active.transfering = True
338
339 def dccStall(split, full, data):
340     global CommandQueue, Active
341     if not Active: return
342     if Active.bot == str(split[2]):
343         print_error("Requested file \"%s\" has stalled during transport." % (split[1]))
344         ret = retryTransfer()
345         if ret:
346             print_info("Re-requesting file \"%s\" (%d of 3 retries)" % (split[1], Active.retries))
347         else:
348             print_error("Retry limit reached for file \"%s\".  Stopping the queue." % (split[1]))
349
350 __unhook__ = xchat.hook_command("xdccq", dispatch, help=USAGE_STR)
351
352 print_info("XdccQ-TNG loaded successfully")
353 usage()
354
355 noticeHook = xchat.hook_print("Notice", notice, "data")
356 dccRecvCompleteHook = xchat.hook_print("DCC RECV Complete", dccComplete, "data")
357 dccRecvConnectHook = xchat.hook_print("DCC RECV Connect", dccConnect, "data")
358 dccRecvStallHook = xchat.hook_print("DCC Stall", dccStall, "data")
Note: See TracBrowser for help on using the browser.