Opened 12 years ago

Closed 9 years ago

#2093 closed bug (Fixed)

[win32] 'open-with' does not work for non-acsii torrents

Reported by: IAmTheClaw Owned by: Calum
Priority: minor Milestone: 1.3.13
Component: Core Version: 1.3.5
Keywords: Cc:


The UI cannot open .torrent files with Unicode characters via the OS "open file with" or via download. In the former case, the UI silently ignores the error and in the latter case, it says it cannot find the file. Downloading the .torrent to disk and then opening the file through the "Add Torrent" dialog works, however.

OS: Windows 7 x64

Deluge daemon and client are both version 1.3.5.

Change History (23)

comment:1 by Calum, 12 years ago

Milestone: 1.3.x1.3.6
Owner: set to Calum
Status: newassigned

I have verified that the open with issue exists and it is a problem with windows non-ascii chars passed as python args.

There is a stackoverflow workaround that seems to work but it may affect other parts of the code so will need further testing.

For reference: ctypes.wintypes module will need including if not done automatically by bbfreeze.

comment:2 by globeaddict, 12 years ago

Summary: [win32] 'open-with' does not work for non-acsii torrents[win32] 'open-with' does not work for non-acsii torrents deleted

Same problem on Linux.

When using french tracker, some torrents are named with accented characters (éàè...). The problem occurs both with autoadd plugin and by adding it manually. The torrent downloads as expected but doesn't appear anywhere in my torrent list. And when I try to display the list of labelled torrents in which it belongs, nothing happens, as well as "all" label list.

The solution is to stop autoadd plugin and rename the torrent before adding it manually.

comment:3 by Calum, 12 years ago

Summary: [win32] 'open-with' does not work for non-acsii torrents deleted[win32] 'open-with' does not work for non-acsii torrents

@globeaddict: That is a different issue see #1983

comment:4 by Chase, 12 years ago

