Update the torrent list every second.
[python-gnt.git] / example / delugent / delugent.py
blob540b4f5367631305a7d50125d2079d57733c056c
1 #!/usr/bin/env python
3 """
4 delugent : Frontend for deluge.
6 This is meant to work with deluge 0.60 (and above, I am sure), which has
7 a core-ui split. This is a work in progress.
9 Copyright (C) 2007 Sadrul Habib Chowdhury <sadrul@users.sourceforge.net>
11 This application is free software; you can redistribute it and/or
12 modify it under the terms of the GNU Lesser General Public
13 License as published by the Free Software Foundation; either
14 version 2.1 of the License, or (at your option) any later version.
16 This application is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 Lesser General Public License for more details.
21 You should have received a copy of the GNU Lesser General Public
22 License along with this application; if not, write to the Free Software
23 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301
24 USA
25 """
27 import deluge.common
28 import deluge.ui.client as client
29 from deluge.log import LOG as log
30 from deluge.configmanager import ConfigManager
32 import sys
33 import os.path
35 import gobject
37 gobject.threads_init()
39 import gnt
40 import signals
42 status_keys = {
43 "name" : "Name",
44 "save_path" : "Path",
45 "state" : "Status",
46 "tracker_status" : "Summary",
47 "total_size" : "Size",
48 "total_done" : "Downloaded",
49 "total_payload_upload" : "Uploaded",
50 "progress" : "Progress",
51 "num_seeds" : "# of seeds",
52 "total_seeds" : "Total seeds",
53 "num_peers" : "# of peers",
54 "total_peers" : "Total peers",
55 "eta" : "ETA",
56 "download_payload_rate" : "Download speed",
57 "upload_payload_rate" : "Upload speed",
58 "ratio" : "Share Ratio",
59 "distributed_copies" : "Availability",
60 "total_uploaded" : "Uploaded",
61 "num_files" : "# of files",
62 "num_pieces" : "# of pieces",
63 "piece_length" : "Piece length",
64 "files": "Files",
67 def show_size(size):
68 unit = "b"
69 if size > (1 << 30):
70 size = size >> 30
71 unit = "G"
72 elif size > (1 << 20):
73 size = size >> 20
74 unit = "M"
75 elif size > (1 << 10):
76 size = size >> 10
77 unit = "K"
78 return str(size) + unit
80 def show_speed(speed):
81 unit = "b"
82 if speed > (1 << 30):
83 speed = speed / (1 << 30)
84 unit = "G"
85 elif speed > (1 << 20):
86 speed = speed / (1 << 20)
87 unit = "M"
88 elif speed > (1 << 10):
89 speed = speed / (1 << 10)
90 uni = "K"
91 return ("%.01f" + unit) % speed
93 def show_state(state):
94 return deluge.common.TORRENT_STATE[state]
96 def compare_tor_row(row1, row2):
97 v1 = row1.row[TorRow.compare_column]
98 v2 = row2.row[TorRow.compare_column]
99 if not TorRow.compare_asc:
100 v1, v2 = v2, v1
101 if v1 < v2:
102 return -1
103 elif v1 > v2:
104 return 1
105 return 0
107 class TorRow(gobject.GObject):
108 COL_NAME = 0
109 COL_STATE = 1
110 COL_SIZE = 3
111 COL_PROG = 2
112 COL_SD = 4
113 COL_PR = 5
114 COL_DS = 6
115 COL_US = 7
116 COL_ETA = 8
117 COLS = 9
119 COMPARE_FUNC = compare_tor_row
120 compare_column = COL_NAME
121 compare_asc = True
123 def __init__(self, id):
124 self.__gobject_init__()
125 self.id = id
126 self.row = None
128 def __del__(self):
129 pass
131 def info(self):
132 status = client.get_torrent_status(self.id, status_keys.keys())
133 state = int(status.get("state", 0))
134 name = str(status.get("name", ""))
135 size = int(status.get("total_size", 0))
136 progress = float(status.get("progress", 0.0))
137 peers = str(status.get("num_peers", 0))
138 seeds = str(status.get("num_seeds", 0))
139 dlspeed = float(status.get("download_payload_rate", 0.0))
140 upspeed= float(status.get("upload_payload_rate", 0.0))
141 eta = float(status.get("eta", 0.0))
143 row = [""] * self.COLS
144 row[self.COL_NAME] = name
145 row[self.COL_STATE] = show_state(state)
146 row[self.COL_SIZE] = show_size(size)
147 row[self.COL_PROG] = str(progress)
148 row[self.COL_SD] = str(seeds)
149 row[self.COL_PR] = str(peers)
150 row[self.COL_DS] = show_speed(dlspeed)
151 row[self.COL_US] = show_speed(upspeed)
152 row[self.COL_ETA] = str(eta)
153 self.row = row
154 return row
156 gobject.type_register(TorRow)
158 class TorFileRow(gobject.GObject):
159 def decide_priority(self):
160 if not self.enabled:
161 return "Ignore"
162 if self.priorities[0] == 1:
163 return "Normal"
164 elif self.priorities[0] < 7:
165 return "Higher"
166 else:
167 return "Highest"
169 def __init__(self, file):
170 self.__gobject_init__()
171 self.path = file['path']
172 self.size = file['size']
173 self.pieces = len(file['priorities'])
174 self.priorities = file['priorities']
175 self.enabled = len([p for p in self.priorities if p > 0]) > 0
177 def info(self):
178 return [self.path, show_size(self.size), self.decide_priority()]
180 class TorList(gnt.Tree):
181 __gntbindings__ = {
182 'info' : ('show_info', 'i'),
183 'toggle_border' : ('toggle_border', 't'),
184 'change-sort' : ('change_sort_column', 'S'),
187 def __init__(self):
188 gnt.Tree.__init__(self)
189 self.set_property('columns', TorRow.COLS)
190 self.set_show_title(True)
191 self.separator = True
193 titles = {TorRow.COL_NAME : "Name",
194 TorRow.COL_STATE : "State",
195 TorRow.COL_SIZE : "Size",
196 TorRow.COL_PROG : "Progr",
197 TorRow.COL_SD : "Sd",
198 TorRow.COL_PR : "Pr",
199 TorRow.COL_DS : "DL Sp",
200 TorRow.COL_US : "Up Sp",
201 TorRow.COL_ETA : "ETA"
203 widths = {TorRow.COL_NAME : 30,
204 TorRow.COL_STATE : 7,
205 TorRow.COL_SIZE : 5,
206 TorRow.COL_PROG : 5,
207 TorRow.COL_SD : 3,
208 TorRow.COL_PR : 3,
209 TorRow.COL_DS : 6,
210 TorRow.COL_US : 6,
211 TorRow.COL_ETA : 6
214 # Set column titles
215 for col in titles:
216 self.set_column_title(col, titles[col])
217 # Set column widths and alignments
218 for col in widths:
219 self.set_col_width(col, widths[col])
220 for col in [TorRow.COL_ETA, TorRow.COL_SD, TorRow.COL_PR, TorRow.COL_DS, TorRow.COL_US, TorRow.COL_SIZE, TorRow.COL_PROG]:
221 self.set_column_resizable(col, False)
222 for col in [TorRow.COL_SIZE, TorRow.COL_DS, TorRow.COL_SD, TorRow.COL_PR, TorRow.COL_US, TorRow.COL_PROG]:
223 self.set_column_is_right_aligned(col, True)
225 def get_row(self, id):
226 tors = self.get_rows()
227 for tor in tors:
228 if tor.id == id:
229 return tor
230 return None
232 def remove_torrent(self, id):
233 tor = self.get_row(id)
234 if tor: self.remove(tor)
236 def add_torrent(self, id):
237 row = TorRow(id)
238 self.add_row_after(row, row.info(), None, None)
240 def update_torrent(self, id, tor = None):
241 if not tor:
242 tor = self.get_row(id)
243 info = tor.info()
244 for i in range(0, TorRow.COLS):
245 self.change_text(tor, i, info[i])
247 def update_all(self):
248 for tor in self.get_rows():
249 self.update_torrent(tor.id, tor)
251 def show_info(self, null):
252 tor = self.get_selection_data()
253 if not tor: return True
254 show_details(tor.id)
255 return True
257 def toggle_border(self, null):
258 self.separator = not self.separator
259 self.set_show_separator(self.separator)
260 self.draw()
261 return True
263 def change_sort_column(self, null):
264 def sort_col_cb(item, col):
265 if TorRow.compare_column == col:
266 TorRow.compare_asc = not TorRow.compare_asc
267 else:
268 TorRow.compare_column = col
269 rows = self.get_rows()
270 self.remove_all()
271 for row in rows:
272 self.add_row_after(row, row.info(), None, None)
273 menu = gnt.Menu(gnt.MENU_POPUP)
274 for label, column in (("Sort by Name", TorRow.COL_NAME),
275 ("Sort by Size", TorRow.COL_SIZE),
276 ("Sort by Progress", TorRow.COL_PROG),
277 ("Sort by Download Speed", TorRow.COL_DS),
278 ("Sort by ETA", TorRow.COL_ETA),
280 item = gnt.MenuItemCheck(label)
281 if TorRow.compare_column == column:
282 item.set_checked(True)
283 item.connect('activate', sort_col_cb, column)
284 menu.add_item(item)
285 gnt.show_menu(menu)
287 gobject.type_register(TorList)
288 gnt.register_bindings(TorList)
290 def setup_fileselector(fl, win):
291 def close_filesel(cancel, window):
292 window.destroy()
293 fl.cancel_button().connect('activate', close_filesel, win)
296 def show_details(tid):
297 """Show detailed information about a torrent."""
298 status = client.get_torrent_status(tid, status_keys.keys())
299 win = gnt.Box(vert = True, homo = False)
300 win.set_toplevel(True)
301 win.set_alignment(gnt.ALIGN_MID)
302 win.set_pad(0)
303 win.set_fill(True)
304 win.set_title("Details")
306 def add_info(info):
307 string = ""
308 tv = gnt.TextView()
309 for label, value in info:
310 label = "%15s: " % label
311 tv.append_text_with_flags(label, gnt.TEXT_FLAG_BOLD)
312 tv.append_text_with_flags(value + "\n", gnt.TEXT_FLAG_NORMAL)
313 string = string + label + value + "\n"
314 tv.set_flag(gnt.TEXT_VIEW_TOP_ALIGN | gnt.TEXT_VIEW_NO_SCROLL)
315 w, h = gnt.get_text_bound(string.strip())
316 tv.set_size(w + 3, h)
317 return tv
319 top = (
320 ("Name", "%s" % status['name']),
321 ("Path", "%s" % status['save_path']),
322 ("# of files", "%s" % status['num_files']),
323 ("Status", show_state(status['state'])),
326 left = (
327 ("Downloaded", "%s (%s)" % (deluge.common.fsize(float(status['total_done'])),
328 deluge.common.fsize(float(status['total_size'])))),
329 ("Download Speed", "%s" % deluge.common.fspeed(float(status['download_payload_rate']))),
330 ("Uploaded", "%s (%s)" % (deluge.common.fsize(float(status['total_uploaded'])),
331 deluge.common.fsize(float(status['total_payload_upload'])))),
332 ("Upload Speed", "%s" % deluge.common.fspeed(float(status['upload_payload_rate']))),
333 ("Pieces", "%s (%s)" % (status['num_pieces'], deluge.common.fsize(status['piece_length']))),
336 right = (
337 ("Seeders", "%s (%s)" % (status['num_seeds'], status['total_seeds'])),
338 ("Peers", "%s (%s)" % (status['num_peers'], status['total_peers'])),
339 ("ETA", "%s" % deluge.common.ftime(status['eta'])),
340 ("Share Ratio", "%.1f" % status['ratio']),
341 ("Availability", "%.1f" % status['distributed_copies']),
344 win.add_widget(add_info(top))
345 win.add_widget(gnt.Line(False))
346 win.add_widget(pack_widget(False, [add_info(left), add_info(right)]))
347 files = gnt.Tree()
348 files.set_property('columns', 3)
349 files.set_col_width(1, 5)
350 files.set_column_resizable(1, False)
351 files.set_col_width(2, 10)
352 files.set_show_title(True)
353 files.set_column_title(0, 'File')
354 files.set_column_title(1, 'Size')
355 files.set_column_title(2, 'Priority')
356 files.set_column_is_right_aligned(1, True)
357 for file in status['files']:
358 torfile = TorFileRow(file)
359 files.add_row_after(torfile, torfile.info(), None)
360 win.add_widget(files)
362 ok = gnt.Button("OK")
363 def close_info_dlg(b, window):
364 window.destroy()
365 ok.connect('activate', close_info_dlg, win)
366 box = gnt.Box(False, False)
367 box.add_widget(ok)
368 win.add_widget(box)
369 win.show()
371 gntui = None
373 def setup_deluge_core(list):
374 """Do the foreplay with the deluge daemon."""
375 DEFAULT_PREFS = {
376 "load_torrent_location" : "/tmp",
378 global gntui
379 client.set_core_uri("http://localhost:58846")
380 gntui = ConfigManager("gntui.conf", DEFAULT_PREFS)
381 signalhandler = signals.Signals(list)
382 def update_rows(list):
383 list.update_all()
384 return True
385 id = gobject.timeout_add_seconds(1, update_rows, list)
386 def remove_update_timeout(list, id):
387 gobject.source_remove(id)
388 list.connect('destroy', remove_update_timeout, id)
390 def pack_widget(vert, widgets):
391 box = gnt.Box(vert = vert, homo = False)
392 for widget in widgets:
393 box.add_widget(widget)
394 return box
396 def dummy(item):
397 pass
399 def show_dl_preferences(item):
400 pass
402 def refine_config_value(config, value):
403 """Return the value of the configuration in correct type."""
404 config_convert = {
405 "max_connections_global" : int,
406 "max_download_speed" : float,
407 "max_upload_speed" : float,
408 "max_upload_slots_global" : int,
409 "max_connections_per_torrent" : int,
410 "max_upload_slots_per_torrent" : int,
411 "listen_ports" : int,
412 "enc_in_policy" : int,
413 "enc_out_policy" : int,
414 "enc_level" : int,
416 if config in config_convert:
417 conv = config_convert[config]
418 if isinstance(value, list):
419 for iter in range(0, len(value)):
420 value[iter] = conv(value[iter])
421 else:
422 value = conv(value)
423 return value
425 def pref_window(title):
426 win = gnt.Window()
427 win.set_property('vertical', True)
428 win.set_title(title)
429 win.set_alignment(gnt.ALIGN_MID)
430 win.set_pad(0)
431 save = gnt.Button("Save")
432 cancel = gnt.Button("Cancel")
433 def close_window(b, window):
434 window.destroy()
435 cancel.connect('activate', close_window, win)
436 return win, save, cancel
438 def add_section(win, title):
439 win.add_widget(gnt.gnt_label_new_with_format(title, gnt.TEXT_FLAG_BOLD | gnt.TEXT_FLAG_UNDERLINE))
441 def show_bandwidth_pref(item):
442 """Bandwidth settings."""
443 win, save, cancel = pref_window("Bandwidth Preference")
445 configs = client.get_config()
446 entries = []
447 def hook_entry_with_config(entry, config):
448 value = str(configs[config])
449 entry.set_data("config", config)
450 entry.set_data("config-value", value)
451 entry.set_text(value)
452 entries.append(entry)
454 win.add_widget(gnt.gnt_label_new_with_format("Global Bandwidth Usage", gnt.TEXT_FLAG_BOLD | gnt.TEXT_FLAG_UNDERLINE))
455 maxcon = gnt.Entry("")
456 maxdlsp = gnt.Entry("")
457 maxupsp = gnt.Entry("")
458 maxupsl = gnt.Entry("")
460 for entry, config in ((maxcon, "max_connections_global"),
461 (maxdlsp, "max_download_speed"),
462 (maxupsp, "max_upload_speed"),
463 (maxupsl, "max_upload_slots_global"),
465 hook_entry_with_config(entry, config)
467 for label, entry in (("Maximum Connections:", maxcon),
468 ("Maximum Download Speed (KB/s):", maxdlsp),
469 ("Maximum Upload Speed (KB/s):", maxupsp),
470 ("Maximum Upload Slots:", maxupsl),
472 win.add_widget(pack_widget(False, [gnt.Label(label), entry]))
474 win.add_widget(gnt.gnt_label_new_with_format("Per Torrent Bandwidth Usage", gnt.TEXT_FLAG_BOLD | gnt.TEXT_FLAG_UNDERLINE))
475 maxconpt = gnt.Entry("")
476 maxupslpt = gnt.Entry("")
477 for entry, config in ((maxconpt, "max_connections_per_torrent"),
478 (maxupslpt, "max_upload_slots_per_torrent"),
480 hook_entry_with_config(entry, config)
482 for label, entry in (("Maximum Connections:", maxconpt),
483 ("Maximum Upload Slots:", maxupslpt),
485 win.add_widget(pack_widget(False, [gnt.Label(label), entry]))
487 def save_prefs(b, window):
488 newconfigs = {}
489 for entry in entries:
490 global gntui
491 config = entry.get_data("config")
492 oldv = entry.get_data("config-value")
493 value = entry.get_text()
494 if oldv != value:
495 value = refine_config_value(config, value)
496 newconfigs[config] = value
497 sys.stderr.write("Changing " + config + " to " + str(value) + "\n")
498 try:
499 client.set_config(newconfigs)
500 window.destroy()
501 except Exception, exc:
502 log.error(str(exc))
503 save.connect('activate', save_prefs, win)
504 win.add_widget(gnt.Line(False))
505 win.add_widget(pack_widget(False, [save, cancel]))
507 win.show()
509 def show_network_pref(item):
510 """Network settings."""
511 win, save, cancel = pref_window("Network Preference")
513 configs = client.get_config()
515 checks = []
516 def create_checkbox(label, config):
517 check = gnt.CheckBox(label)
518 value = configs[config]
519 check.set_checked(value)
520 check.set_data("config", config)
521 check.set_data("config-value", value)
522 checks.append(check)
523 return check
526 add_section(win, "Ports")
527 randport = create_checkbox("Use Random Ports", "random_port")
528 win.add_widget(pack_widget(False, [randport, gnt.Label(" (Active Port: " + str(client.get_listen_port()) + ")")]))
530 add_section(win, "DHT")
531 dht = create_checkbox("Enable Mainline DHT", "dht")
532 win.add_widget(dht)
534 add_section(win, "Network Extras")
535 upnp = create_checkbox("UPnP", "upnp")
536 nat = create_checkbox("NAT-PMP", "natpmp")
537 pex = create_checkbox("uTorrent-PeX", "utpex")
538 win.add_widget(pack_widget(False, [upnp, nat, pex]))
540 def save_callback(vt, window):
541 newconfigs = {}
542 for check in checks:
543 oldv = check.get_data("config-value")
544 config = check.get_data("config")
545 val = check.get_checked()
546 if val != oldv:
547 newconfigs[config] = val
548 client.set_config(newconfigs)
549 window.destroy()
550 save.connect('activate', save_callback, win)
551 win.add_widget(gnt.Line(False))
552 win.add_widget(pack_widget(False, [save, cancel]))
553 win.show()
555 def create_menu():
556 """Create the menu for the main window."""
557 menu = gnt.Menu(gnt.MENU_TOPLEVEL)
559 file = gnt.MenuItem("File")
560 menu.add_item(file)
562 filesub = gnt.Menu(gnt.MENU_POPUP)
563 file.set_submenu(filesub)
565 qt = gnt.MenuItem("Quit")
566 def qt_cb(q):
567 gnt.gnt_quit()
568 sys.exit(0)
569 qt.connect('activate', qt_cb)
570 filesub.add_item(qt)
572 edit = gnt.MenuItem("Edit")
573 menu.add_item(edit)
575 editsub = gnt.Menu(gnt.MENU_POPUP)
576 edit.set_submenu(editsub)
578 pref = gnt.MenuItem("Preference")
579 prefsub = gnt.Menu(gnt.MENU_POPUP)
580 pref.set_submenu(prefsub)
581 editsub.add_item(pref)
583 for name, cb in (("Downloads", show_dl_preferences),
584 ("Network", show_network_pref),
585 ("Bandwidth", show_bandwidth_pref),
586 ("Other", dummy),
587 ("Plugins", dummy),
589 dl = gnt.MenuItem(name)
590 dl.connect('activate', cb)
591 prefsub.add_item(dl)
593 conn = gnt.MenuItem("Connections")
594 editsub.add_item(conn)
596 return menu
598 def main():
599 win = gnt.Window()
600 win.set_property('vertical', True)
601 win.set_toplevel(True)
602 win.set_title("Delugent")
603 win.set_pad(0)
604 win.set_alignment(gnt.ALIGN_MID)
606 list = TorList()
607 list.enable_sort()
609 win.add_widget(list)
610 width, height = gnt.screen_size()
611 list.set_size(width, height)
613 hbox = gnt.Box(homo = False, vert = False)
615 add = gnt.Button("Add")
616 def add_clicked(b):
617 def file_selected(widget, path, file, window):
618 files = widget.get_selected_multi_files()
619 window.destroy()
620 client.add_torrent_file(files)
621 path = os.path.dirname(files[0])
622 gntui['load_torrent_location'] = path
623 def change_dl_loc(b, label):
624 def location_selected(fl, path, file, label):
625 fl.destroy()
626 client.set_config({'download_location': path})
627 label.set_text("Download to: " + path)
628 fl = gnt.FileSel()
629 fl.set_title("Select Location")
630 fl.set_dirs_only(True)
631 fl.set_current_location(configs['download_location'])
632 setup_fileselector(fl, fl)
633 fl.connect('file_selected', location_selected, label)
634 fl.show()
636 win = gnt.Window()
637 win.set_property('vertical', True)
638 win.set_pad(0)
640 configs = client.get_config()
642 fl = gnt.FileSel()
643 win.set_title("Select a Torrent")
644 fl.set_multi_select(True)
645 fl.set_current_location(gntui['load_torrent_location'])
646 fl.connect("file_selected", file_selected, win)
647 setup_fileselector(fl, win)
648 gnt.set_flag(fl, 8 | 16)
649 fl.draw()
651 downloadto = gnt.gnt_label_new_with_format("Download to: " + configs['download_location'], gnt.TEXT_FLAG_BOLD)
652 change = gnt.Button("...")
653 change.connect('activate', change_dl_loc, downloadto)
654 box = pack_widget(False, [downloadto, change])
655 box.set_fill(False)
656 box.set_alignment(gnt.ALIGN_MID)
657 win.add_widget(box)
658 win.add_widget(gnt.Line(False))
659 win.add_widget(fl)
660 win.give_focus_to_child(fl)
661 win.show()
663 add.connect('activate', add_clicked)
664 hbox.add_widget(add)
665 gnt.gnt_util_set_trigger_widget(win, gnt.KEY_INS, add)
667 rm = gnt.Button("Remove")
668 def rm_clicked(b):
669 tor = list.get_selection_data()
670 client.remove_torrent([tor.id])
671 rm.connect('activate', rm_clicked)
672 hbox.add_widget(rm)
673 gnt.gnt_util_set_trigger_widget(win, gnt.KEY_DEL, rm)
675 hbox.add_widget(gnt.Label("|"))
677 pause = gnt.Button("Pause")
678 def pause_clicked(b):
679 tor = list.get_selection_data()
680 client.pause_torrent([tor.id])
681 pause.connect('activate', pause_clicked)
682 hbox.add_widget(pause)
683 gnt.gnt_util_set_trigger_widget(win, 'p', pause)
685 resume = gnt.Button("Resume")
686 def resume_clicked(b):
687 tor = list.get_selection_data()
688 client.resume_torrent([tor.id])
689 resume.connect('activate', resume_clicked)
690 hbox.add_widget(resume)
691 gnt.gnt_util_set_trigger_widget(win, 'r', resume)
693 hbox.add_widget(gnt.Label("|"))
695 info = gnt.Button("Details")
696 def info_clicked(b):
697 list.perform_action_named('info')
698 info.connect('activate', info_clicked)
699 hbox.add_widget(info)
701 win.add_widget(hbox)
703 menu = create_menu() # XXX: Investigate why this causes warnings on exit
704 win.set_menu(menu)
706 win.show()
708 try:
709 setup_deluge_core(list)
710 except Exception, msg:
711 gnt.gnt_quit()
712 print "Error:", msg
713 return
715 # Add the torrents in the list
716 session_state = client.get_session_state()
717 for torrent_id in session_state:
718 list.add_torrent(torrent_id)
720 gnt.gnt_main()
721 gnt.gnt_quit()
723 if __name__ == "__main__":
724 main()