diff --git a/ChangeLog b/ChangeLog index 1137f2a94..e6bc6a863 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,3 +7,4 @@ Deluge 1.1.0 - "" (In Development) GtkUI: * Add peer progress to the peers tab * Sorting # column will place downloaders above seeds + * Remove dependency on libtorrent for add torrent dialog diff --git a/deluge/bencode.py b/deluge/bencode.py new file mode 100644 index 000000000..d82a1efbd --- /dev/null +++ b/deluge/bencode.py @@ -0,0 +1,128 @@ +# The contents of this file are subject to the BitTorrent Open Source License +# Version 1.1 (the License). You may not copy or use this file, in either +# source code or executable form, except in compliance with the License. You +# may obtain a copy of the License at http://www.bittorrent.com/license/. +# +# Software distributed under the License is distributed on an AS IS basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. + +# Written by Petru Paler + +def decode_int(x, f): + f += 1 + newf = x.index('e', f) + n = int(x[f:newf]) + if x[f] == '-': + if x[f + 1] == '0': + raise ValueError + elif x[f] == '0' and newf != f+1: + raise ValueError + return (n, newf+1) + +def decode_string(x, f): + colon = x.index(':', f) + n = int(x[f:colon]) + if x[f] == '0' and colon != f+1: + raise ValueError + colon += 1 + return (x[colon:colon+n], colon+n) + +def decode_list(x, f): + r, f = [], f+1 + while x[f] != 'e': + v, f = decode_func[x[f]](x, f) + r.append(v) + return (r, f + 1) + +def decode_dict(x, f): + r, f = {}, f+1 + while x[f] != 'e': + k, f = decode_string(x, f) + r[k], f = decode_func[x[f]](x, f) + return (r, f + 1) + +decode_func = {} +decode_func['l'] = decode_list +decode_func['d'] = decode_dict +decode_func['i'] = decode_int +decode_func['0'] = decode_string +decode_func['1'] = decode_string +decode_func['2'] = decode_string +decode_func['3'] = decode_string +decode_func['4'] = decode_string +decode_func['5'] = decode_string +decode_func['6'] = decode_string +decode_func['7'] = decode_string +decode_func['8'] = decode_string +decode_func['9'] = decode_string + +def bdecode(x): + try: + r, l = decode_func[x[0]](x, 0) + except (IndexError, KeyError, ValueError): + raise Exception("not a valid bencoded string") + if l != len(x): + raise Exception("invalid bencoded value (data after valid prefix)") + return r + +from types import StringType, IntType, LongType, DictType, ListType, TupleType + + +class Bencached(object): + + __slots__ = ['bencoded'] + + def __init__(self, s): + self.bencoded = s + +def encode_bencached(x,r): + r.append(x.bencoded) + +def encode_int(x, r): + r.extend(('i', str(x), 'e')) + +def encode_bool(x, r): + if x: + encode_int(1, r) + else: + encode_int(0, r) + +def encode_string(x, r): + r.extend((str(len(x)), ':', x)) + +def encode_list(x, r): + r.append('l') + for i in x: + encode_func[type(i)](i, r) + r.append('e') + +def encode_dict(x,r): + r.append('d') + ilist = x.items() + ilist.sort() + for k, v in ilist: + r.extend((str(len(k)), ':', k)) + encode_func[type(v)](v, r) + r.append('e') + +encode_func = {} +encode_func[Bencached] = encode_bencached +encode_func[IntType] = encode_int +encode_func[LongType] = encode_int +encode_func[StringType] = encode_string +encode_func[ListType] = encode_list +encode_func[TupleType] = encode_list +encode_func[DictType] = encode_dict + +try: + from types import BooleanType + encode_func[BooleanType] = encode_bool +except ImportError: + pass + +def bencode(x): + r = [] + encode_func[type(x)](x, r) + return ''.join(r) diff --git a/deluge/ui/gtkui/addtorrentdialog.py b/deluge/ui/gtkui/addtorrentdialog.py index 13a40d1c1..345335a5b 100644 --- a/deluge/ui/gtkui/addtorrentdialog.py +++ b/deluge/ui/gtkui/addtorrentdialog.py @@ -2,24 +2,24 @@ # addtorrentdialog.py # # Copyright (C) 2007 Andrew Resch ('andar') -# +# # Deluge is free software. -# +# # You may redistribute it and/or modify it under the terms of the # GNU General Public License, as published by the Free Software # Foundation; either version 3 of the License, or (at your option) # any later version. -# +# # deluge is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with deluge. If not, write to: -# The Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor -# Boston, MA 02110-1301, USA. +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. # # In addition, as a special exception, the copyright holders give # permission to link the code of portions of this program with the OpenSSL @@ -52,11 +52,11 @@ class AddTorrentDialog(component.Component): self.glade = gtk.glade.XML( pkg_resources.resource_filename( "deluge.ui.gtkui", "glade/add_torrent_dialog.glade")) - + self.dialog = self.glade.get_widget("dialog_add_torrent") - + self.dialog.connect("delete-event", self._on_delete_event) - + self.glade.signal_autoconnect({ "on_button_file_clicked": self._on_button_file_clicked, "on_button_url_clicked": self._on_button_url_clicked, @@ -74,15 +74,15 @@ class AddTorrentDialog(component.Component): self.files_treestore = gtk.TreeStore(bool, str, gobject.TYPE_UINT64, gobject.TYPE_INT64, bool, str) self.files_treestore.set_sort_column_id(1, gtk.SORT_ASCENDING) - + # Holds the files info self.files = {} self.infos = {} self.core_config = {} self.options = {} self.previous_selected_torrent = None - - + + self.listview_torrents = self.glade.get_widget("listview_torrents") self.listview_files = self.glade.get_widget("listview_files") @@ -110,12 +110,12 @@ class AddTorrentDialog(component.Component): column.pack_start(render) column.set_cell_data_func(render, listview.cell_data_size, 2) self.listview_files.append_column(column) - + self.listview_torrents.set_model(self.torrent_liststore) self.listview_files.set_model(self.files_treestore) self.listview_files.get_selection().set_mode(gtk.SELECTION_MULTIPLE) - self.listview_torrents.get_selection().connect("changed", + self.listview_torrents.get_selection().connect("changed", self._on_torrent_changed) # Get default config values from the core @@ -130,10 +130,10 @@ class AddTorrentDialog(component.Component): "add_paused" ] self.core_config = {} - + def start(self): self.update_core_config() - + def show(self, focus=False): self.update_core_config() @@ -142,15 +142,15 @@ class AddTorrentDialog(component.Component): self.glade.get_widget("entry_download_path").hide() else: self.glade.get_widget("button_location").hide() - self.glade.get_widget("entry_download_path").show() - - self.dialog.set_transient_for(component.get("MainWindow").window) + self.glade.get_widget("entry_download_path").show() + + self.dialog.set_transient_for(component.get("MainWindow").window) self.dialog.present() if focus: self.dialog.window.focus() - + return None - + def hide(self): self.dialog.hide() self.files = {} @@ -159,9 +159,9 @@ class AddTorrentDialog(component.Component): self.previous_selected_torrent = None self.torrent_liststore.clear() self.files_treestore.clear() - self.dialog.set_transient_for(component.get("MainWindow").window) + self.dialog.set_transient_for(component.get("MainWindow").window) return None - + def update_core_config(self): self.core_config = {} # Send requests to the core for these config values @@ -171,77 +171,72 @@ class AddTorrentDialog(component.Component): # Force a call to the core because we need this data now client.force_call() self.set_default_options() - + def _on_config_value(self, value): for key in self.core_keys: if not self.core_config.has_key(key): self.core_config[key] = value break - + def add_from_files(self, filenames): - import deluge.libtorrent as lt + import deluge.bencode import os.path new_row = None - + for filename in filenames: # Get the torrent data from the torrent file try: log.debug("Attempting to open %s for add.", filename) - _file = open(filename, "rb") - filedump = _file.read() - if not filedump: - log.warning("Torrent appears to be corrupt!") - continue - filedump = lt.bdecode(filedump) - _file.close() + metadata = deluge.bencode.bdecode(open(filename, "rb").read()) except Exception, e: log.warning("Unable to open %s: %s", filename, e) continue - try: - info = lt.torrent_info(filedump) - except RuntimeError, e: - log.warning("Torrent appears to be corrupt!") - continue + from sha import sha + info_hash = sha(deluge.bencode.bencode(metadata["info"])).hexdigest() - if str(info.info_hash()) in self.infos: + if info_hash in self.infos: log.debug("Torrent already in list!") continue - + # Get list of files from torrent info files = [] - for f in info.files(): + prefix = "" + if len(metadata["info"]["files"]) > 1: + prefix = metadata["info"]["name"] + + for f in metadata["info"]["files"]: files.append({ - 'path': f.path, - 'size': f.size, + 'path': os.path.join(prefix, *f["path"]), + 'size': f["length"], 'download': True }) - name = "%s (%s)" % (info.name(), os.path.split(filename)[-1]) + name = "%s (%s)" % (metadata["info"]["name"], os.path.split(filename)[-1]) new_row = self.torrent_liststore.append( - [str(info.info_hash()), name, filename]) - self.files[str(info.info_hash())] = files - self.infos[str(info.info_hash())] = info + [info_hash, name, filename]) + self.files[info_hash] = files + self.infos[info_hash] = metadata (model, row) = self.listview_torrents.get_selection().get_selected() if not row and new_row: self.listview_torrents.get_selection().select_iter(new_row) - + def _on_torrent_changed(self, treeselection): (model, row) = treeselection.get_selected() self.files_treestore.clear() - + if row is None: return - - # Update files list + + # Update files list files_list = self.files[model.get_value(row, 0)] self.prepare_file_store(files_list) if self.core_config == {}: self.update_core_config() - + # Save the previous torrents options self.save_torrent_options() # Update the options frame @@ -258,6 +253,7 @@ class AddTorrentDialog(component.Component): self.add_files(None, split_files) def prepare_file(self, file, file_name, file_num, files_storage): + log.debug("file_name: %s", file_name) first_slash_index = file_name.find("/") if first_slash_index == -1: files_storage[file_name] = (file_num, file) @@ -287,16 +283,16 @@ class AddTorrentDialog(component.Component): if torrent_id not in self.options: self.set_default_options() return - + options = self.options[torrent_id] - + if client.is_localhost(): self.glade.get_widget("button_location").set_current_folder( options["download_location"]) else: self.glade.get_widget("entry_download_path").set_text( options["download_location"]) - + self.glade.get_widget("radio_compact").set_active( options["compact_allocation"]) self.glade.get_widget("spin_maxdown").set_value( @@ -311,7 +307,7 @@ class AddTorrentDialog(component.Component): options["add_paused"]) self.glade.get_widget("chk_prioritize").set_active( options["prioritize_first_last_pieces"]) - + def save_torrent_options(self, row=None): # Keeps the torrent options dictionary up-to-date with what the user has # selected. @@ -319,10 +315,10 @@ class AddTorrentDialog(component.Component): row = self.previous_selected_torrent if row is None or not self.torrent_liststore.iter_is_valid(row): return - + torrent_id = self.torrent_liststore.get_value(row, 0) - - options = {} + + options = {} if client.is_localhost(): options["download_location"] = \ self.glade.get_widget("button_location").get_current_folder() @@ -345,16 +341,16 @@ class AddTorrentDialog(component.Component): self.glade.get_widget("chk_prioritize").get_active() options["default_private"] = \ self.glade.get_widget("chk_private").get_active() - + self.options[torrent_id] = options - + # Save the file priorities files_priorities = self.build_priorities( self.files_treestore.get_iter_first(), {}) - + for i, file_dict in enumerate(self.files[torrent_id]): file_dict["download"] = files_priorities[i] - + def build_priorities(self, iter, priorities): while iter is not None: if self.files_treestore.iter_has_child(iter): @@ -372,7 +368,7 @@ class AddTorrentDialog(component.Component): else: self.glade.get_widget("entry_download_path").set_text( self.core_config["download_location"]) - + self.glade.get_widget("radio_compact").set_active( self.core_config["compact_allocation"]) self.glade.get_widget("spin_maxdown").set_value( @@ -387,7 +383,7 @@ class AddTorrentDialog(component.Component): self.core_config["add_paused"]) self.glade.get_widget("chk_prioritize").set_active( self.core_config["prioritize_first_last_pieces"]) - + def get_file_priorities(self, torrent_id): # A list of priorities files_list = [] @@ -397,9 +393,9 @@ class AddTorrentDialog(component.Component): files_list.append(0) else: files_list.append(1) - + return files_list - + def _on_file_toggled(self, render, path): (model, paths) = self.listview_files.get_selection().get_selected_rows() if len(paths) > 1: @@ -449,13 +445,13 @@ class AddTorrentDialog(component.Component): chooser = gtk.FileChooserDialog(_("Choose a .torrent file"), None, gtk.FILE_CHOOSER_ACTION_OPEN, - buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, + buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)) chooser.set_transient_for(self.dialog) chooser.set_select_multiple(True) chooser.set_property("skip-taskbar-hint", True) - + # Add .torrent and * file filters file_filter = gtk.FileFilter() file_filter.set_name(_("Torrent files")) @@ -483,7 +479,7 @@ class AddTorrentDialog(component.Component): chooser.destroy() self.add_from_files(result) - + def _on_button_url_clicked(self, widget): log.debug("_on_button_url_clicked") dialog = self.glade.get_widget("url_dialog") @@ -492,13 +488,13 @@ class AddTorrentDialog(component.Component): dialog.set_default_response(gtk.RESPONSE_OK) dialog.set_transient_for(self.dialog) entry.grab_focus() - + if deluge.common.windows_check(): - import win32clipboard as clip + import win32clipboard as clip import win32con - clip.OpenClipboard() - text = clip.GetClipboardData(win32con.CF_UNICODETEXT) - clip.CloseClipboard() + clip.OpenClipboard() + text = clip.GetClipboardData(win32con.CF_UNICODETEXT) + clip.CloseClipboard() else: clip = gtk.clipboard_get(selection='PRIMARY') text = clip.wait_for_text() @@ -520,7 +516,7 @@ class AddTorrentDialog(component.Component): log.debug("url: %s", url) if url != None: self.add_from_url(url) - + entry.set_text("") dialog.hide() @@ -535,7 +531,7 @@ class AddTorrentDialog(component.Component): filename, headers = urllib.urlretrieve(url, tmp_file) log.debug("filename: %s", filename) self.add_from_files([filename]) - + def _on_button_hash_clicked(self, widget): log.debug("_on_button_hash_clicked") @@ -544,9 +540,9 @@ class AddTorrentDialog(component.Component): (model, row) = self.listview_torrents.get_selection().get_selected() if row is None: return - + torrent_id = model.get_value(row, 0) - + model.remove(row) del self.files[torrent_id] del self.infos[torrent_id] @@ -567,7 +563,7 @@ class AddTorrentDialog(component.Component): torrent_filenames = [] torrent_options = [] - + row = self.torrent_liststore.get_iter_first() while row != None: torrent_id = self.torrent_liststore.get_value(row, 0) @@ -576,16 +572,16 @@ class AddTorrentDialog(component.Component): options = self.options[torrent_id] except: options = None - + file_priorities = self.get_file_priorities(torrent_id) if options != None: options["file_priorities"] = file_priorities - + torrent_filenames.append(filename) torrent_options.append(options) - + row = self.torrent_liststore.iter_next(row) - + client.add_torrent_file(torrent_filenames, torrent_options) client.force_call() self.hide() @@ -595,25 +591,25 @@ class AddTorrentDialog(component.Component): (model, row) = self.listview_torrents.get_selection().get_selected() if row is None: return - + self.save_torrent_options(row) - + # The options we want all the torrents to have options = self.options[model.get_value(row, 0)] - + # Set all the torrent options row = model.get_iter_first() while row != None: torrent_id = model.get_value(row, 0) self.options[torrent_id] = options row = model.iter_next(row) - + def _on_button_revert_clicked(self, widget): log.debug("_on_button_revert_clicked") (model, row) = self.listview_torrents.get_selection().get_selected() if row is None: return - + del self.options[model.get_value(row, 0)] self.set_default_options()