This seems to work, tested on Windows 7 x64. Some other people should test it though, I'm never quite sure with this encoding stuff.

  • deluge/ui/gtkui/

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP
    <+>#\n#\n#\n# Copyright (C) 2007 Andrew Resch <>\n#\n# Deluge is free software.\n#\n# You may redistribute it and/or modify it under the terms of the\n# GNU General Public License, as published by the Free Software\n# Foundation; either version 3 of the License, or (at your option)\n# any later version.\n#\n# deluge is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with deluge.    If not, write to:\n#   The Free Software Foundation, Inc.,\n#   51 Franklin Street, Fifth Floor\n#   Boston, MA  02110-1301, USA.\n#\n#    In addition, as a special exception, the copyright holders give\n#    permission to link the code of portions of this program with the OpenSSL\n#    library.\n#    You must obey the GNU General Public License in all respects for all of\n#    the code used other than OpenSSL. If you modify file(s) with this\n#    exception, you may extend this exception to your version of the file(s),\n#    but you are not obligated to do so. If you do not wish to do so, delete\n#    this exception statement from your version. If you delete this exception\n#    statement from all source files in the program, then also delete it here.\n#\n#\n\n\nimport pygtk\npygtk.require('2.0')\nimport gtk\nimport gobject\nimport base64\nimport logging\nimport os\nfrom urlparse import urljoin\n\nimport twisted.web.client\nimport twisted.web.error\nfrom deluge.ui.client import client\nfrom deluge.httpdownloader import download_file\nimport deluge.component as component\nimport listview\nfrom deluge.configmanager import ConfigManager\nimport deluge.common\nimport deluge.ui.common\nimport dialogs\nimport common\n\nlog = logging.getLogger(__name__)\n\nclass AddTorrentDialog(component.Component):\n    def __init__(self):\n        component.Component.__init__(self, \"AddTorrentDialog\")\n        self.builder = gtk.Builder()\n        # The base dialog\n        self.builder.add_from_file(deluge.common.resource_filename(\n            \"deluge.ui.gtkui\", os.path.join(\"glade\", \"add_torrent_dialog.ui\")\n        ))\n        # The infohash dialog\n        self.builder.add_from_file(deluge.common.resource_filename(\n            \"deluge.ui.gtkui\", os.path.join(\"glade\", \"add_torrent_dialog.infohash.ui\")\n        ))\n        # The url dialog\n        self.builder.add_from_file(deluge.common.resource_filename(\n            \"deluge.ui.gtkui\", os.path.join(\"glade\", \"add_torrent_dialog.url.ui\")\n        ))\n\n        self.dialog = self.builder.get_object(\"dialog_add_torrent\")\n\n        self.dialog.connect(\"delete-event\", self._on_delete_event)\n\n        self.builder.connect_signals({\n            \"on_button_file_clicked\": self._on_button_file_clicked,\n            \"on_button_url_clicked\": self._on_button_url_clicked,\n            \"on_button_hash_clicked\": self._on_button_hash_clicked,\n            \"on_button_remove_clicked\": self._on_button_remove_clicked,\n            \"on_button_trackers_clicked\": self._on_button_trackers_clicked,\n            \"on_button_cancel_clicked\": self._on_button_cancel_clicked,\n            \"on_button_add_clicked\": self._on_button_add_clicked,\n            \"on_button_apply_clicked\": self._on_button_apply_clicked,\n            \"on_button_revert_clicked\": self._on_button_revert_clicked,\n            \"on_alocation_toggled\": self._on_alocation_toggled,\n            \"on_chk_move_completed_toggled\": self._on_chk_move_completed_toggled\n        })\n\n        self.torrent_liststore = gtk.ListStore(str, str, str)\n        #download?, path, filesize, sequence number, inconsistent?\n        self.files_treestore = gtk.TreeStore(bool, str, gobject.TYPE_UINT64,\n                                        gobject.TYPE_INT64, bool, str)\n        self.files_treestore.set_sort_column_id(1, gtk.SORT_ASCENDING)\n\n        # Holds the files info\n        self.files = {}\n        self.infos = {}\n        self.core_config = {}\n        self.options = {}\n\n        self.previous_selected_torrent = None\n\n\n        self.listview_torrents = self.builder.get_object(\"listview_torrents\")\n        self.listview_files = self.builder.get_object(\"listview_files\")\n\n        render = gtk.CellRendererText()\n        column = gtk.TreeViewColumn(_(\"Torrent\"), render, text=1)\n        self.listview_torrents.append_column(column)\n\n        render = gtk.CellRendererToggle()\n        render.connect(\"toggled\", self._on_file_toggled)\n        column = gtk.TreeViewColumn(None, render, active=0, inconsistent=4)\n        self.listview_files.append_column(column)\n\n        column = gtk.TreeViewColumn(_(\"Filename\"))\n        render = gtk.CellRendererPixbuf()\n        column.pack_start(render, False)\n        column.add_attribute(render, \"stock-id\", 5)\n        render = gtk.CellRendererText()\n        render.set_property(\"editable\", True)\n        render.connect(\"edited\", self._on_filename_edited)\n        column.pack_start(render, True)\n        column.add_attribute(render, \"text\", 1)\n        column.set_expand(True)\n        self.listview_files.append_column(column)\n\n        render = gtk.CellRendererText()\n        column = gtk.TreeViewColumn(_(\"Size\"))\n        column.pack_start(render)\n        column.set_cell_data_func(render, listview.cell_data_size, 2)\n        self.listview_files.append_column(column)\n\n        self.listview_torrents.set_model(self.torrent_liststore)\n        self.listview_files.set_model(self.files_treestore)\n\n        self.listview_files.get_selection().set_mode(gtk.SELECTION_MULTIPLE)\n        self.listview_torrents.get_selection().connect(\"changed\",\n                                    self._on_torrent_changed)\n\n        # Get default config values from the core\n        self.core_keys = [\n            \"compact_allocation\",\n            \"max_connections_per_torrent\",\n            \"max_upload_slots_per_torrent\",\n            \"max_upload_speed_per_torrent\",\n            \"max_download_speed_per_torrent\",\n            \"prioritize_first_last_pieces\",\n            \"sequential_download\",\n            \"download_location\",\n            \"add_paused\",\n            \"move_completed\",\n            \"move_completed_path\"\n        ]\n        self.core_config = {}\n\n        self.builder.get_object(\"notebook1\").connect(\"switch-page\", self._on_switch_page)\n\n    def start(self):\n        self.update_core_config()\n\n    def show(self, focus=False):\n        return self.update_core_config(True, focus)\n\n    def _show(self, focus=False):\n        if client.is_localhost():\n            self.builder.get_object(\"button_location\").show()\n            self.builder.get_object(\"entry_download_path\").hide()\n            self.builder.get_object(\"button_move_completed_location\").show()\n            self.builder.get_object(\"entry_move_completed_path\").hide()\n        else:\n            self.builder.get_object(\"button_location\").hide()\n            self.builder.get_object(\"entry_download_path\").show()\n            self.builder.get_object(\"button_move_completed_location\").hide()\n            self.builder.get_object(\"entry_move_completed_path\").show()\n\n        self.dialog.set_transient_for(component.get(\"MainWindow\").window)\n        self.dialog.present()\n        if focus:\n            self.dialog.window.focus()\n\n        return None\n\n    def hide(self):\n        self.dialog.hide()\n        self.files = {}\n        self.infos = {}\n        self.options = {}\n        self.previous_selected_torrent = None\n        self.torrent_liststore.clear()\n        self.files_treestore.clear()\n        self.dialog.set_transient_for(component.get(\"MainWindow\").window)\n        return None\n\n    def update_core_config(self, show=False, focus=False):\n        def _on_config_values(config):\n            self.core_config = config\n            self.set_default_options()\n            if show:\n                self._show(focus)\n\n        # Send requests to the core for these config values\n        return client.core.get_config_values(self.core_keys).addCallback(_on_config_values)\n\n    def add_from_files(self, filenames):\n        import os.path\n        new_row = None\n\n        for filename in filenames:\n            # Convert the path to unicode\n            filename = unicode(filename)\n\n            # Get the torrent data from the torrent file\n            try:\n                info = deluge.ui.common.TorrentInfo(filename)\n            except Exception, e:\n                log.debug(\"Unable to open torrent file: %s\", e)\n                dialogs.ErrorDialog(_(\"Invalid File\"), e, self.dialog).run()\n                continue\n\n            if info.info_hash in self.files:\n                log.debug(\"Trying to add a duplicate torrent!\")\n                dialogs.ErrorDialog(\n                    _(\"Duplicate Torrent\"),\n                    _(\"You cannot add the same torrent twice.\"),\n                    self.dialog\n                ).run()\n                continue\n\n            name = \"%s (%s)\" % (, os.path.split(filename)[-1])\n            new_row = self.torrent_liststore.append(\n                [info.info_hash,, filename])\n            self.files[info.info_hash] = info.files\n            self.infos[info.info_hash] = info.filedata\n            self.listview_torrents.get_selection().select_iter(new_row)\n\n            self.set_default_options()\n            self.save_torrent_options(new_row)\n\n        (model, row) = self.listview_torrents.get_selection().get_selected()\n        if not row and new_row:\n            self.listview_torrents.get_selection().select_iter(new_row)\n\n    def add_from_magnets(self, uris):\n        import base64\n        new_row = None\n\n        for uri in uris:\n            s = uri.split(\"&\")[0][20:]\n            if len(s) == 32:\n                info_hash = base64.b32decode(s).encode(\"hex\")\n            elif len(s) == 40:\n                info_hash = s\n            if info_hash in self.infos:\n                log.debug(\"Torrent already in list!\")\n                continue\n            name = None\n            for i in uri.split(\"&\"):\n                if i[:3] == \"dn=\":\n                    name = \"%s (%s)\" % (i.split(\"=\")[1], uri)\n            if not name:\n                name = uri\n            new_row = self.torrent_liststore.append(\n                [info_hash, name, uri])\n            self.files[info_hash] = []\n            self.infos[info_hash] = None\n            self.listview_torrents.get_selection().select_iter(new_row)\n            self.set_default_options()\n            self.save_torrent_options(new_row)\n\n        (model, row) = self.listview_torrents.get_selection().get_selected()\n        if not row and new_row:\n            self.listview_torrents.get_selection().select_iter(new_row)\n\n    def _on_torrent_changed(self, treeselection):\n        (model, row) = treeselection.get_selected()\n        if row is None or not model.iter_is_valid(row):\n            self.files_treestore.clear()\n            self.previous_selected_torrent = None\n            return\n\n        if model[row][0] not in self.files:\n            self.files_treestore.clear()\n            self.previous_selected_torrent = None\n            return\n\n        # Save the previous torrents options\n        self.save_torrent_options()\n        # Update files list\n        files_list = self.files[model.get_value(row, 0)]\n\n        self.prepare_file_store(files_list)\n\n        if self.core_config == {}:\n            self.update_core_config()\n\n        # Update the options frame\n        self.update_torrent_options(model.get_value(row, 0))\n\n        self.previous_selected_torrent = row\n\n    def _on_switch_page(self, widget, page, page_num):\n        # Save the torrent options when switching notebook pages\n        self.save_torrent_options()\n\n    def prepare_file_store(self, files):\n        self.listview_files.set_model(None)\n        self.files_treestore.clear()\n        split_files = { }\n        i = 0\n        for file in files:\n            self.prepare_file(\n                file, file[\"path\"], i, file[\"download\"], split_files\n            )\n            i += 1\n        self.add_files(None, split_files)\n        self.listview_files.set_model(self.files_treestore)\n        self.listview_files.expand_row(\"0\", False)\n\n    def prepare_file(self, file, file_name, file_num, download, files_storage):\n        first_slash_index = file_name.find(os.path.sep)\n        if first_slash_index == -1:\n            files_storage[file_name] = (file_num, file, download)\n        else:\n            file_name_chunk = file_name[:first_slash_index+1]\n            if file_name_chunk not in files_storage:\n                files_storage[file_name_chunk] = { }\n            self.prepare_file(file, file_name[first_slash_index+1:],\n                              file_num, download, files_storage[file_name_chunk])\n\n    def add_files(self, parent_iter, split_files):\n        ret = 0\n        for key,value in split_files.iteritems():\n            if key.endswith(os.path.sep):\n                chunk_iter = self.files_treestore.append(parent_iter,\n                                [True, key, 0, -1, False, gtk.STOCK_DIRECTORY])\n                chunk_size = self.add_files(chunk_iter, value)\n                self.files_treestore.set(chunk_iter, 2, chunk_size)\n                ret += chunk_size\n            else:\n                self.files_treestore.append(parent_iter, [\n                    value[2], key, value[1][\"size\"],\n                    value[0], False, gtk.STOCK_FILE\n                ])\n\n                if parent_iter and self.files_treestore.iter_has_child(parent_iter):\n                    # Iterate through the children and see what we should label the\n                    # folder, download true, download false or inconsistent.\n                    itr = self.files_treestore.iter_children(parent_iter)\n                    download = []\n                    download_value = False\n                    inconsistent = False\n                    while itr:\n                        download.append(self.files_treestore.get_value(itr, 0))\n                        itr = self.files_treestore.iter_next(itr)\n\n                    if sum(download) == len(download):\n                        download_value = True\n                    elif sum(download) == 0:\n                        download_value = False\n                    else:\n                        inconsistent = True\n\n                    self.files_treestore.set_value(parent_iter, 0, download_value)\n                    self.files_treestore.set_value(parent_iter, 4, inconsistent)\n\n                ret += value[1][\"size\"]\n        return ret\n\n    def update_torrent_options(self, torrent_id):\n        if torrent_id not in self.options:\n            self.set_default_options()\n            return\n\n        options = self.options[torrent_id]\n\n        if client.is_localhost():\n            self.builder.get_object(\"button_location\").set_current_folder(\n                options[\"download_location\"])\n            self.builder.get_object(\"button_move_completed_location\").set_current_folder(\n                options[\"move_completed_path\"])\n        else:\n            self.builder.get_object(\"entry_download_path\").set_text(\n                options[\"download_location\"])\n            self.builder.get_object(\"entry_move_completed_path\").set_text(\n                options[\"move_completed_path\"])\n\n        self.builder.get_object(\"radio_full\").set_active(\n            not options[\"compact_allocation\"])\n        self.builder.get_object(\"radio_compact\").set_active(\n            options[\"compact_allocation\"])\n        self.builder.get_object(\"spin_maxdown\").set_value(\n            options[\"max_download_speed\"])\n        self.builder.get_object(\"spin_maxup\").set_value(\n            options[\"max_upload_speed\"])\n        self.builder.get_object(\"spin_maxconnections\").set_value(\n            options[\"max_connections\"])\n        self.builder.get_object(\"spin_maxupslots\").set_value(\n            options[\"max_upload_slots\"])\n        self.builder.get_object(\"chk_paused\").set_active(\n            options[\"add_paused\"])\n        self.builder.get_object(\"chk_prioritize\").set_active(\n            options[\"prioritize_first_last_pieces\"])\n        self.builder.get_object(\"chk_sequential_download\").set_active(\n            options[\"sequential_download\"])\n        self.builder.get_object(\"chk_move_completed\").set_active(\n            options[\"move_completed\"])\n\n    def save_torrent_options(self, row=None):\n        # Keeps the torrent options dictionary up-to-date with what the user has\n        # selected.\n        if row is None:\n            if self.previous_selected_torrent and \\\n                self.torrent_liststore.iter_is_valid(self.previous_selected_torrent):\n                row = self.previous_selected_torrent\n            else:\n                return\n\n        torrent_id = self.torrent_liststore.get_value(row, 0)\n\n        if torrent_id in self.options:\n            options = self.options[torrent_id]\n        else:\n            options = {}\n\n        if client.is_localhost():\n            options[\"download_location\"] = \\\n                self.builder.get_object(\"button_location\").get_filename()\n            options[\"move_completed_path\"] = \\\n                self.builder.get_object(\"button_move_completed_location\").get_filename()\n        else:\n            options[\"download_location\"] = \\\n                self.builder.get_object(\"entry_download_path\").get_text()\n            options[\"move_completed_path\"] = \\\n                self.builder.get_object(\"entry_move_completed_path\").get_text()\n        options[\"compact_allocation\"] = \\\n            self.builder.get_object(\"radio_compact\").get_active()\n\n        if options[\"compact_allocation\"]:\n            # We need to make sure all the files are set to download\n            def set_download_true(model, path, itr):\n                model[path][0] = True\n            self.files_treestore.foreach(set_download_true)\n            self.update_treeview_toggles(self.files_treestore.get_iter_first())\n\n        options[\"max_download_speed\"] = \\\n            self.builder.get_object(\"spin_maxdown\").get_value()\n        options[\"max_upload_speed\"] = \\\n            self.builder.get_object(\"spin_maxup\").get_value()\n        options[\"max_connections\"] = \\\n            self.builder.get_object(\"spin_maxconnections\").get_value_as_int()\n        options[\"max_upload_slots\"] = \\\n            self.builder.get_object(\"spin_maxupslots\").get_value_as_int()\n        options[\"add_paused\"] = \\\n            self.builder.get_object(\"chk_paused\").get_active()\n        options[\"prioritize_first_last_pieces\"] = \\\n            self.builder.get_object(\"chk_prioritize\").get_active()\n        options[\"sequential_download\"] = \\\n            self.builder.get_object(\"radio_full\").get_active() and \\\n            self.builder.get_object(\"chk_sequential_download\").get_active() or False\n        options[\"move_completed\"] = \\\n            self.builder.get_object(\"chk_move_completed\").get_active()\n\n        self.options[torrent_id] = options\n\n        # Save the file priorities\n        files_priorities = self.build_priorities(\n            self.files_treestore.get_iter_first(), {}\n        )\n\n        if len(files_priorities) > 0:\n            for i, file_dict in enumerate(self.files[torrent_id]):\n                file_dict[\"download\"] = files_priorities[i]\n\n    def build_priorities(self, iter, priorities):\n        while iter is not None:\n            if self.files_treestore.iter_has_child(iter):\n                self.build_priorities(self.files_treestore.iter_children(iter),\n                                          priorities)\n            elif not self.files_treestore.get_value(iter, 1).endswith(os.path.sep):\n                priorities[self.files_treestore.get_value(iter, 3)] = \\\n                                        self.files_treestore.get_value(iter, 0)\n            iter = self.files_treestore.iter_next(iter)\n        return priorities\n\n    def set_default_options(self):\n        if client.is_localhost():\n            self.builder.get_object(\"button_location\").set_current_folder(\n                self.core_config[\"download_location\"])\n            self.builder.get_object(\"button_move_completed_location\").set_current_folder(\n                self.core_config[\"move_completed_path\"])\n        else:\n            self.builder.get_object(\"entry_download_path\").set_text(\n                self.core_config[\"download_location\"])\n            self.builder.get_object(\"entry_move_completed_path\").set_text(\n                self.core_config[\"move_completed_path\"])\n\n        self.builder.get_object(\"radio_compact\").set_active(\n            self.core_config[\"compact_allocation\"])\n        self.builder.get_object(\"radio_full\").set_active(\n            not self.core_config[\"compact_allocation\"])\n        self.builder.get_object(\"spin_maxdown\").set_value(\n            self.core_config[\"max_download_speed_per_torrent\"])\n        self.builder.get_object(\"spin_maxup\").set_value(\n            self.core_config[\"max_upload_speed_per_torrent\"])\n        self.builder.get_object(\"spin_maxconnections\").set_value(\n            self.core_config[\"max_connections_per_torrent\"])\n        self.builder.get_object(\"spin_maxupslots\").set_value(\n            self.core_config[\"max_upload_slots_per_torrent\"])\n        self.builder.get_object(\"chk_paused\").set_active(\n            self.core_config[\"add_paused\"])\n        self.builder.get_object(\"chk_prioritize\").set_active(\n            self.core_config[\"prioritize_first_last_pieces\"])\n        self.builder.get_object(\"chk_sequential_download\").set_active(\n            self.core_config[\"sequential_download\"])\n        self.builder.get_object(\"chk_move_completed\").set_active(\n            self.core_config[\"move_completed\"])\n\n    def get_file_priorities(self, torrent_id):\n        # A list of priorities\n        files_list = []\n\n        for file_dict in self.files[torrent_id]:\n            if not file_dict[\"download\"]:\n                files_list.append(0)\n            else:\n                files_list.append(1)\n\n        return files_list\n\n    def _on_file_toggled(self, render, path):\n        # Check to see if we can change file priorities\n        (model, row) = self.listview_torrents.get_selection().get_selected()\n        if self.options[model[row][0]][\"compact_allocation\"]:\n            def on_answer(response):\n                if response == gtk.RESPONSE_YES:\n                    self.options[model[row][0]][\"compact_allocation\"] = False\n                    self.update_torrent_options(model[row][0])\n\n            d = dialogs.YesNoDialog(\n                _(\"Unable to set file priority!\"),\n                _(\"File prioritization is unavailable when using Compact \"\n                  \"allocation.  Would you like to switch to Full allocation?\"),\n                self.dialog\n            ).run()\n            d.addCallback(on_answer)\n\n            return\n        (model, paths) = self.listview_files.get_selection().get_selected_rows()\n        if len(paths) > 1:\n            for path in paths:\n                row = model.get_iter(path)\n                self.toggle_iter(row)\n        else:\n            row = model.get_iter(path)\n            self.toggle_iter(row)\n        self.update_treeview_toggles(self.files_treestore.get_iter_first())\n\n    def toggle_iter(self, iter, toggle_to=None):\n        if toggle_to is None:\n            toggle_to = not self.files_treestore.get_value(iter, 0)\n        self.files_treestore.set_value(iter, 0, toggle_to)\n        if self.files_treestore.iter_has_child(iter):\n            child = self.files_treestore.iter_children(iter)\n            while child is not None:\n                self.toggle_iter(child, toggle_to)\n                child = self.files_treestore.iter_next(child)\n\n    def update_treeview_toggles(self, iter):\n        TOGGLE_INCONSISTENT = -1\n        this_level_toggle = None\n        while iter is not None:\n            if self.files_treestore.iter_has_child(iter):\n                toggle = self.update_treeview_toggles(\n                        self.files_treestore.iter_children(iter))\n                if toggle == TOGGLE_INCONSISTENT:\n                    self.files_treestore.set_value(iter, 4, True)\n                else:\n                    self.files_treestore.set_value(iter, 0, toggle)\n                    #set inconsistent to false\n                    self.files_treestore.set_value(iter, 4, False)\n            else:\n                toggle = self.files_treestore.get_value(iter, 0)\n            if this_level_toggle is None:\n                this_level_toggle = toggle\n            elif this_level_toggle != toggle:\n                this_level_toggle = TOGGLE_INCONSISTENT\n            iter = self.files_treestore.iter_next(iter)\n        return this_level_toggle\n\n    def _on_button_file_clicked(self, widget):\n        log.debug(\"_on_button_file_clicked\")\n        # Setup the filechooserdialog\n        chooser = gtk.FileChooserDialog(\n            _(\"Choose a .torrent file\"),\n            None,\n            gtk.FILE_CHOOSER_ACTION_OPEN,\n            buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN,\n                     gtk.RESPONSE_OK)\n        )\n\n        chooser.set_transient_for(self.dialog)\n        chooser.set_select_multiple(True)\n        chooser.set_property(\"skip-taskbar-hint\", True)\n        chooser.set_local_only(False)\n\n        # Add .torrent and * file filters\n        file_filter = gtk.FileFilter()\n        file_filter.set_name(_(\"Torrent files\"))\n        file_filter.add_pattern(\"*.\" + \"torrent\")\n        chooser.add_filter(file_filter)\n        file_filter = gtk.FileFilter()\n        file_filter.set_name(_(\"All files\"))\n        file_filter.add_pattern(\"*\")\n        chooser.add_filter(file_filter)\n\n        # Load the 'default_load_path' from the config\n        self.config = ConfigManager(\"gtkui.conf\")\n        if self.config[\"default_load_path\"] is not None:\n            chooser.set_current_folder(self.config[\"default_load_path\"])\n\n        # Run the dialog\n        response =\n\n        if response == gtk.RESPONSE_OK:\n            result = chooser.get_filenames()\n            self.config[\"default_load_path\"] = chooser.get_current_folder()\n        else:\n            chooser.destroy()\n            return\n\n        chooser.destroy()\n        self.add_from_files(result)\n\n    def _on_button_url_clicked(self, widget):\n        log.debug(\"_on_button_url_clicked\")\n        dialog = self.builder.get_object(\"url_dialog\")\n        entry = self.builder.get_object(\"entry_url\")\n\n        dialog.set_default_response(gtk.RESPONSE_OK)\n        dialog.set_transient_for(self.dialog)\n        entry.grab_focus()\n\n        text = (gtk.clipboard_get(selection='PRIMARY').wait_for_text() or\n                gtk.clipboard_get().wait_for_text())\n        if text:\n            text = text.strip()\n            if deluge.common.is_url(text) or deluge.common.is_magnet(text):\n                entry.set_text(text)\n\n        dialog.show_all()\n        response =\n\n        if response == gtk.RESPONSE_OK:\n            url = entry.get_text().decode(\"utf-8\")\n        else:\n            url = None\n\n        entry.set_text(\"\")\n        dialog.hide()\n\n        # This is where we need to fetch the .torrent file from the URL and\n        # add it to the list.\n        log.debug(\"url: %s\", url)\n        if url:\n            if deluge.common.is_url(url):\n                self.add_from_url(url)\n            elif deluge.common.is_magnet(url):\n                self.add_from_magnets([url])\n            else:\n                dialogs.ErrorDialog(\n                    _(\"Invalid URL\"),\n                    \"%s %s\" % (url, _(\"is not a valid URL.\")),\n                    self.dialog\n                ).run()\n\n    def add_from_url(self, url):\n        dialog = gtk.Dialog(\n            _(\"Downloading...\"),\n            flags=gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_NO_SEPARATOR,\n            parent=self.dialog)\n        dialog.set_transient_for(self.dialog)\n\n        pb = gtk.ProgressBar()\n        dialog.vbox.pack_start(pb, True, True)\n        dialog.show_all()\n\n        # Create a tmp file path\n        import tempfile\n        import os.path\n        tmp_file = os.path.join(tempfile.gettempdir(), url.split(\"/\")[-1])\n\n        def on_part(data, current_length, total_length):\n            if total_length:\n                percent = float(current_length) / float(total_length)\n                pb.set_fraction(percent)\n                pb.set_text(\"%.2f%% (%s / %s)\" % (\n                    percent * 100,\n                    deluge.common.fsize(current_length),\n                    deluge.common.fsize(total_length)))\n            else:\n                pb.pulse()\n                pb.set_text(\"%s\" % deluge.common.fsize(current_length))\n\n        def on_download_success(result):\n            log.debug(\"Download success!\")\n            self.add_from_files([result])\n            dialog.destroy()\n\n        def on_download_fail(result):\n            if result.check(twisted.web.error.PageRedirect):\n                new_url = urljoin(url, result.getErrorMessage().split(\" to \")[1])\n                result = download_file(new_url, tmp_file, on_part)\n                result.addCallbacks(on_download_success, on_download_fail)\n            elif result.check(twisted.web.client.PartialDownloadError):\n                result = download_file(url, tmp_file, on_part, allow_compression=False)\n                result.addCallbacks(on_download_success, on_download_fail)\n            else:\n                log.debug(\"Download failed: %s\", result)\n                dialog.destroy()\n                dialogs.ErrorDialog(\n                    _(\"Download Failed\"), \"%s %s\" % (_(\"Failed to download:\"), url),\n                    details=result.getErrorMessage(), parent=self.dialog\n                ).run()\n            return result\n\n        d = download_file(url, tmp_file, on_part)\n        d.addCallbacks(on_download_success, on_download_fail)\n\n    def _on_button_hash_clicked(self, widget):\n        log.debug(\"_on_button_hash_clicked\")\n        dialog = self.builder.get_object(\"dialog_infohash\")\n        entry = self.builder.get_object(\"entry_hash\")\n        textview = self.builder.get_object(\"text_trackers\")\n\n        dialog.set_default_response(gtk.RESPONSE_OK)\n        dialog.set_transient_for(self.dialog)\n        entry.grab_focus()\n        dialog.show_all()\n        response =\n        if response == gtk.RESPONSE_OK and len(entry.get_text()) == 40:\n            trackers = []\n            b = textview.get_buffer()\n            lines = b.get_text(b.get_start_iter(), b.get_end_iter()).strip().split(\"\\n\")\n            log.debug(\"lines: %s\", lines)\n            for l in lines:\n                if deluge.common.is_url(l):\n                    trackers.append(l)\n            # Convert the information to a magnet uri, this is just easier to\n            # handle this way.\n            log.debug(\"trackers: %s\", trackers)\n            magnet = deluge.common.create_magnet_uri(\n                infohash=entry.get_text().decode(\"utf-8\"),\n                trackers=trackers)\n            log.debug(\"magnet uri: %s\", magnet)\n            self.add_from_magnets([magnet])\n\n        entry.set_text(\"\")\n        textview.get_buffer().set_text(\"\")\n        dialog.hide()\n\n    def _on_button_remove_clicked(self, widget):\n        log.debug(\"_on_button_remove_clicked\")\n        (model, row) = self.listview_torrents.get_selection().get_selected()\n        if row is None:\n            return\n\n        torrent_id = model.get_value(row, 0)\n\n        model.remove(row)\n        del self.files[torrent_id]\n        del self.infos[torrent_id]\n\n    def _on_button_trackers_clicked(self, widget):\n        log.debug(\"_on_button_trackers_clicked\")\n\n    def _on_button_cancel_clicked(self, widget):\n        log.debug(\"_on_button_cancel_clicked\")\n        self.hide()\n\n    def _on_button_add_clicked(self, widget):\n        log.debug(\"_on_button_add_clicked\")\n        # Save the options for selected torrent prior to adding\n        (model, row) = self.listview_torrents.get_selection().get_selected()\n        if row is not None:\n            self.save_torrent_options(row)\n\n        row = self.torrent_liststore.get_iter_first()\n        while row != None:\n            torrent_id = self.torrent_liststore.get_value(row, 0)\n            filename = self.torrent_liststore.get_value(row, 2)\n            try:\n                options = self.options[torrent_id]\n            except KeyError:\n                options = None\n\n            file_priorities = self.get_file_priorities(torrent_id)\n            if options != None:\n                options[\"file_priorities\"] = file_priorities\n\n            if deluge.common.is_magnet(filename):\n                del options[\"file_priorities\"]\n                client.core.add_torrent_magnet(filename, options)\n            else:\n                client.core.add_torrent_file(\n                    os.path.split(filename)[-1],\n                    base64.encodestring(self.infos[torrent_id]),\n                    options)\n\n            row = self.torrent_liststore.iter_next(row)\n\n        self.hide()\n\n    def _on_button_apply_clicked(self, widget):\n        log.debug(\"_on_button_apply_clicked\")\n        (model, row) = self.listview_torrents.get_selection().get_selected()\n        if row is None:\n            return\n\n        self.save_torrent_options(row)\n\n        # The options we want all the torrents to have\n        options = self.options[model.get_value(row, 0)]\n\n        # Set all the torrent options\n        row = model.get_iter_first()\n        while row != None:\n            torrent_id = model.get_value(row, 0)\n            self.options[torrent_id] = options\n            row = model.iter_next(row)\n\n    def _on_button_revert_clicked(self, widget):\n        log.debug(\"_on_button_revert_clicked\")\n        (model, row) = self.listview_torrents.get_selection().get_selected()\n        if row is None:\n            return\n\n        del self.options[model.get_value(row, 0)]\n        self.set_default_options()\n\n    def _on_chk_move_completed_toggled(self, widget):\n        value = widget.get_active()\n        self.builder.get_object(\"button_move_completed_location\").set_sensitive(value)\n        self.builder.get_object(\"entry_move_completed_path\").set_sensitive(value)\n\n    def _on_delete_event(self, widget, event):\n        self.hide()\n        return True\n\n    def get_file_path(self, row, path=\"\"):\n        if not row:\n            return path\n\n        path = self.files_treestore[row][1] + path\n        return self.get_file_path(self.files_treestore.iter_parent(row), path)\n\n    def _on_filename_edited(self, renderer, path, new_text):\n        index = self.files_treestore[path][3]\n\n        new_text = new_text.strip(os.path.sep)\n\n        # Return if the text hasn't changed\n        if new_text == self.files_treestore[path][1]:\n            return\n\n        # Get the tree iter\n        itr = self.files_treestore.get_iter(path)\n\n        # Get the torrent_id\n        (model, row) = self.listview_torrents.get_selection().get_selected()\n        torrent_id = model[row][0]\n\n        if \"mapped_files\" not in self.options[torrent_id]:\n            self.options[torrent_id][\"mapped_files\"] = {}\n\n        if index > -1:\n            # We're renaming a file! Yay! That's easy!\n            parent = self.files_treestore.iter_parent(itr)\n            file_path = os.path.join(self.get_file_path(parent), new_text)\n\n            if os.path.sep in new_text:\n                # There are folders in this path, so we need to create them\n                # and then move the file iter to top\n                split_text = new_text.split(os.path.sep)\n                for s in split_text[:-1]:\n                    parent = self.files_treestore.append(parent,\n                                [True, s, 0, -1, False, gtk.STOCK_DIRECTORY])\n\n                self.files_treestore[itr][1] = split_text[-1]\n                common.reparent_iter(self.files_treestore, itr, parent)\n            else:\n                # Update the row's text\n                self.files_treestore[itr][1] = new_text\n\n            # Update the mapped_files dict in the options with the index and new\n            # file path.\n            # We'll send this to the core when adding the torrent so it knows\n            # what to rename before adding.\n            self.options[torrent_id][\"mapped_files\"][index] = file_path\n            self.files[torrent_id][index]['path'] = file_path\n        else:\n            # Folder!\n            def walk_tree(row):\n                if not row:\n                    return\n\n                # Get the file path base once, since it will be the same for\n                # all siblings\n                file_path_base = self.get_file_path(\n                    self.files_treestore.iter_parent(row)\n                )\n\n                # Iterate through all the siblings at this level\n                while row:\n                    # We recurse if there are children\n                    if self.files_treestore.iter_has_child(row):\n                        walk_tree(self.files_treestore.iter_children(row))\n\n                    index = self.files_treestore[row][3]\n\n                    if index > -1:\n                        # Get the new full path for this file\n                        file_path = file_path_base + self.files_treestore[row][1]\n\n                        # Update the file path in the mapped_files dict\n                        self.options[torrent_id][\"mapped_files\"][index] = file_path\n                        self.files[torrent_id][index]['path'] = file_path\n\n                    # Get the next siblings iter\n                    row = self.files_treestore.iter_next(row)\n\n            # Update the treestore row first so that when walking the tree\n            # we can construct the new proper paths\n\n            # We need to check if this folder has been split\n            if os.path.sep in new_text:\n                # It's been split, so we need to add new folders and then re-parent\n                # itr.\n                parent = self.files_treestore.iter_parent(itr)\n                split_text = new_text.split(os.path.sep)\n                for s in split_text[:-1]:\n                    # We don't iterate over the last item because we'll just use\n                    # the existing itr and change the text\n                    parent = self.files_treestore.append(parent, [\n                        True, s + os.path.sep, 0, -1, False, gtk.STOCK_DIRECTORY\n                    ])\n\n                self.files_treestore[itr][1] = split_text[-1] + os.path.sep\n\n                # Now re-parent itr to parent\n                common.reparent_iter(self.files_treestore, itr, parent)\n                itr = parent\n\n                # We need to re-expand the view because it might contracted\n                # if we change the root iter\n                self.listview_files.expand_row(\"0\", False)\n            else:\n                # This was a simple folder rename without any splits, so just\n                # change the path for itr\n                self.files_treestore[itr][1] = new_text + os.path.sep\n\n            # Walk through the tree from 'itr' and add all the new file paths\n            # to the 'mapped_files' option\n            walk_tree(itr)\n\n    def _on_alocation_toggled(self, widget):\n        full_allocation_active = self.builder.get_object(\"radio_full\").get_active()\n        self.builder.get_object(\"chk_prioritize\").set_sensitive(full_allocation_active)\n        self.builder.get_object(\"chk_sequential_download\").set_sensitive(full_allocation_active)\n
    4141import base64
    4242import logging
    4343import os
     44import sys
    4445from urlparse import urljoin
    4647import twisted.web.client
    215216        for filename in filenames:
    216217            # Convert the path to unicode
    217             filename = unicode(filename)
     218            filename = unicode(filename, sys.getfilesystemencoding())
    219220            # Get the torrent data from the torrent file
    220221            try:

