class BtSyncApp(BtInputHelper,BtMessageHelper): def __init__(self,agent): self.agent = agent self.builder = Gtk.Builder() self.builder.set_translation_domain('btsync-gui') self.builder.add_from_file(os.path.dirname(__file__) + "/btsyncapp.glade") self.builder.connect_signals (self) width, height = self.agent.get_pref('windowsize', (602,328)) self.window = self.builder.get_object('btsyncapp') self.window.set_default_size(width, height) self.window.connect('delete-event',self.onDelete) if not self.agent.is_auto(): title = self.window.get_title() self.window.set_title('{0} - ({1}:{2})'.format( title, agent.get_host(), agent.get_port() )) self.window.show() self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) self.app_status_to = BtDynamicTimeout(1000,self.refresh_app_status) self.dlg = None self.prefs = self.agent.get_prefs() self.init_folders_controls() self.init_devices_controls() self.init_transfers_controls() self.init_history_controls() self.init_preferences_controls() # TODO: Hide pages not supported by API notebook = self.builder.get_object('notebook1') transfers = notebook.get_nth_page(2) history = notebook.get_nth_page(3) transfers.hide() history.hide() # TODO: End self.init_transfer_status() self.init_folders_values() self.init_preferences_values() def close(self): self.app_status_to.stop() if self.dlg is not None: self.dlg.response(Gtk.ResponseType.CANCEL) def connect_close_signal(self,handler): return self.window.connect('delete-event', handler) def init_folders_controls(self): self.folders = self.builder.get_object('folders_list') self.folders_menu = self.builder.get_object('folders_menu') self.folders_menu_openfolder = self.builder.get_object('folder_menu_openfolder') self.folders_menu_openarchive = self.builder.get_object('folder_menu_openarchive') self.folders_menu_editsyncignore = self.builder.get_object('folder_menu_editsyncignore') self.folders_selection = self.builder.get_object('folders_selection') self.folders_treeview = self.builder.get_object('folders_tree_view') self.folders_activity_label = self.builder.get_object('folders_activity_label') self.folders_add = self.builder.get_object('folders_add') self.folders_remove = self.builder.get_object('folders_remove') self.folders_remove.set_sensitive(False) self.set_treeview_column_widths( self.folders_treeview, self.agent.get_pref('folders_columns',[300]) ) self.set_treeview_sort_info( self.folders_treeview, self.agent.get_pref('folders_sortinfo', [0, Gtk.SortType.ASCENDING]) ) def init_folders_values(self): try: self.lock() folders = self.agent.get_folders() if folders is not None: for index, value in enumerate(folders): # see in update_folder_values the insane explanation why # also an md5 digest has to be saved digest = md5.new(value['dir'].encode('latin-1')).hexdigest() self.folders.append ([ self.agent.fix_decode(value['dir']), # 0:Folder self.get_folder_info_string(value), # 1:Content value['secret'], # 2:Secret digest, # 3:FolderTag Pango.EllipsizeMode.END # 4:EllipsizeMode ]) self.add_device_infos(value,digest) self.unlock() self.app_status_to.start() except requests.exceptions.ConnectionError: self.unlock() self.onConnectionError() except requests.exceptions.HTTPError: self.unlock() self.onCommunicationError() def init_devices_controls(self): self.devices = self.builder.get_object('devices_list') self.devices_treeview = self.builder.get_object('devices_tree_view') self.devices_activity_label = self.builder.get_object('devices_activity_label') self.set_treeview_column_widths( self.devices_treeview,self.agent.get_pref('devices_columns',[150,300]) ) self.set_treeview_sort_info( self.devices_treeview, self.agent.get_pref('devices_sortinfo', [0, Gtk.SortType.ASCENDING]) ) def init_transfers_controls(self): self.transfers = self.builder.get_object('transfers_list') self.transfers_treeview = self.builder.get_object('transfers_tree_view') self.transfers_activity_label = self.builder.get_object('transfers_activity_label') # TODO: remove placeholder as soon as the suitable API call permits # a working implementation... self.transfers.append ([ _('Cannot implement due to missing API'), # 0: _('BitTorrent Inc.'), # 1: '', # 2: '', # 3: Pango.EllipsizeMode.END # 4:EllipsizeMode ]) self.set_treeview_column_widths( self.transfers_treeview,self.agent.get_pref('transfers_columns',[300,150,80]) ) def init_history_controls(self): self.history = self.builder.get_object('history_list') self.history_treeview = self.builder.get_object('history_tree_view') self.history_activity_label = self.builder.get_object('history_activity_label') # TODO: remove placeholder as soon as the suitable API call permits # a working implementation... self.history.append ([ _('Now'), # 0: _('Cannot implement due to missing API'), # 1: Pango.EllipsizeMode.END # 4:EllipsizeMode ]) self.set_treeview_column_widths( self.history_treeview,self.agent.get_pref('history_columns',[150]) ) def refresh_app_status(self): try: self.lock() folders = self.agent.get_folders() # forward scan updates existing folders and adds new ones for index, value in enumerate(folders): # see in update_folder_values the insane explanation why # also an md5 digest has to be saved digest = md5.new(value['dir'].encode('latin-1')).hexdigest() if not self.update_folder_values(value): # it must be new (probably added via web interface) - let's add it self.folders.append ([ self.agent.fix_decode(value['dir']), # 0:Folder self.get_folder_info_string(value), # 1:Content value['secret'], # 2:Secret digest, # 3:FolderTag Pango.EllipsizeMode.END # 4:EllipsizeMode ]) self.update_device_infos(value,digest) # reverse scan deletes disappeared folders... for row in self.folders: if not self.folder_exists(folders,row): self.folders.remove(row.iter) self.remove_device_infos(row[2],row[3]) # update transfer status self.update_transfer_status(self.agent.get_speed()) # TODO: fill file list... # but there is still no suitable API call... self.unlock() return True except requests.exceptions.ConnectionError: self.unlock() return self.onConnectionError() except requests.exceptions.HTTPError: self.unlock() return self.onCommunicationError() def init_transfer_status(self): self.update_transfer_status({'upload':0,'download':0}) def update_transfer_status(self,speed): activity = _('{0:.1f} kB/s up, {1:.1f} kB/s down').format(speed['upload'] / 1000, speed['download'] / 1000) self.folders_activity_label.set_label(activity) self.devices_activity_label.set_label(activity) self.transfers_activity_label.set_label(activity) self.history_activity_label.set_label(activity) def update_folder_values(self,value): for row in self.folders: if value['secret'] == row[2]: # found - update information row[1] = self.get_folder_info_string(value) return True elif md5.new(value['dir'].encode('latin-1')).hexdigest() == row[3]: # comparing the md5 digests avoids casting errors due to the # insane encoding fix tecnique # found - secret was changed row[1] = self.get_folder_info_string(value) row[2] = value['secret'] return True # not found return False def folder_exists(self,folders,row): if folders is not None: for index, value in enumerate(folders): if value['secret'] == row[2]: return True elif md5.new(value['dir'].encode('latin-1')).hexdigest() == row[3]: # comparing the md5 digests avoids casting errors due to the # insane encoding fix tecnique return True return False def add_device_infos(self,folder,digest): foldername = self.agent.fix_decode(folder['dir']) peers = self.agent.get_folder_peers(folder['secret']) for index, value in enumerate(peers): self.devices.append ([ self.agent.fix_decode(value['name']), # 0:Device foldername, # 1:Folder self.get_device_info_string(value), # 2:Status folder['secret'], # 3:Secret digest, # 4:FolderTag value['id'], # 5:DeviceTag self.get_device_info_icon_name(value), # 6:ConnectionIconName Pango.EllipsizeMode.END # 7:EllipsizeMode ]) def update_device_infos(self,folder,digest): foldername = self.agent.fix_decode(folder['dir']) peers = self.agent.get_folder_peers(folder['secret']) # forward scan updates existing and adds new for index, value in enumerate(peers): if not self.update_device_values(folder,value,digest): # it must be new - let's add it self.devices.append ([ self.agent.fix_decode(value['name']), # 0:Device foldername, # 1:Folder self.get_device_info_string(value), # 2:Status folder['secret'], # 3:Secret digest, # 4:FolderTag value['id'], # 5:DeviceTag self.get_device_info_icon_name(value), # 6:ConnectionIconName Pango.EllipsizeMode.END # 7:EllipsizeMode ]) # reverse scan deletes disappeared folders... for row in self.devices: if row[3] == folder['secret'] or row[4] == digest: # it's our folder if not self.device_exists(peers,row): self.devices.remove(row.iter) def update_device_values(self,folder,peer,digest): for row in self.devices: if peer['id'] == row[5] and folder['secret'] == row[3]: # found - update information row[0] = self.agent.fix_decode(peer['name']) row[2] = self.get_device_info_string(peer) row[6] = self.get_device_info_icon_name(peer) return True elif peer['id'] == row[5] and digest == row[4]: # found - secret probably changed... row[0] = self.agent.fix_decode(peer['name']) row[2] = self.get_device_info_string(peer) row[3] = folder['secret'] row[6] = self.get_device_info_icon_name(peer) return True # not found return False def remove_device_infos(self,secret,digest=None): for row in self.devices: if secret == row[3]: self.devices.remove(row.iter) elif digest is not None and digest == row[4]: self.devices.remove(row.iter) def device_exists(self,peers,row): for index, value in enumerate(peers): if value['id'] == row[5]: return True return False def get_folder_info_string(self,folder): if folder['error'] == 0: if folder['indexing'] == 0: return _('{0} in {1} files').format(self.sizeof_fmt(folder['size']), str(folder['files'])) else: return _('{0} in {1} files (indexing...)').format(self.sizeof_fmt(folder['size']), str(folder['files'])) else: return self.agent.get_error_message(folder) def get_device_info_icon_name(self,peer): return { 'direct' : 'btsync-gui-direct', 'relay' : 'btsync-gui-cloud' }.get(peer['connection'], 'btsync-gui-unknown') def get_device_info_string(self,peer): if peer['synced'] != 0: dt = datetime.datetime.fromtimestamp(peer['synced']) return _('Synced on {0}').format(dt.strftime("%x %X")) elif peer['download'] == 0 and peer['upload'] != 0: return _('⇧ {0}').format(self.sizeof_fmt(peer['upload'])) elif peer['download'] != 0 and peer['upload'] == 0: return _('⇩ {0}').format(self.sizeof_fmt(peer['download'])) elif peer['download'] != 0 and peer['upload'] != 0: return _('⇧ {0} - ⇩ {1}').format(self.sizeof_fmt(peer['upload']), self.sizeof_fmt(peer['download'])) else: return _('Idle...') def init_preferences_controls(self): self.devname = self.builder.get_object('devname') self.autostart = self.builder.get_object('autostart') self.listeningport = self.builder.get_object('listeningport') self.upnp = self.builder.get_object('upnp') self.limitdn = self.builder.get_object('limitdn') self.limitdnrate = self.builder.get_object('limitdnrate') self.limitup = self.builder.get_object('limitup') self.limituprate = self.builder.get_object('limituprate') def init_preferences_values(self): self.lock() self.attach(self.devname,BtValueDescriptor.new_from('device_name',self.prefs['device_name'])) # self.autostart.set_active(self.prefs[""]); self.autostart.set_sensitive(False) self.attach(self.listeningport,BtValueDescriptor.new_from('listening_port',self.prefs['listening_port'])) self.attach(self.upnp,BtValueDescriptor.new_from('use_upnp',self.prefs['use_upnp'])) self.attach(self.limitdnrate,BtValueDescriptor.new_from('download_limit',self.prefs['download_limit'])) self.attach(self.limituprate,BtValueDescriptor.new_from('upload_limit',self.prefs['upload_limit'])) self.limitdn.set_active(self.prefs['download_limit'] > 0) self.limitup.set_active(self.prefs['upload_limit'] > 0) self.unlock() def get_treeview_column_widths(self,treewidget): columns = treewidget.get_columns() widths = [] for index, value in enumerate(columns): widths.append(value.get_width()) return widths def set_treeview_column_widths(self,treewidget,widths): columns = treewidget.get_columns() for index, value in enumerate(columns): if index < len(widths): value.set_sizing(Gtk.TreeViewColumnSizing.FIXED) value.set_fixed_width(max(widths[index],32)) def get_treeview_sort_info(self,treewidget): treemodel = treewidget.get_model() column_id, sort_order = treemodel.get_sort_column_id() return [column_id, int(sort_order)] def set_treeview_sort_info(self,treewidget,sortinfo): if sortinfo[0] is not None: treemodel = treewidget.get_model() treemodel.set_sort_column_id(sortinfo[0],sortinfo[1]) columns = treewidget.get_columns() for index, value in enumerate(columns): if value.get_sort_column_id() == sortinfo[0]: value.set_sort_order(sortinfo[1]) value.set_sort_indicator(True) return def onDelete(self, *args): width, height = self.window.get_size() self.agent.set_pref('windowsize', (width, height)) self.agent.set_pref('folders_columns', self.get_treeview_column_widths(self.folders_treeview)) self.agent.set_pref('devices_columns', self.get_treeview_column_widths(self.devices_treeview)) self.agent.set_pref('transfers_columns', self.get_treeview_column_widths(self.transfers_treeview)) self.agent.set_pref('history_columns', self.get_treeview_column_widths(self.history_treeview)) self.agent.set_pref('folders_sortinfo', self.get_treeview_sort_info (self.folders_treeview)) self.agent.set_pref('devices_sortinfo', self.get_treeview_sort_info (self.devices_treeview)) self.close() def onSaveEntry(self,widget,valDesc,newValue): try: self.agent.set_prefs({valDesc.Name : newValue}) self.prefs[valDesc.Name] = newValue except requests.exceptions.ConnectionError: return self.onConnectionError() except requests.exceptions.HTTPError: return self.onCommunicationError() return True def onFoldersSelectionChanged(self,selection): model, tree_iter = selection.get_selected() self.folders_remove.set_sensitive(selection.count_selected_rows() > 0) def onFoldersAdd(self,widget): self.dlg = BtSyncFolderAdd(self.agent) try: self.dlg.create() result = self.dlg.run() if result == Gtk.ResponseType.OK: # all checks have already been done. let's go! result = self.agent.add_folder(self.dlg.folder,self.dlg.secret) if self.agent.get_error_code(result) > 0: self.show_warning(self.window,self.agent.get_error_message(result)) except requests.exceptions.ConnectionError: pass except requests.exceptions.HTTPError: pass finally: self.dlg.destroy() self.dlg = None def onFoldersRemove(self,widget): self.dlg = BtSyncFolderRemove() self.dlg.create() result = self.dlg.run() self.dlg.destroy() if result == Gtk.ResponseType.OK: model, tree_iter = self.folders_selection.get_selected() if tree_iter is not None: # ok - let's delete it! secret = model[tree_iter][2] try: result = self.agent.remove_folder(secret) if self.agent.get_error_code(result) == 0: self.folders.remove(tree_iter) self.remove_device_infos(secret) else: logging.error('Failed to remove folder ' + str(secret)) except requests.exceptions.ConnectionError: pass except requests.exceptions.HTTPError: pass def onFoldersMouseClick(self,widget,event): x = int(event.x) y = int(event.y) time = event.time pathinfo = widget.get_path_at_pos(x,y) if pathinfo is not None: if event.button == 1: if event.type == Gdk.EventType._2BUTTON_PRESS or event.type == Gdk.EventType._3BUTTON_PRESS: path, column, cellx, celly = pathinfo widget.grab_focus() widget.set_cursor(path,column,0) model, tree_iter = self.folders_selection.get_selected() if tree_iter is not None: if os.path.isdir(model[tree_iter][0]): os.system('xdg-open "{0}"'.format(model[tree_iter][0])) return True elif event.button == 3: path, column, cellx, celly = pathinfo widget.grab_focus() widget.set_cursor(path,column,0) model, tree_iter = self.folders_selection.get_selected() if self.agent.is_local() and tree_iter is not None: self.folders_menu_openfolder.set_sensitive( os.path.isdir(model[tree_iter][0]) ) self.folders_menu_openarchive.set_sensitive( os.path.isdir(model[tree_iter][0] + '/.SyncArchive') ) self.folders_menu_editsyncignore.set_sensitive( os.path.isfile(model[tree_iter][0] + '/.SyncIgnore') ) else: self.folders_menu_openfolder.set_sensitive(False) self.folders_menu_openarchive.set_sensitive(False) self.folders_menu_editsyncignore.set_sensitive(False) self.folders_menu.popup(None,None,None,None,event.button,time) return True def onFoldersCopySecret(self,widget): model, tree_iter = self.folders_selection.get_selected() if tree_iter is not None: self.clipboard.set_text(model[tree_iter][2], -1) def onFoldersConnectMobile(self,widget): model, tree_iter = self.folders_selection.get_selected() if tree_iter is not None: result = self.agent.get_secrets(model[tree_iter][2], False) if self.agent.get_error_code(result) == 0: self.dlg = BtSyncFolderScanQR( result['read_write'] if result.has_key('read_write') else None, result['read_only'], os.path.basename(model[tree_iter][0]) ) self.dlg.create() result = self.dlg.run() self.dlg.destroy() self.dlg = None def onFoldersOpenFolder(self,widget): model, tree_iter = self.folders_selection.get_selected() if tree_iter is not None: if os.path.isdir(model[tree_iter][0]): os.system('xdg-open "{0}"'.format(model[tree_iter][0])) def onFoldersOpenArchive(self,widget): model, tree_iter = self.folders_selection.get_selected() if tree_iter is not None: syncarchive = model[tree_iter][0] + '/.SyncArchive' if os.path.isdir(syncarchive): os.system('xdg-open "{0}"'.format(syncarchive)) def onFoldersEditSyncIgnore(self,widget): model, tree_iter = self.folders_selection.get_selected() if tree_iter is not None: syncignore = model[tree_iter][0] + '/.SyncIgnore' if os.path.isfile(syncignore): os.system('xdg-open "{0}"'.format(syncignore)) def onFoldersPreferences(self,widget): model, tree_iter = self.folders_selection.get_selected() if tree_iter is not None: self.dlg = BtSyncFolderPrefs(self.agent) try: self.dlg.create(model[tree_iter][0],model[tree_iter][2]) self.dlg.run() except requests.exceptions.ConnectionError: pass except requests.exceptions.HTTPError: pass finally: self.dlg.destroy() self.dlg = None def onPreferencesToggledLimitDn(self,widget): self.limitdnrate.set_sensitive(widget.get_active()) if not self.is_locked(): rate = int(self.limitdnrate.get_text()) if widget.get_active() else 0 try: self.agent.set_prefs({"download_limit" : rate}) self.prefs['download_limit'] = rate except requests.exceptions.ConnectionError: return self.onConnectionError() except requests.exceptions.HTTPError: return self.onCommunicationError() def onPreferencesToggledLimitUp(self,widget): self.limituprate.set_sensitive(widget.get_active()) if not self.is_locked(): rate = int(self.limituprate.get_text()) if widget.get_active() else 0 try: self.agent.set_prefs({"upload_limit" : rate}) self.prefs['upload_limit'] = rate except requests.exceptions.ConnectionError: return self.onConnectionError() except requests.exceptions.HTTPError: return self.onCommunicationError() def onPreferencesClickedAdvanced(self,widget): try: self.dlg = BtSyncPrefsAdvanced(self.agent) self.dlg.run() except requests.exceptions.ConnectionError: logging.error('BtSync API Connection Error') except requests.exceptions.HTTPError: logging.error('BtSync API HTTP error: {0}'.format(self.agent.get_status_code())) except Exception as e: # this should not really happen... logging.error('onPreferencesClickedAdvanced: Unexpected exception caught: '+str(e)) finally: if isinstance(self.dlg, BtSyncPrefsAdvanced): self.dlg.destroy() self.dlg = None def onConnectionError(self): logging.error('BtSync API Connection Error') self.window.destroy() return False def onCommunicationError(self): logging.error('BtSync API HTTP error: {0}'.format(self.agent.get_status_code())) self.window.destroy() return False
class BtSyncApp(BtInputHelper, BtMessageHelper): def __init__(self, agent): self.agent = agent self.builder = Gtk.Builder() self.builder.set_translation_domain('btsync-gui') self.builder.add_from_file( os.path.dirname(__file__) + "/btsyncapp.glade") self.builder.connect_signals(self) width, height = self.agent.get_pref('windowsize', (602, 328)) self.window = self.builder.get_object('btsyncapp') self.window.set_default_size(width, height) self.window.connect('delete-event', self.onDelete) if not self.agent.is_auto(): title = self.window.get_title() self.window.set_title('{0} - ({1}:{2})'.format( title, agent.get_host(), agent.get_port())) self.window.show() self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) self.app_status_to = BtDynamicTimeout(1000, self.refresh_app_status) self.dlg = None self.prefs = self.agent.get_prefs() self.init_folders_controls() self.init_devices_controls() self.init_transfers_controls() self.init_history_controls() self.init_preferences_controls() # TODO: Hide pages not supported by API notebook = self.builder.get_object('notebook1') transfers = notebook.get_nth_page(2) history = notebook.get_nth_page(3) transfers.hide() history.hide() # TODO: End self.init_transfer_status() self.init_folders_values() self.init_preferences_values() def close(self): self.app_status_to.stop() if self.dlg is not None: self.dlg.response(Gtk.ResponseType.CANCEL) def connect_close_signal(self, handler): return self.window.connect('delete-event', handler) def init_folders_controls(self): self.folders = self.builder.get_object('folders_list') self.folders_menu = self.builder.get_object('folders_menu') self.folders_menu_openfolder = self.builder.get_object( 'folder_menu_openfolder') self.folders_menu_openarchive = self.builder.get_object( 'folder_menu_openarchive') self.folders_menu_editsyncignore = self.builder.get_object( 'folder_menu_editsyncignore') self.folders_selection = self.builder.get_object('folders_selection') self.folders_treeview = self.builder.get_object('folders_tree_view') self.folders_activity_label = self.builder.get_object( 'folders_activity_label') self.folders_add = self.builder.get_object('folders_add') self.folders_remove = self.builder.get_object('folders_remove') self.folders_remove.set_sensitive(False) self.set_treeview_column_widths( self.folders_treeview, self.agent.get_pref('folders_columns', [300])) self.set_treeview_sort_info( self.folders_treeview, self.agent.get_pref('folders_sortinfo', [0, Gtk.SortType.ASCENDING])) def init_folders_values(self): try: self.lock() folders = self.agent.get_folders() if folders is not None: for index, value in enumerate(folders): # see in update_folder_values the insane explanation why # also an md5 digest has to be saved digest = md5.new( value['dir'].encode('latin-1')).hexdigest() self.folders.append([ self.agent.fix_decode(value['dir']), # 0:Folder self.get_folder_info_string(value), # 1:Content value['secret'], # 2:Secret digest, # 3:FolderTag Pango.EllipsizeMode.END # 4:EllipsizeMode ]) self.add_device_infos(value, digest) self.unlock() self.app_status_to.start() except requests.exceptions.ConnectionError: self.unlock() self.onConnectionError() except requests.exceptions.HTTPError: self.unlock() self.onCommunicationError() def init_devices_controls(self): self.devices = self.builder.get_object('devices_list') self.devices_treeview = self.builder.get_object('devices_tree_view') self.devices_activity_label = self.builder.get_object( 'devices_activity_label') self.set_treeview_column_widths( self.devices_treeview, self.agent.get_pref('devices_columns', [150, 300])) self.set_treeview_sort_info( self.devices_treeview, self.agent.get_pref('devices_sortinfo', [0, Gtk.SortType.ASCENDING])) def init_transfers_controls(self): self.transfers = self.builder.get_object('transfers_list') self.transfers_treeview = self.builder.get_object( 'transfers_tree_view') self.transfers_activity_label = self.builder.get_object( 'transfers_activity_label') # TODO: remove placeholder as soon as the suitable API call permits # a working implementation... self.transfers.append([ _('Cannot implement due to missing API'), # 0: _('BitTorrent Inc.'), # 1: '', # 2: '', # 3: Pango.EllipsizeMode.END # 4:EllipsizeMode ]) self.set_treeview_column_widths( self.transfers_treeview, self.agent.get_pref('transfers_columns', [300, 150, 80])) def init_history_controls(self): self.history = self.builder.get_object('history_list') self.history_treeview = self.builder.get_object('history_tree_view') self.history_activity_label = self.builder.get_object( 'history_activity_label') # TODO: remove placeholder as soon as the suitable API call permits # a working implementation... self.history.append([ _('Now'), # 0: _('Cannot implement due to missing API'), # 1: Pango.EllipsizeMode.END # 4:EllipsizeMode ]) self.set_treeview_column_widths( self.history_treeview, self.agent.get_pref('history_columns', [150])) def refresh_app_status(self): try: self.lock() folders = self.agent.get_folders() # forward scan updates existing folders and adds new ones for index, value in enumerate(folders): # see in update_folder_values the insane explanation why # also an md5 digest has to be saved digest = md5.new(value['dir'].encode('latin-1')).hexdigest() if not self.update_folder_values(value): # it must be new (probably added via web interface) - let's add it self.folders.append([ self.agent.fix_decode(value['dir']), # 0:Folder self.get_folder_info_string(value), # 1:Content value['secret'], # 2:Secret digest, # 3:FolderTag Pango.EllipsizeMode.END # 4:EllipsizeMode ]) self.update_device_infos(value, digest) # reverse scan deletes disappeared folders... for row in self.folders: if not self.folder_exists(folders, row): self.folders.remove(row.iter) self.remove_device_infos(row[2], row[3]) # update transfer status self.update_transfer_status(self.agent.get_speed()) # TODO: fill file list... # but there is still no suitable API call... self.unlock() return True except requests.exceptions.ConnectionError: self.unlock() return self.onConnectionError() except requests.exceptions.HTTPError: self.unlock() return self.onCommunicationError() def init_transfer_status(self): self.update_transfer_status({'upload': 0, 'download': 0}) def update_transfer_status(self, speed): activity = _('{0:.1f} kB/s up, {1:.1f} kB/s down').format( speed['upload'] / 1000, speed['download'] / 1000) self.folders_activity_label.set_label(activity) self.devices_activity_label.set_label(activity) self.transfers_activity_label.set_label(activity) self.history_activity_label.set_label(activity) def update_folder_values(self, value): for row in self.folders: if value['secret'] == row[2]: # found - update information row[1] = self.get_folder_info_string(value) return True elif md5.new(value['dir'].encode('latin-1')).hexdigest() == row[3]: # comparing the md5 digests avoids casting errors due to the # insane encoding fix tecnique # found - secret was changed row[1] = self.get_folder_info_string(value) row[2] = value['secret'] return True # not found return False def folder_exists(self, folders, row): if folders is not None: for index, value in enumerate(folders): if value['secret'] == row[2]: return True elif md5.new( value['dir'].encode('latin-1')).hexdigest() == row[3]: # comparing the md5 digests avoids casting errors due to the # insane encoding fix tecnique return True return False def add_device_infos(self, folder, digest): foldername = self.agent.fix_decode(folder['dir']) peers = self.agent.get_folder_peers(folder['secret']) for index, value in enumerate(peers): self.devices.append([ self.agent.fix_decode(value['name']), # 0:Device foldername, # 1:Folder self.get_device_info_string(value), # 2:Status folder['secret'], # 3:Secret digest, # 4:FolderTag value['id'], # 5:DeviceTag self.get_device_info_icon_name(value), # 6:ConnectionIconName Pango.EllipsizeMode.END # 7:EllipsizeMode ]) def update_device_infos(self, folder, digest): foldername = self.agent.fix_decode(folder['dir']) peers = self.agent.get_folder_peers(folder['secret']) # forward scan updates existing and adds new for index, value in enumerate(peers): if not self.update_device_values(folder, value, digest): # it must be new - let's add it self.devices.append([ self.agent.fix_decode(value['name']), # 0:Device foldername, # 1:Folder self.get_device_info_string(value), # 2:Status folder['secret'], # 3:Secret digest, # 4:FolderTag value['id'], # 5:DeviceTag self.get_device_info_icon_name( value), # 6:ConnectionIconName Pango.EllipsizeMode.END # 7:EllipsizeMode ]) # reverse scan deletes disappeared folders... for row in self.devices: if row[3] == folder['secret'] or row[4] == digest: # it's our folder if not self.device_exists(peers, row): self.devices.remove(row.iter) def update_device_values(self, folder, peer, digest): for row in self.devices: if peer['id'] == row[5] and folder['secret'] == row[3]: # found - update information row[0] = self.agent.fix_decode(peer['name']) row[2] = self.get_device_info_string(peer) row[6] = self.get_device_info_icon_name(peer) return True elif peer['id'] == row[5] and digest == row[4]: # found - secret probably changed... row[0] = self.agent.fix_decode(peer['name']) row[2] = self.get_device_info_string(peer) row[3] = folder['secret'] row[6] = self.get_device_info_icon_name(peer) return True # not found return False def remove_device_infos(self, secret, digest=None): for row in self.devices: if secret == row[3]: self.devices.remove(row.iter) elif digest is not None and digest == row[4]: self.devices.remove(row.iter) def device_exists(self, peers, row): for index, value in enumerate(peers): if value['id'] == row[5]: return True return False def get_folder_info_string(self, folder): if folder['error'] == 0: if folder['indexing'] == 0: return _('{0} in {1} files').format( self.sizeof_fmt(folder['size']), str(folder['files'])) else: return _('{0} in {1} files (indexing...)').format( self.sizeof_fmt(folder['size']), str(folder['files'])) else: return self.agent.get_error_message(folder) def get_device_info_icon_name(self, peer): return { 'direct': 'btsync-gui-direct', 'relay': 'btsync-gui-cloud' }.get(peer['connection'], 'btsync-gui-unknown') def get_device_info_string(self, peer): if peer['synced'] != 0: dt = datetime.datetime.fromtimestamp(peer['synced']) return _('Synced on {0}').format(dt.strftime("%x %X")) elif peer['download'] == 0 and peer['upload'] != 0: return _('⇧ {0}').format(self.sizeof_fmt(peer['upload'])) elif peer['download'] != 0 and peer['upload'] == 0: return _('⇩ {0}').format(self.sizeof_fmt(peer['download'])) elif peer['download'] != 0 and peer['upload'] != 0: return _('⇧ {0} - ⇩ {1}').format(self.sizeof_fmt(peer['upload']), self.sizeof_fmt(peer['download'])) else: return _('Idle...') def init_preferences_controls(self): self.devname = self.builder.get_object('devname') self.autostart = self.builder.get_object('autostart') self.listeningport = self.builder.get_object('listeningport') self.upnp = self.builder.get_object('upnp') self.limitdn = self.builder.get_object('limitdn') self.limitdnrate = self.builder.get_object('limitdnrate') self.limitup = self.builder.get_object('limitup') self.limituprate = self.builder.get_object('limituprate') def init_preferences_values(self): self.lock() self.attach( self.devname, BtValueDescriptor.new_from('device_name', self.prefs['device_name'])) # self.autostart.set_active(self.prefs[""]); self.autostart.set_sensitive(False) self.attach( self.listeningport, BtValueDescriptor.new_from('listening_port', self.prefs['listening_port'])) self.attach( self.upnp, BtValueDescriptor.new_from('use_upnp', self.prefs['use_upnp'])) self.attach( self.limitdnrate, BtValueDescriptor.new_from('download_limit', self.prefs['download_limit'])) self.attach( self.limituprate, BtValueDescriptor.new_from('upload_limit', self.prefs['upload_limit'])) self.limitdn.set_active(self.prefs['download_limit'] > 0) self.limitup.set_active(self.prefs['upload_limit'] > 0) self.unlock() def get_treeview_column_widths(self, treewidget): columns = treewidget.get_columns() widths = [] for index, value in enumerate(columns): widths.append(value.get_width()) return widths def set_treeview_column_widths(self, treewidget, widths): columns = treewidget.get_columns() for index, value in enumerate(columns): if index < len(widths): value.set_sizing(Gtk.TreeViewColumnSizing.FIXED) value.set_fixed_width(max(widths[index], 32)) def get_treeview_sort_info(self, treewidget): treemodel = treewidget.get_model() column_id, sort_order = treemodel.get_sort_column_id() return [column_id, int(sort_order)] def set_treeview_sort_info(self, treewidget, sortinfo): if sortinfo[0] is not None: treemodel = treewidget.get_model() treemodel.set_sort_column_id(sortinfo[0], sortinfo[1]) columns = treewidget.get_columns() for index, value in enumerate(columns): if value.get_sort_column_id() == sortinfo[0]: value.set_sort_order(sortinfo[1]) value.set_sort_indicator(True) return def onDelete(self, *args): width, height = self.window.get_size() self.agent.set_pref('windowsize', (width, height)) self.agent.set_pref( 'folders_columns', self.get_treeview_column_widths(self.folders_treeview)) self.agent.set_pref( 'devices_columns', self.get_treeview_column_widths(self.devices_treeview)) self.agent.set_pref( 'transfers_columns', self.get_treeview_column_widths(self.transfers_treeview)) self.agent.set_pref( 'history_columns', self.get_treeview_column_widths(self.history_treeview)) self.agent.set_pref('folders_sortinfo', self.get_treeview_sort_info(self.folders_treeview)) self.agent.set_pref('devices_sortinfo', self.get_treeview_sort_info(self.devices_treeview)) self.close() def onSaveEntry(self, widget, valDesc, newValue): try: self.agent.set_prefs({valDesc.Name: newValue}) self.prefs[valDesc.Name] = newValue except requests.exceptions.ConnectionError: return self.onConnectionError() except requests.exceptions.HTTPError: return self.onCommunicationError() return True def onFoldersSelectionChanged(self, selection): model, tree_iter = selection.get_selected() self.folders_remove.set_sensitive(selection.count_selected_rows() > 0) def onFoldersAdd(self, widget): self.dlg = BtSyncFolderAdd(self.agent) try: self.dlg.create() result = self.dlg.run() if result == Gtk.ResponseType.OK: # all checks have already been done. let's go! result = self.agent.add_folder(self.dlg.folder, self.dlg.secret) if self.agent.get_error_code(result) == 105: if self.show_question(self.window, self.agent.get_error_message( result)) == Gtk.ResponseType.YES: result = self.agent.add_folder(self.dlg.folder, self.dlg.secret, force=True) if self.agent.get_error_code(result) > 0: self.show_warning( self.window, self.agent.get_error_message(result)) elif self.agent.get_error_code(result) > 0: self.show_warning(self.window, self.agent.get_error_message(result)) except requests.exceptions.ConnectionError: pass except requests.exceptions.HTTPError: pass finally: self.dlg.destroy() self.dlg = None def onFoldersRemove(self, widget): self.dlg = BtSyncFolderRemove() self.dlg.create() result = self.dlg.run() self.dlg.destroy() if result == Gtk.ResponseType.OK: model, tree_iter = self.folders_selection.get_selected() if tree_iter is not None: # ok - let's delete it! secret = model[tree_iter][2] try: result = self.agent.remove_folder(secret) if self.agent.get_error_code(result) == 0: self.folders.remove(tree_iter) self.remove_device_infos(secret) else: logging.error('Failed to remove folder ' + str(secret)) except requests.exceptions.ConnectionError: pass except requests.exceptions.HTTPError: pass def onFoldersMouseClick(self, widget, event): x = int(event.x) y = int(event.y) time = event.time pathinfo = widget.get_path_at_pos(x, y) if pathinfo is not None: if event.button == 1: if event.type == Gdk.EventType._2BUTTON_PRESS or event.type == Gdk.EventType._3BUTTON_PRESS: path, column, cellx, celly = pathinfo widget.grab_focus() widget.set_cursor(path, column, 0) model, tree_iter = self.folders_selection.get_selected() if tree_iter is not None: if os.path.isdir(model[tree_iter][0]): os.system('xdg-open "{0}"'.format( model[tree_iter][0])) return True elif event.button == 3: path, column, cellx, celly = pathinfo widget.grab_focus() widget.set_cursor(path, column, 0) model, tree_iter = self.folders_selection.get_selected() if self.agent.is_local() and tree_iter is not None: self.folders_menu_openfolder.set_sensitive( os.path.isdir(model[tree_iter][0])) self.folders_menu_openarchive.set_sensitive( os.path.isdir(model[tree_iter][0] + '/.SyncArchive')) self.folders_menu_editsyncignore.set_sensitive( os.path.isfile(model[tree_iter][0] + '/.SyncIgnore')) else: self.folders_menu_openfolder.set_sensitive(False) self.folders_menu_openarchive.set_sensitive(False) self.folders_menu_editsyncignore.set_sensitive(False) self.folders_menu.popup(None, None, None, None, event.button, time) return True def onFoldersCopySecret(self, widget): model, tree_iter = self.folders_selection.get_selected() if tree_iter is not None: self.clipboard.set_text(model[tree_iter][2], -1) def onFoldersConnectMobile(self, widget): model, tree_iter = self.folders_selection.get_selected() if tree_iter is not None: result = self.agent.get_secrets(model[tree_iter][2], False) if self.agent.get_error_code(result) == 0: self.dlg = BtSyncFolderScanQR( result['read_write'] if result.has_key('read_write') else None, result['read_only'], os.path.basename(model[tree_iter][0])) self.dlg.create() result = self.dlg.run() self.dlg.destroy() self.dlg = None def onFoldersOpenFolder(self, widget): model, tree_iter = self.folders_selection.get_selected() if tree_iter is not None: if os.path.isdir(model[tree_iter][0]): os.system('xdg-open "{0}"'.format(model[tree_iter][0])) def onFoldersOpenArchive(self, widget): model, tree_iter = self.folders_selection.get_selected() if tree_iter is not None: syncarchive = model[tree_iter][0] + '/.SyncArchive' if os.path.isdir(syncarchive): os.system('xdg-open "{0}"'.format(syncarchive)) def onFoldersEditSyncIgnore(self, widget): model, tree_iter = self.folders_selection.get_selected() if tree_iter is not None: syncignore = model[tree_iter][0] + '/.SyncIgnore' if os.path.isfile(syncignore): os.system('xdg-open "{0}"'.format(syncignore)) def onFoldersPreferences(self, widget): model, tree_iter = self.folders_selection.get_selected() if tree_iter is not None: self.dlg = BtSyncFolderPrefs(self.agent) try: self.dlg.create(model[tree_iter][0], model[tree_iter][2]) self.dlg.run() except requests.exceptions.ConnectionError: pass except requests.exceptions.HTTPError: pass finally: self.dlg.destroy() self.dlg = None def onPreferencesToggledLimitDn(self, widget): self.limitdnrate.set_sensitive(widget.get_active()) if not self.is_locked(): rate = int( self.limitdnrate.get_text()) if widget.get_active() else 0 try: self.agent.set_prefs({"download_limit": rate}) self.prefs['download_limit'] = rate except requests.exceptions.ConnectionError: return self.onConnectionError() except requests.exceptions.HTTPError: return self.onCommunicationError() def onPreferencesToggledLimitUp(self, widget): self.limituprate.set_sensitive(widget.get_active()) if not self.is_locked(): rate = int( self.limituprate.get_text()) if widget.get_active() else 0 try: self.agent.set_prefs({"upload_limit": rate}) self.prefs['upload_limit'] = rate except requests.exceptions.ConnectionError: return self.onConnectionError() except requests.exceptions.HTTPError: return self.onCommunicationError() def onPreferencesClickedAdvanced(self, widget): try: self.dlg = BtSyncPrefsAdvanced(self.agent) self.dlg.run() except requests.exceptions.ConnectionError: logging.error('BtSync API Connection Error') except requests.exceptions.HTTPError: logging.error('BtSync API HTTP error: {0}'.format( self.agent.get_status_code())) except Exception as e: # this should not really happen... logging.error( 'onPreferencesClickedAdvanced: Unexpected exception caught: ' + str(e)) finally: if isinstance(self.dlg, BtSyncPrefsAdvanced): self.dlg.destroy() self.dlg = None def onConnectionError(self): logging.error('BtSync API Connection Error') self.window.destroy() return False def onCommunicationError(self): logging.error('BtSync API HTTP error: {0}'.format( self.agent.get_status_code())) self.window.destroy() return False
class BtSyncStatus: DISCONNECTED = 0 CONNECTING = 1 CONNECTED = 2 PAUSED = 3 def __init__(self,agent): self.builder = Gtk.Builder() self.builder.set_translation_domain('btsync-gui') self.builder.add_from_file(os.path.dirname(__file__) + "/btsyncstatus.glade") self.builder.connect_signals (self) self.menu = self.builder.get_object('btsyncmenu') self.menuconnection = self.builder.get_object('connectionitem') self.menustatus = self.builder.get_object('statusitem') self.menupause = self.builder.get_object('pausesyncing') self.menudebug = self.builder.get_object('setdebug') self.menuopenweb = self.builder.get_object('openweb') self.menuopenapp = self.builder.get_object('openapp') self.about = self.builder.get_object('aboutdialog') self.ind = TrayIndicator ( 'btsync', 'btsync-gui-disconnected' ) if agent.is_auto(): self.menuconnection.set_visible(False) self.ind.set_title(_('BitTorrent Sync')) self.ind.set_tooltip_text(_('BitTorrent Sync Status Indicator')) else: self.menuconnection.set_label('{0}:{1}'.format(agent.get_host(),agent.get_port())) self.ind.set_title(_('BitTorrent Sync {0}:{1}').format(agent.get_host(),agent.get_port())) self.ind.set_tooltip_text(_('BitTorrent Sync {0}:{1}').format(agent.get_host(),agent.get_port())) self.menuopenweb.set_visible(agent.is_webui()) self.ind.set_menu(self.menu) self.ind.set_default_action(self.onActivate) # icon animator self.frame = 0 self.rotating = False self.transferring = False self.animator_id = None # application window self.app = None # other variables self.connection = BtSyncStatus.DISCONNECTED self.connect_id = None self.status_to = BtDynamicTimeout(1000,self.btsync_refresh_status) self.agent = agent def startup(self): self.btsyncver = { 'version': '0.0.0' } # status if self.agent.is_auto(): self.menupause.set_sensitive(self.agent.is_auto()) if self.agent.is_paused(): self.set_status(BtSyncStatus.PAUSED) self.menupause.set_active(True) else: self.set_status(BtSyncStatus.CONNECTING) self.menupause.set_active(False) self.connect_id = GObject.timeout_add(1000, self.btsync_connect) else: self.set_status(BtSyncStatus.CONNECTING) self.menupause.set_sensitive(False) self.menupause.set_active(False) self.connect_id = GObject.timeout_add(1000, self.btsync_connect) def shutdown(self): if self.animator_id is not None: GObject.source_remove(self.animator_id) if self.connect_id is not None: GObject.source_remove(self.connect_id) self.status_to.stop() def open_app(self): if isinstance(self.app, BtSyncApp): self.app.window.present() else: try: self.app = BtSyncApp(self.agent) self.app.connect_close_signal(self.onDeleteApp) except requests.exceptions.ConnectionError: return self.onConnectionError() except requests.exceptions.HTTPError: return self.onCommunicationError() def close_app(self,stillopen=True): if isinstance(self.app, BtSyncApp): if stillopen: self.app.close() # self.app.window.close() self.app.window.destroy() del self.app self.app = None def btsync_connect(self): if self.connection is BtSyncStatus.DISCONNECTED or \ self.connection is BtSyncStatus.CONNECTING or \ self.connection is BtSyncStatus.PAUSED: try: self.set_status(BtSyncStatus.CONNECTING) self.menustatus.set_label(_('Connecting...')) version = self.agent.get_version() self.btsyncver = version self.set_status(BtSyncStatus.CONNECTED) self.menustatus.set_label(_('Idle')) self.status_to.start() self.connect_id = None return False except requests.exceptions.ConnectionError: self.connect_id = None return self.onConnectionError() except requests.exceptions.HTTPError: self.connect_id = None return self.onCommunicationError() else: logging.info('Cannot connect since I\'m already connected') def btsync_refresh_status(self): if self.connection is not BtSyncStatus.CONNECTED: logging.info('Interrupting refresh sequence...') return False logging.info('Refresh status...') indexing = False transferring = False try: folders = self.agent.get_folders() for fIndex, fValue in enumerate(folders): if fValue['indexing'] > 0: indexing = True # this takes too much resources... # peers = self.agent.get_folder_peers(fValue['secret']) # for pIndex, pValue in enumerate(peers): # if long(pValue['upload']) + long(pValue['download']) > 0: # transferring = True ##### speed = self.agent.get_speed() if transferring or speed['upload'] > 0 or speed['download'] > 0: # there are active transfers... self.set_status(BtSyncStatus.CONNECTED,True) self.menustatus.set_label(_('{0:.1f} kB/s up, {1:.1f} kB/s down').format(speed['upload'] / 1000, speed['download'] / 1000)) elif indexing: self.set_status(BtSyncStatus.CONNECTED) self.menustatus.set_label(_('Indexing...')) else: self.set_status(BtSyncStatus.CONNECTED) self.menustatus.set_label(_('Idle')) return True except requests.exceptions.ConnectionError: return self.onConnectionError() except requests.exceptions.HTTPError: return self.onCommunicationError() def set_status(self,connection,transferring=False): if connection is BtSyncStatus.DISCONNECTED: self.frame = -1 self.transferring = False self.ind.set_from_icon_name('btsync-gui-disconnected') self.menudebug.set_sensitive(False) self.menudebug.set_active(self.agent.get_debug()) self.menuopenapp.set_sensitive(False) self.menuopenweb.set_sensitive(False) elif connection is BtSyncStatus.CONNECTING: self.frame = -1 self.transferring = False self.ind.set_from_icon_name('btsync-gui-connecting') self.menudebug.set_sensitive(False) self.menudebug.set_active(self.agent.get_debug()) self.menuopenapp.set_sensitive(False) self.menuopenweb.set_sensitive(False) elif connection is BtSyncStatus.PAUSED: self.frame = -1 self.transferring = False self.ind.set_from_icon_name('btsync-gui-paused') self.menudebug.set_sensitive(self.agent.is_local()) self.menudebug.set_active(self.agent.get_debug()) self.menuopenapp.set_sensitive(False) self.menuopenweb.set_sensitive(False) else: self.menudebug.set_sensitive(self.agent.is_local()) self.menudebug.set_active(self.agent.get_debug()) self.menuopenapp.set_sensitive(True) self.menuopenweb.set_sensitive(True) if transferring and not self.transferring: if not self.rotating: # initialize animation self.transferring = True self.frame = 0 self.animator_id = GObject.timeout_add(200, self.onIconRotate) self.transferring = transferring if not self.transferring: self.ind.set_from_icon_name('btsync-gui-0') self.connection = connection def show_status(self,statustext): self.menustatus.set_label(statustext) def is_connected(self): return self.connection is BtSyncStatus.CONNECTED def onActivate(self,widget): # self.menu.popup(None,None,Gtk.StatusIcon.position_menu,widget,3,0) if self.is_connected(): self.open_app() def onAbout(self,widget): self.about.set_version(_('Version {0} ({0})').format(self.btsyncver['version'])) self.about.set_comments(_('Linux UI Version {0}').format(VERSION)) self.about.show() self.about.run() self.about.hide() def onOpenApp(self,widget): self.open_app() def onOpenWeb(self,widget): webbrowser.open('http://{0}:{1}@{2}:{3}'.format( urllib.quote(self.agent.get_username(),''), urllib.quote(self.agent.get_password(),''), self.agent.get_host(), self.agent.get_port() ), 2) def onDeleteApp(self, *args): self.close_app(False) def onSendFeedback(self,widget): webbrowser.open( 'http://forum.bittorrent.com/topic/28106-linux-desktop-gui-unofficial-packages-for-bittorrent-sync/', 2 ) def onOpenManual(self,widget): os.system('xdg-open "/usr/share/doc/btsync-common/BitTorrentSyncUserGuide.pdf.gz"') def onTogglePause(self,widget): if widget.get_active() and not self.agent.is_paused(): logging.info('Suspending agent...') self.close_app(); self.set_status(BtSyncStatus.PAUSED) self.agent.suspend() elif not widget.get_active() and self.agent.is_paused(): logging.info('Resuming agent...') self.set_status(BtSyncStatus.CONNECTING) self.agent.resume() self.connect_id = GObject.timeout_add(1000, self.btsync_connect) def onToggleLogging(self,widget): if self.is_connected(): if widget.get_active() and not self.agent.get_debug(): logging.info('Activate logging...') self.agent.set_debug(True) elif not widget.get_active() and self.agent.get_debug(): logging.info('Disable logging...') self.agent.set_debug(False) def onQuit(self,widget): Gtk.main_quit() def onIconRotate(self): if self.frame == -1: # immediate stop self.frame = 0 self.rotating = False self.animator_id = None return False elif not self.transferring and self.frame % 12 == 0: # do not stop immediately - wait for the # cycle to finish. self.ind.set_from_icon_name('btsync-gui-0') self.rotating = False self.frame = 0 self.animator_id = None return False else: self.ind.set_from_icon_name('btsync-gui-{0}'.format(self.frame % 12)) self.rotating = True self.frame += 1 return True def onConnectionError(self): self.set_status(BtSyncStatus.DISCONNECTED) self.menustatus.set_label(_('Disconnected')) self.close_app(); logging.info('BtSync API Connection Error') if self.agent.is_auto() and not self.agent.is_running(): logging.warning('BitTorrent Sync seems to be crashed. Restarting...') self.agent.start_agent() self.connect_id = GObject.timeout_add(1000, self.btsync_connect) else: self.connect_id = GObject.timeout_add(5000, self.btsync_connect) return False def onCommunicationError(self): self.set_status(BtSyncStatus.DISCONNECTED) self.menustatus.set_label(_('Disconnected: Communication Error {0}').format(self.agent.get_status_code())) self.close_app(); logging.warning('BtSync API HTTP error: {0}'.format(self.agent.get_status_code())) self.connect_id = GObject.timeout_add(5000, self.btsync_connect) return False