Ticket #1412: transmission-remote-cli.py

File transmission-remote-cli.py, 30.0 KB (added by fagga, 12 years ago)
Line 
1#!/usr/bin/python
2########################################################################
3# This is transmission-remote-cli, a client for the daemon of the      #
4# BitTorrent client Transmission.                                      #
5#                                                                      #
6# This program is free software: you can redistribute it and/or modify #
7# it under the terms of the GNU General Public License as published by #
8# the Free Software Foundation, either version 3 of the License, or    #
9# (at your option) any later version.                                  #
10#                                                                      #
11# This program is distributed in the hope that it will be useful,      #
12# but WITHOUT ANY WARRANTY; without even the implied warranty of       #
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the        #
14# GNU General Public License for more details:                         #
15# http://www.gnu.org/licenses/gpl-3.0.txt                              #
16########################################################################
17
18
19
20
21DEBUG=True
22
23HOST = 'localhost'
24PORT = 9091
25
26
27from optparse import OptionParser
28parser = OptionParser(usage="Usage: %prog [HOST[:PORT]]")
29(options, args) = parser.parse_args()
30
31if args:
32    if args[0].find(':') >= 0:
33        HOST, PORT = args[0].split(':')
34        PORT = int(PORT)
35    else:
36        HOST = args[0]
37
38
39
40# Handle communication with Transmission server.
41import simplejson as json
42import socket
43import time
44
45class TransmissionRequest:
46    def __init__(self, host, port, method=None, tag=None, arguments=None):
47        self.host   = host
48        self.port   = port
49        self.socket = None
50        self.response_data = ''
51        self.last_update   = 0
52        if method and tag:
53            self.set_request_data(method, tag, arguments)
54
55
56    def set_request_data(self, method, tag, arguments=None):
57        # put request data together
58        request_data = {'method':method, 'tag':tag}
59        if arguments: request_data['arguments'] = arguments
60
61        # convert request data into json format
62        json_request = json.dumps(request_data)
63
64        # create HTTP POST request
65        self.http_request  = "POST /transmission/rpc HTTP/1.0\n"
66        self.http_request += "Content-Length: %d\n\n" % len(json_request)
67        self.http_request += json_request
68
69
70    def send_request(self):
71        """Ask for information from server OR submit command."""
72
73        try:
74            self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
75            self.socket.connect((self.host, self.port))
76            self.socket.send(self.http_request)
77            self.socket.setblocking(0)
78        except socket.error, msg:
79            self.error = msg[1]
80
81
82    def get_response(self):
83        """Get response to previously sent request."""
84
85        if self.socket == None:
86            return {'result': 'no open request'}
87
88        buffer = ''
89        while True:
90            try:
91                buffer = self.socket.recv(8192)
92            except socket.error, msg:
93                return {'result': msg}
94
95            if len(buffer) > 0:
96                self.response_data += buffer
97            else:
98                data = json.loads(self.response_data.split("\r\n\r\n")[1])
99                self.socket = None
100                self.response_data = ''
101                return data
102
103
104class Transmission:
105    STATUS_CHECK_WAIT = 1 << 0 # Waiting in queue to check files
106    STATUS_CHECK      = 1 << 1 # Checking files
107    STATUS_DOWNLOAD   = 1 << 2 # Downloading
108    STATUS_SEED       = 1 << 3 # Seeding
109    STATUS_STOPPED    = 1 << 4 # Torrent is stopped
110
111    LIST_FIELDS = [ 'id', 'name', 'status', 'seeders', 'leechers',
112                    'rateDownload', 'rateUpload', 'eta', 'uploadRatio',
113                    'sizeWhenDone', 'leftUntilDone', 'addedDate',
114                    'announceResponse', 'error', 'errorString' ]
115
116    def __init__(self, host, port):
117        self.host   = host
118        self.port   = port
119        self.error = None
120
121        self.requests = [TransmissionRequest(host, port, 'torrent-get', 7, {'fields': self.LIST_FIELDS}),
122                         TransmissionRequest(host, port, 'session-stats', 21),
123                         TransmissionRequest(host, port, 'session-get', 22)]
124
125        self.torrents = []
126        self.stats    = dict()
127
128
129        # initial initialization
130        while True:
131            self.update(0)
132            if self.get_error():
133                print self.get_error()
134                exit(1)
135            if len(self.stats) >= 15 and self.torrents:
136                break
137
138
139
140    def update(self, delay):
141        """Maintain up-to-date data."""
142
143        torrentlist_update = False
144        for request in self.requests:
145            if time.time() - request.last_update >= delay:
146                request.last_update = time.time()
147
148                response = request.get_response()
149
150                if response['result'] == 'no open request':
151                    request.send_request()
152
153                elif response['result'] == 'success':
154                    tag = self.parse_response(response)
155                    if tag == 7:
156                        torrentlist_update = True
157
158        return torrentlist_update
159
160                   
161
162    def parse_response(self, response):
163        # response is a reply to torrent-get
164        if response['tag'] == 7:
165            self.torrents = response['arguments']['torrents']
166            for t in self.torrents:
167                try: t['percent_done'] = 1/(float(t['sizeWhenDone']) / float(t['sizeWhenDone']-t['leftUntilDone']))
168                except ZeroDivisionError: t['percent_done'] = 0.0
169                t['current_size'] = t['sizeWhenDone'] - t['leftUntilDone']
170                if int(t['seeders'])  < 0: t['seeders']  = 0
171                if int(t['leechers']) < 0: t['leechers'] = 0
172                if float(t['uploadRatio']) == -2.0:
173                    t['uploadRatio'] = 'oo'
174                elif float(t['uploadRatio']) == -1.0:
175                    t['uploadRatio'] = '0.0'
176                else:
177                    t['uploadRatio'] = "%.1f" % float(t['uploadRatio'])
178
179        # response is a reply to session-stats
180        elif response['tag'] == 21:
181            self.stats.update(response['arguments']['session-stats'])
182
183        # response is a reply to session-get
184        elif response['tag'] == 22:
185            self.stats.update(response['arguments'])
186
187        return response['tag']
188
189
190
191
192
193    def get_error(self):
194        return self.error
195    def get_daemon_stats(self):
196        return self.stats
197
198    def get_torrentlist(self, sort_order='name', reverse=False):
199        self.torrents.sort(cmp=lambda x,y: self.my_cmp(x, y, sort_order), reverse=reverse)
200        return self.torrents
201
202    def my_cmp(self, x, y, sort_order):
203        if isinstance(x[sort_order], int):
204            return cmp(x[sort_order], y[sort_order])
205        else:
206            return cmp(x[sort_order].lower(), y[sort_order].lower())
207
208           
209
210
211    def set_upload_limit(self, new_limit):
212        request = TransmissionRequest(self.host, self.port)
213        request.set_request_data('session-set', 1,
214                                 { 'speed-limit-up': int(new_limit),
215                                   'speed-limit-up-enabled': 1 })
216        request.send_request()
217    def set_download_limit(self, new_limit):
218        request = TransmissionRequest(self.host, self.port)
219        request.set_request_data('session-set', 1,
220                                 { 'speed-limit-down': int(new_limit),
221                                   'speed-limit-down-enabled': 1 })
222        request.send_request()
223
224
225    def stop_torrent(self, id):
226        request = TransmissionRequest(self.host, self.port)
227        request.set_request_data('torrent-stop',   1, {'ids': [id]})
228        request.send_request()
229        self.wait_for_torrentlist_update()
230
231    def start_torrent(self, id):
232        request = TransmissionRequest(self.host, self.port)
233        request.set_request_data('torrent-start',  1, {'ids': [id]})
234        request.send_request()
235        self.wait_for_torrentlist_update()
236
237    def verify_torrent(self, id):
238        request = TransmissionRequest(self.host, self.port)
239        request.set_request_data('torrent-verify', 1, {'ids': [id]})
240        request.send_request()
241        self.wait_for_torrentlist_update()
242
243    def remove_torrent(self, id):
244        request = TransmissionRequest(self.host, self.port)
245        request.set_request_data('torrent-remove', 1, {'ids': [id]})
246        request.send_request()
247        self.wait_for_torrentlist_update()
248
249
250    def wait_for_torrentlist_update(self):
251        # if we don't wait twice, the update isn't always up to date
252        while True:
253            if self.update(0): break
254            time.sleep(0.1)
255        while True:
256            if self.update(0): break
257            time.sleep(0.1)
258
259
260# End of Class Transmission       
261
262
263
264def scale_time(seconds, type):
265    if seconds < 0:
266        return ('?', 'unknown')[type=='long']
267    elif seconds < 60:
268        if type == 'long':
269            return "%s second%s" % (seconds, ('', 's')[seconds>1])
270        else:
271            return "%ss" % seconds
272    elif seconds < 3600:
273        minutes = int(seconds / 60)
274        if type == 'long':
275            return "%d minute%s" % (minutes, ('', 's')[minutes>1])
276        else:
277            return "%dm" % minutes
278    elif seconds < 86400:
279        hours = int(seconds / 3600)
280        if type == 'long':
281            return "%d hour%s" % (hours, ('', 's')[hours>1])
282        else:
283            return "%dh" % hours
284    else:
285        days = int(seconds / 86400)
286        if type == 'long':
287            return "%d day%s" % (days, ('', 's')[days>1])
288        else:
289            return "%dd" % days
290
291
292def scale_bytes(bytes):
293    if bytes >= 1073741824:
294        scaled_bytes = round((bytes / 1073741824.0), 2)
295        unit = "G"
296    elif bytes >= 1048576:
297        scaled_bytes = round((bytes / 1048576.0), 1)
298        if scaled_bytes >= 100:
299            scaled_bytes = int(scaled_bytes)
300        unit = "M"
301    elif bytes >= 1024:
302        scaled_bytes = round((bytes / 1024.0), 1)
303        if scaled_bytes >= 10:
304            scaled_bytes = int(scaled_bytes)
305        unit = "K"
306    else:
307        return "%dB" % bytes
308
309    # convert to integer if .0
310    if int(scaled_bytes) == float(scaled_bytes):
311        return "%d%s" % (int(scaled_bytes), unit)
312    else:
313        return "%s%s" % (str(scaled_bytes).rstrip('0'), unit)
314   
315   
316
317# User Interface
318import curses
319import os
320import signal
321import locale
322locale.setlocale(locale.LC_ALL, '')
323
324class Interface:
325    def __init__(self, host, port):
326        self.host = host
327        self.port = port
328        self.server = Transmission(host, port)
329
330        self.sort_order   = 'name'
331        self.sort_reverse = False
332        self.torrents = self.server.get_torrentlist(self.sort_order, self.sort_reverse)
333        self.stats    = self.server.get_daemon_stats()
334
335        self.focus     = -1  # -1: nothing focused; min: 0 (top of list); max: <# of torrents>-1 (bottom of list)
336        self.scrollpos = 0   # start of torrentlist
337        self.torrents_per_page  = 0
338        self.rateDownload_width = self.rateUpload_width = 0
339
340        os.environ['ESCDELAY'] = '0' # make escape usable
341        curses.wrapper(self.run)
342
343
344    def quit(self, msg):
345        curses.nocbreak()
346        curses.endwin()
347        print msg
348        exit(0)
349
350
351    def init_screen(self):
352        curses.halfdelay(10) # STDIN timeout
353        curses.curs_set(0)   # hide cursor
354        self.screen.keypad(True) # enable special keys
355
356        curses.init_pair(1, curses.COLOR_BLACK,   curses.COLOR_BLUE)  # download rate
357        curses.init_pair(2, curses.COLOR_BLACK,   curses.COLOR_RED)   # upload rate
358        curses.init_pair(3, curses.COLOR_BLUE,    curses.COLOR_BLACK) # unfinished progress
359        curses.init_pair(4, curses.COLOR_GREEN,   curses.COLOR_BLACK) # finished progress
360        curses.init_pair(5, curses.COLOR_BLACK,   curses.COLOR_WHITE) # eta/ratio
361        curses.init_pair(6, curses.COLOR_CYAN,    curses.COLOR_BLACK) # idle progress
362        curses.init_pair(7, curses.COLOR_MAGENTA, curses.COLOR_BLACK) # verifying
363
364        signal.signal(signal.SIGWINCH, lambda y,frame: self.get_screen_size())
365        self.get_screen_size()
366
367
368    def get_screen_size(self):
369        curses.endwin()
370        self.screen.refresh()
371        self.height, self.width = self.screen.getmaxyx()
372        self.focus = -1
373        self.scrollpos = 0
374        self.manage_layout()
375
376
377    def manage_layout(self):
378        self.pad = curses.newpad((len(self.torrents)+1)*3, self.width)
379        self.torrentlist_height = self.height - 2
380        self.torrents_per_page  = self.torrentlist_height/3
381
382        if self.torrents:
383            visible_torrents = self.torrents[self.scrollpos/3 : self.scrollpos/3 + self.torrents_per_page + 1]
384            self.rateDownload_width = self.get_rateDownload_width(visible_torrents)
385            self.rateUpload_width   = self.get_rateUpload_width(visible_torrents)
386
387            self.torrent_title_width = self.width - self.rateUpload_width - 2
388            # show downloading column only if any downloading torrents are visible
389            if filter(lambda x: x['status']==Transmission.STATUS_DOWNLOAD, visible_torrents):
390                self.torrent_title_width -= self.rateDownload_width + 2
391        else:
392            self.torrent_title_width = 80
393
394
395    def get_rateDownload_width(self, torrents):
396        new_width = max(map(lambda x: len(scale_bytes(x['rateDownload'])), torrents))
397        new_width = max(max(map(lambda x: len(scale_time(x['eta'], 'short')), torrents)), new_width)
398        new_width = max(len(scale_bytes(self.stats['downloadSpeed'])), new_width)
399        new_width = max(self.rateDownload_width, new_width) # don't shrink
400        return new_width
401
402    def get_rateUpload_width(self, torrents):
403        new_width = max(map(lambda x: len(scale_bytes(x['rateUpload'])), torrents))
404        new_width = max(max(map(lambda x: len(x['uploadRatio']), torrents)), new_width)
405        new_width = max(len(scale_bytes(self.stats['uploadSpeed'])), new_width)
406        new_width = max(self.rateUpload_width, new_width) # don't shrink
407        return new_width
408
409
410    def run(self, screen):
411        self.screen = screen
412        self.init_screen()
413
414        self.draw_title_bar()
415        self.draw_stats()
416        self.draw_torrentlist()
417
418        while True:
419            self.server.update(1)
420            if self.server.get_error():
421                self.quit(self.server.get_error())
422
423            self.torrents = self.server.get_torrentlist(self.sort_order, self.sort_reverse)
424            self.stats    = self.server.get_daemon_stats()
425
426            self.draw_torrentlist()
427            self.draw_title_bar()
428            self.draw_stats()
429
430            self.handle_user_input()
431
432
433    def handle_user_input(self):
434        c = self.screen.getch()
435        if c == -1: return
436
437        elif c == curses.KEY_RESIZE:
438            self.get_screen_size()
439
440        # reset + redraw
441        elif c == 27 or c == curses.KEY_BREAK or c == 12:
442            self.focus = -1
443            self.scrollpos = 0
444            self.draw_torrentlist()
445
446        # quit on q or ctrl-c
447        elif c == ord('q'):
448            exit(0)
449
450
451        # show sort order menu
452        elif c == ord('s'):
453            options = [('name','Name'), ('addedDate','Age'), ('percent_done','Progress'),
454                       ('seeders','Seeds'), ('leechers','Leeches'), ('sizeWhenDone', 'Size'),
455                       ('reverse','Reverse')]
456            choice = self.dialog_menu('Sort order', options, map(lambda x: x[0]==self.sort_order, options).index(True)+1)
457            if choice:
458                if choice == 'reverse':
459                    self.sort_reverse = not self.sort_reverse
460                else:
461                    self.sort_order = choice
462                self.focus = -1
463                self.scrollpos = 0
464
465
466        # movement
467        elif c == curses.KEY_UP:
468            self.scroll_up()
469        elif c == curses.KEY_DOWN:
470            self.scroll_down()
471        elif c == curses.KEY_HOME:
472            self.scroll_home()
473        elif c == curses.KEY_END:
474            self.scroll_end()
475
476
477        # upload/download limits
478        elif c == ord('u'):
479            limit = self.dialog_input_number("Upload limit in K/s", self.stats['speed-limit-up']/1024)
480            if limit >= 0: self.server.set_upload_limit(limit)
481        elif c == ord('d'):
482            limit = self.dialog_input_number("Download limit in K/s", self.stats['speed-limit-down']/1024)
483            if limit >= 0: self.server.set_download_limit(limit)
484
485        # pause/unpause torrent
486        elif c == ord('p'):
487            if self.focus < 0: return
488            id = self.torrents[self.focus]['id']
489            if self.torrents[self.focus]['status'] == Transmission.STATUS_STOPPED:
490                self.server.start_torrent(id)
491            else:
492                self.server.stop_torrent(id)
493            self.torrents = self.server.get_torrentlist(self.sort_order, self.sort_reverse)
494           
495        # verify torrent data
496        elif c == ord('v'):
497            if self.focus < 0: return
498            id = self.torrents[self.focus]['id']
499            if self.torrents[self.focus]['status'] != Transmission.STATUS_CHECK:
500                self.server.verify_torrent(id)
501            self.torrents = self.server.get_torrentlist(self.sort_order, self.sort_reverse)
502
503        # remove torrent
504        elif c == ord('r'):
505            if self.focus < 0: return
506            id = self.torrents[self.focus]['id']
507            name = self.torrents[self.focus]['name'][0:self.width - 15]
508            if self.dialog_yesno("Remove %s?" % name.encode('utf8')) == True:
509                self.server.remove_torrent(id)
510            self.torrents = self.server.get_torrentlist(self.sort_order, self.sort_reverse)
511
512        else: return
513
514        self.draw_torrentlist()
515
516
517
518
519    def draw_torrentlist(self):
520        self.manage_layout() # length of torrentlist may have changed
521
522        ypos = 0
523        for i in range(len(self.torrents)):
524            self.draw_torrentitem(self.torrents[i], (i == self.focus), ypos, 0)
525            ypos += 3
526
527        self.pad.refresh(self.scrollpos,0, 1,0, self.torrentlist_height,self.width-1)
528        self.screen.refresh()
529
530
531    def draw_torrentitem(self, info, focused, y, x):
532        # the torrent name is also a progress bar
533        self.draw_torrent_title(info, focused, y)
534
535        rates = ''
536        if info['status'] == Transmission.STATUS_DOWNLOAD:
537            self.draw_downloadrate(info['rateDownload'], y)
538        if info['status'] == Transmission.STATUS_DOWNLOAD or info['status'] == Transmission.STATUS_SEED:
539            self.draw_uploadrate(info['rateUpload'], y)
540        if info['percent_done'] < 1:
541            self.draw_eta(info, y)
542
543        self.draw_ratio(info, y)
544
545        # the line below the title/progress
546        self.draw_torrent_status(info, focused, y)
547
548
549
550    def draw_downloadrate(self, rate, ypos):
551        self.pad.addstr(ypos, self.width-self.rateDownload_width-self.rateUpload_width-3, "D")
552        self.pad.addstr(ypos, self.width-self.rateDownload_width-self.rateUpload_width-2,
553                        "%s" % scale_bytes(rate).rjust(self.rateDownload_width, ' '),
554                        curses.color_pair(1) + curses.A_BOLD + curses.A_REVERSE)
555
556    def draw_uploadrate(self, rate, ypos):
557        self.pad.addstr(ypos, self.width-self.rateUpload_width-1, "U")
558        self.pad.addstr(ypos, self.width-self.rateUpload_width,
559                       "%s" % scale_bytes(rate).rjust(self.rateUpload_width, ' '),
560                       curses.color_pair(2) + curses.A_BOLD + curses.A_REVERSE)
561
562    def draw_ratio(self, info, ypos):
563        self.pad.addstr(ypos+1, self.width-self.rateUpload_width-1, "R")
564        self.pad.addstr(ypos+1, self.width-self.rateUpload_width,
565                       "%s" % info['uploadRatio'].rjust(self.rateUpload_width, ' '),
566                       curses.color_pair(5) + curses.A_BOLD + curses.A_REVERSE)
567
568    def draw_eta(self, info, ypos):
569        self.pad.addstr(ypos+1, self.width-self.rateDownload_width-self.rateUpload_width-3, "T")
570        self.pad.addstr(ypos+1, self.width-self.rateDownload_width-self.rateUpload_width-2,
571                        "%s" % scale_time(info['eta'], 'short').rjust(self.rateDownload_width, ' '),
572                        curses.color_pair(5) + curses.A_BOLD + curses.A_REVERSE)
573
574
575    def draw_torrent_title(self, info, focused, ypos):
576        bar_width = int(self.torrent_title_width * info['percent_done'])
577        title = info['name'][0:self.torrent_title_width].ljust(self.torrent_title_width, ' ')
578
579        size = " %s" % scale_bytes(info['sizeWhenDone'])
580        if info['percent_done'] < 1:
581            size = " %s /" % scale_bytes(info['current_size']) + size
582        title = title[:-len(size)] + size
583
584        if info['status'] == Transmission.STATUS_SEED:
585            color = curses.color_pair(4)
586        elif info['status'] == Transmission.STATUS_STOPPED:
587            color = curses.color_pair(5) + curses.A_UNDERLINE
588        elif info['status'] == Transmission.STATUS_CHECK:
589            color = curses.color_pair(7)
590        elif info['rateDownload'] == 0:
591            color = curses.color_pair(6)
592        elif info['percent_done'] < 1:
593            color = curses.color_pair(3)
594        else:
595            color = 0
596
597        title = title.encode('utf-8')
598        if focused: 
599            self.pad.addstr(ypos, 0, title[0:bar_width], curses.A_REVERSE + color + curses.A_BOLD)
600            self.pad.addstr(ypos, bar_width, title[bar_width:], curses.A_REVERSE + curses.A_BOLD)
601        else:
602            self.pad.addstr(ypos, 0, title[0:bar_width], curses.A_REVERSE + color)
603            self.pad.addstr(ypos, bar_width, title[bar_width:], curses.A_REVERSE)
604
605
606    def draw_torrent_status(self, info, focused, ypos):
607        status = 'unknown status'
608        if   info['status'] == Transmission.STATUS_CHECK_WAIT: status = 'will verify'
609        elif info['status'] == Transmission.STATUS_CHECK:      status = 'verifying'
610
611        elif info['errorString']:
612            line = info['errorString'].ljust(self.torrent_title_width, ' ')
613
614        elif info['status'] == Transmission.STATUS_SEED:     status = 'seeding'
615        elif info['status'] == Transmission.STATUS_STOPPED:  status = 'paused'
616        elif info['status'] == Transmission.STATUS_DOWNLOAD:
617            status = ('idle','downloading')[info['rateDownload'] > 0]
618        line = status
619
620        if info['percent_done'] < 1:
621            line += " (%s%%)" % int(info['percent_done'] * 100)
622       
623        peers  = "%d seed%s " % (info['seeders'], ('s', '')[info['seeders']==1])
624        peers += "%d leech%s" % (info['leechers'], ('es', '')[info['leechers']==1])
625        line = line + peers.rjust(self.torrent_title_width - len(line), ' ')
626
627        if focused:
628            self.pad.addstr(ypos+1, 0, line, curses.A_REVERSE + curses.A_BOLD)
629        else:
630            self.pad.addstr(ypos+1, 0, line)
631
632
633
634
635
636    def scroll_up(self):
637        if self.focus < 0:
638            return
639        else:
640            self.focus -= 1
641            if self.scrollpos/3 - self.focus > 0:
642                self.scrollpos -= 3
643                self.scrollpos = max(0, self.scrollpos)
644            while self.scrollpos % 3:
645                self.scrollpos -= 1
646
647    def scroll_down(self):
648        if self.focus >= len(self.torrents)-1:
649            return
650        else:
651            self.focus += 1
652            if self.focus+1 - self.scrollpos/3 > self.torrents_per_page:
653                self.scrollpos += 3
654
655    def scroll_home(self):
656        self.focus     = 0
657        self.scrollpos = 0
658
659    def scroll_end(self):
660        self.focus     = len(self.torrents)-1
661        self.scrollpos = max(0, (len(self.torrents) - self.torrents_per_page) * 3)
662
663
664
665
666
667
668
669    def draw_stats(self):
670        self.screen.insstr((self.height-1), 0, ' '.center(self.width, ' '), curses.A_REVERSE)
671        self.draw_torrent_stats()
672        self.draw_transmission_stats()
673
674
675    def draw_torrent_stats(self):
676        torrents = "%d Torrents: " % self.stats['torrentCount']
677
678        downloading_torrents = filter(lambda x: x['status']==Transmission.STATUS_DOWNLOAD, self.torrents)
679        torrents += "%d downloading; " % len(downloading_torrents)
680
681        seeding_torrents = filter(lambda x: x['status']==Transmission.STATUS_SEED, self.torrents)
682        torrents += "%d seeding; " % len(seeding_torrents)
683
684        torrents += "%d paused" % self.stats['pausedTorrentCount']
685
686        self.screen.addstr((self.height-1), 0, torrents, curses.A_REVERSE)
687
688
689    def draw_transmission_stats(self):
690        rates_width = self.rateDownload_width + self.rateUpload_width + 3
691        self.screen.move((self.height-1), self.width-rates_width)
692
693        self.screen.addstr('D', curses.A_REVERSE)
694        self.screen.addstr(scale_bytes(self.stats['downloadSpeed']).rjust(self.rateDownload_width, ' '),
695                           curses.A_REVERSE + curses.A_BOLD + curses.color_pair(1))
696
697        self.screen.addstr(' U', curses.A_REVERSE)
698        self.screen.insstr(scale_bytes(self.stats['uploadSpeed']).rjust(self.rateUpload_width, ' '),
699                           curses.A_REVERSE + curses.A_BOLD + curses.color_pair(2))
700
701
702
703
704
705    def draw_title_bar(self):
706        self.screen.insstr(0, 0, ' '.center(self.width, ' '), curses.A_REVERSE)
707        self.draw_connection_status()
708        self.draw_quick_help()
709       
710    def draw_connection_status(self):
711        status = "Transmission @ %s:%s" % (self.host, self.port)
712        self.screen.addstr(0, 0, status.encode('utf-8'), curses.A_REVERSE)
713
714    def draw_quick_help(self):
715        help = "| s Sort | u Upload Limit | d Download Limit | q Quit"
716        if self.focus >= 0:
717            help = "| p Pause/Unpause | r Remove | v Verify " + help
718
719        if len(help) > self.width:
720            help = help[0:self.width]
721
722        self.screen.insstr(0, self.width-len(help), help, curses.A_REVERSE)
723       
724
725
726
727
728    def window(self, height, width, message=''):
729        ypos = int(self.height - height)/2
730        xpos = int(self.width  - width)/2
731        win = curses.newwin(height, width, ypos, xpos)
732        win.box()
733        win.bkgd(' ', curses.A_REVERSE + curses.A_BOLD)
734
735        ypos = 1
736        for msg in message.split("\n"):
737            win.addstr(ypos, 2, msg)
738            ypos += 1
739
740        return win
741
742
743    def dialog_message(self, message):
744        height = 5 + message.count("\n")
745        width  = len(message)+4
746        win = self.window(height, width, message)
747        win.addstr(height-2, (width/2) - 6, 'Press any key')
748        win.notimeout(True)
749        win.getch()
750
751    def dialog_yesno(self, message):
752        height = 5 + message.count("\n")
753        width  = len(message)+4
754        win = self.window(height, width, message)
755        win.notimeout(True)
756        win.keypad(True)
757
758        input = False
759        while True:
760            win.move(height-2, (width/2)-6)
761            if input:
762                win.addstr('Yes', curses.color_pair(2))
763                win.addstr('  ')
764                win.addstr('No', curses.color_pair(5))
765            else:
766                win.addstr('Yes', curses.color_pair(5))
767                win.addstr('  ')
768                win.addstr('No', curses.color_pair(2))
769
770            c = win.getch()
771
772            if c == ord('y'):
773                return True
774            elif c == ord('n'):
775                return False
776            elif c == ord("\t"):
777                input = not input
778            elif c == curses.KEY_LEFT:
779                input = True
780            elif c == curses.KEY_RIGHT:
781                input = False
782            elif c == ord("\n") or c == ord(' '):
783                return input
784            elif c == 27 or c == curses.KEY_BREAK:
785                return -1
786
787
788    def dialog_input_number(self, message, current_value):
789        message += "\nup/down    +/- 100"
790        message += "\nleft/right +/-  10"
791        height = 4 + message.count("\n")
792        width  = max(map(lambda x: len(x), message.split("\n"))) + 4
793
794        win = self.window(height, width, message)
795        win.notimeout(True)
796        win.keypad(True)
797
798        input = str(current_value)
799        while True:
800            win.addstr(height-2, 2, input.ljust(width-4, ' '), curses.color_pair(5))
801            c = win.getch()
802            if c == 27 or c == curses.KEY_BREAK:
803                return -1
804            elif c == ord("\n"):
805                if input: return int(input)
806                else:     return -1
807               
808            elif c == curses.KEY_BACKSPACE or c == curses.KEY_DC or c == 127 or c == 8:
809                input = input[:-1]
810                if input == '': input = '0'
811            elif len(input) >= width-4:
812                curses.beep()
813            elif c >= ord('0') and c <= ord('9'):
814                input += chr(c)
815
816            elif c == curses.KEY_LEFT:
817                input = str(int(input) - 10)
818            elif c == curses.KEY_RIGHT:
819                input = str(int(input) + 10)
820            elif c == curses.KEY_DOWN:
821                input = str(int(input) - 100)
822            elif c == curses.KEY_UP:
823                input = str(int(input) + 100)
824            if int(input) < 0: input = '0'
825
826
827    def dialog_menu(self, title, options, focus=1):
828        height = len(options) + 2
829        width  = max(max(map(lambda x: len(x[1])+4, options)), len(title)+3)
830        win = self.window(height, width)
831
832        win.addstr(0,1, title)
833        win.notimeout(True)
834        win.keypad(True)
835
836        while True:
837            i = 1
838            for option in options:
839                if i == focus:
840                    win.addstr(i,2, option[1].ljust(width-4, ' '), curses.color_pair(5))
841                else:
842                    win.addstr(i,2, option[1].ljust(width-4, ' '))
843                i+=1
844
845            c = win.getch()
846            if c == 27 or c == curses.KEY_BREAK:
847                return None
848            elif c == ord("\n"):
849                return options[focus-1][0]
850            elif c == curses.KEY_DOWN:
851                focus += 1
852                if focus > len(options): focus = 1
853            elif c == curses.KEY_UP:
854                focus -= 1
855                if focus < 1: focus = len(options)
856            elif c == curses.KEY_HOME:
857                focus = 1
858            elif c == curses.KEY_END:
859                focus = len(options)
860
861
862def debug(data):
863    if DEBUG:
864        file = open("debug.log", 'a')
865        file.write(data.encode('utf-8'))
866        file.close
867   
868
869ui = Interface(HOST, PORT)
870
871
872
873
874