comment:5 by Chase, 12 years ago

That patch was on master, I just tried 1.3-stable, and it seems like it's already working. This commit seems to have fixed it 370035ffc5e I think there should probably be a sys.getfilesystemencoding() in there though, or at least 'mbcs' if it's on windows, as windows can use a configurable encoding, looks like mine just happens to be latin-1.

comment:7 by Calum, 12 years ago

I'm afraid the issue is not in the add dialog but in ipcinterface because optparse can't handle the unicode filenames as arguments. The only solution for 1.3 is to add the associated code from stackoverflow and in git master maybe move to argparse however that would force py2.7 requirement.

comment:8 by Chase, 12 years ago

Ok, pretend all my other comments don't exist. Turns out my old way just worked with characters which were in the ansi code page which windows uses in its backwards compatible api. I believe I am much closer to the real solution now in this branch (here's the diff) It works when calling deluge-gtk with unicode arguments, however in my testing it didn't work when double clicking a torrent from the system. I can't figure out how that could be, so maybe I'm testing wrong. It has the added benefit of also allowing unicode config directory and log file paths. This branch also needs to be tested on linux (and osx) to make sure nothing got messed up.

comment:9 by Chase, 12 years ago

I think for 1.3-stable maybe we just keep the commit Cas already added, which should allow torrent/path names within the ansi code page (essentially latin-1.) Decoding all arguments to unicode, like my proposed branch, is a bit more dramatic change. Ultimately these problems should all be solved on python 3 (without hacks,) if/when we go that route. The problem with python 2.x is that the standard library uses the backwards compatible Windows api in several places, which does not allow for the full unicode support that Windows uses natively.

comment:10 by Chase, 12 years ago

So, it turns out the problem with file association opening is that when using the .exe instead of the python script to launch, the arguments already come through with the question marks. I am unsure if this is also the case with the .exes generated by bbfreeze as well. (I was testing with the ones made by Other than that, everything is running as it should on the new branch (on Windows.)

comment:11 by Chase, 12 years ago

I verified that exes from bbfreeze do not have the same problem as the exe wrappers generated by, everything should work fine with this branch once it's made into a bbfrozen package.

comment:12 by Calum, 12 years ago

@gazpachoking So there is not going to be any fix applied to 1.3-stable?

For testing, ticket #1388 has steps to reproduce the issue.

comment:13 by Chase, 12 years ago

Well, it's already partially fixed in 1.3-stable, at least latin1 characters work now instead of just ascii. We could certainly apply the full fix to 1.3-stable too, I was just a little worried about unforeseen repercussions of going to entirely unicode arguments.

comment:14 by Calum, 12 years ago


comment:15 by Chase, 12 years ago

Resolution: fixed
Status: assignedclosed

Fixed in master c2d301bf52

comment:16 by zertap, 10 years ago

Cc: added
Resolution: Fixed
Status: closedreopened

On 1.4.0dev, code from around 28th Dec 2013 (from the development branch).

Still cannot open .torrent files with unicode in their names. Doubleclicking, dragging into window, dragging onto .exe, opening from browser nor opening with "Add Torrent" dialog do work.

Last edited 10 years ago by zertap (previous) (diff)

comment:17 by Calum, 10 years ago

@zertap, We need more info, do you have an error log for the problems you are seeing?

comment:18 by Calum, 10 years ago

@gazpachoking: Could you have a look at this, a forum user (Win 7) has a different trace to what I am seeing on XP for 1.3.7:

comment:19 by Calum, 10 years ago

Oh and I also backported decode_string so you could substitute it in the 1.3 code change.

comment:20 by Torbjörn Lönnemark, 9 years ago

This is all you get in the log when double-clicking the torrent in explorer, using deluge 1.3.11 running with the UI connected to a local daemon (not classic mode). Actual filename is ☃.torrent.

[DEBUG   ] 17:47:41 ipcinterface:181 Processing args from other process: ['O:\\?.torrent']
[DEBUG   ] 17:47:41 configmanager:111 Getting config 'gtkui.conf'
[DEBUG   ] 17:47:41 ipcinterface:192 arg: O:\?.torrent
[DEBUG   ] 17:47:41 ipcinterface:211 Attempting to add file (O:\?.torrent) from external source...
[ERROR   ] 17:47:41 ipcinterface:217 No such file: O:\?.torrent

Using the Add Torrent dialog works here too.

comment:21 by Calum, 9 years ago

Milestone: 2.0.x1.3.13

I shall have a look at testing this before next 1.3 release, probably backport the complete fix from develop.

in reply to:  20 comment:22 by Tydus, 9 years ago

Replying to tobbez:

This is all you get in the log when double-clicking the torrent in explorer, using deluge 1.3.11 running with the UI connected to a local daemon (not classic mode). Actual filename is ☃.torrent.

[DEBUG   ] 17:47:41 ipcinterface:181 Processing args from other process: ['O:\\?.torrent']
[DEBUG   ] 17:47:41 configmanager:111 Getting config 'gtkui.conf'
[DEBUG   ] 17:47:41 ipcinterface:192 arg: O:\?.torrent
[DEBUG   ] 17:47:41 ipcinterface:211 Attempting to add file (O:\?.torrent) from external source...
[ERROR   ] 17:47:41 ipcinterface:217 No such file: O:\?.torrent

Using the Add Torrent dialog works here too.

Indeed this is a further issue. If there is unicode-only characters (e.g. "・") in the file path, it will be encoded to "?" (question mark) in mbcs.

The link above gives a more robust solution than decoding from sys.argv using mbcs, though it requires WinAPI support.

comment:23 by Calum, 9 years ago

Resolution: Fixed
Status: reopenedclosed

Already was testing the fix from develop and have now pushed it to 1.3-stable: [9f3b2f3167fad0a]

Note: See TracTickets for help on using tickets.