| | 1 | # |
| | 2 | # manage.py |
| | 3 | # |
| | 4 | # Copyright (C) 2008-2009 Ido Abramovich <ido.deluge@gmail.com> |
| | 5 | # Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> |
| | 6 | # |
| | 7 | # Deluge is free software. |
| | 8 | # |
| | 9 | # You may redistribute it and/or modify it under the terms of the |
| | 10 | # GNU General Public License, as published by the Free Software |
| | 11 | # Foundation; either version 3 of the License, or (at your option) |
| | 12 | # any later version. |
| | 13 | # |
| | 14 | # deluge is distributed in the hope that it will be useful, |
| | 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| | 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| | 17 | # See the GNU General Public License for more details. |
| | 18 | # |
| | 19 | # You should have received a copy of the GNU General Public License |
| | 20 | # along with deluge. If not, write to: |
| | 21 | # The Free Software Foundation, Inc., |
| | 22 | # 51 Franklin Street, Fifth Floor |
| | 23 | # Boston, MA 02110-1301, USA. |
| | 24 | # |
| | 25 | # In addition, as a special exception, the copyright holders give |
| | 26 | # permission to link the code of portions of this program with the OpenSSL |
| | 27 | # library. |
| | 28 | # You must obey the GNU General Public License in all respects for all of |
| | 29 | # the code used other than OpenSSL. If you modify file(s) with this |
| | 30 | # exception, you may extend this exception to your version of the file(s), |
| | 31 | # but you are not obligated to do so. If you do not wish to do so, delete |
| | 32 | # this exception statement from your version. If you delete this exception |
| | 33 | # statement from all source files in the program, then also delete it here. |
| | 34 | # |
| | 35 | # |
| | 36 | |
| | 37 | from deluge.ui.console.main import BaseCommand |
| | 38 | import deluge.ui.console.colors as colors |
| | 39 | from deluge.ui.client import client |
| | 40 | import deluge.component as component |
| | 41 | from deluge.log import LOG as log |
| | 42 | |
| | 43 | from optparse import make_option |
| | 44 | |
| | 45 | |
| | 46 | # torrent.py's Torrent.get_status() renames some of the options. |
| | 47 | # This maps the getter names to the setter names |
| | 48 | get_torrent_options = { |
| | 49 | 'max_download_speed': 'max_download_speed', |
| | 50 | 'max_upload_speed': 'max_upload_speed', |
| | 51 | 'max_connections': 'max_connections', |
| | 52 | 'max_upload_slots': 'max_upload_slots', |
| | 53 | 'prioritize_first_last': 'prioritize_first_last_pieces', |
| | 54 | 'is_auto_managed': 'auto_managed', |
| | 55 | 'stop_at_ratio': 'stop_at_ratio', |
| | 56 | 'stop_ratio': 'stop_ratio', |
| | 57 | 'remove_at_ratio': 'remove_at_ratio', |
| | 58 | 'move_on_completed': 'move_completed', |
| | 59 | 'move_on_completed_path': 'move_completed_path', |
| | 60 | #'file_priorities': 'file_priorities', |
| | 61 | #'compact': 'compact_allocation', |
| | 62 | #'save_path': 'download_location' |
| | 63 | } |
| | 64 | |
| | 65 | # These are the things that can be set, as far as I can tell. The |
| | 66 | # ones that aren't commented out are the ones that look like they |
| | 67 | # probably will behave as expected if set. |
| | 68 | # |
| | 69 | # Each value is [ type, getter name, help text ] |
| | 70 | set_torrent_options = { |
| | 71 | # These are handled by torrent.py's Torrent.set_options() |
| | 72 | 'auto_managed': [bool, None, '(Actually, I do not know what this does)'], |
| | 73 | #'download_location': str, # Probably not useful to set |
| | 74 | #'file_priorities': ???, # Not a simple value to set, probably needs its own command |
| | 75 | 'max_connections': [int, None, 'Maximum number of connections to use for this torrent'], |
| | 76 | 'max_download_speed': [float, None, 'Maximum total download speed to allow this torrent to use (KiB/s)'], |
| | 77 | 'max_upload_slots': [int, None, 'Maximum number of connections to use for uploading for this torrent'], |
| | 78 | 'max_upload_speed': [float, None, 'Maximum total upload speed to allow this torrent to use (KiB/s)'], |
| | 79 | 'prioritize_first_last_pieces': [bool, None, 'Whether to download the first and last piece of the torrent first. NOTE: Only has effect if the torrent contains a single file'], # Only has effect if torrent contains a single file |
| | 80 | |
| | 81 | # These are "handled" by being set directly on the options dict |
| | 82 | 'stop_at_ratio': [bool, None, 'Whether to stop seeding when share ratio reaches "stop_ratio".'], |
| | 83 | 'stop_ratio': [float, None, 'The ratio at which to stop seeding (if "stop_at_ratio" is True)'], |
| | 84 | 'remove_at_ratio': [bool, None, 'Whether to remove torrent when share ratio reaches "stop_ratio".'], |
| | 85 | 'move_completed': [bool, None, 'Whether to move the downloaded data when downloading is complete.'], |
| | 86 | 'move_completed_path': [str, None, 'Where to move completed data to (if "move_completed" is True)'], |
| | 87 | |
| | 88 | #'compact_allocation': bool, # Unclear what setting this would do |
| | 89 | #'add_paused': ???, # Not returned by get_status, unclear what setting it would do |
| | 90 | #'mapped_files': ??? # Not returned by get_status, unclear what setting it would do |
| | 91 | } |
| | 92 | for k,v in get_torrent_options.items(): |
| | 93 | set_torrent_options[v][1] = k |
| | 94 | |
| | 95 | |
| | 96 | class Command(BaseCommand): |
| | 97 | """Show and set per-torrent options""" |
| | 98 | |
| | 99 | option_help = [ ' ' * 7 + k + ': ' + v[2] for k,v in set_torrent_options.items() ] |
| | 100 | option_help.sort() |
| | 101 | |
| | 102 | option_list = BaseCommand.option_list + ( |
| | 103 | make_option('-s', '--set', action='store', nargs=2, dest='set', |
| | 104 | help='set value for key'), |
| | 105 | ) |
| | 106 | usage = '''Usage: manage <torrent-id> [<torrent-id> ...] [<key1> [<key2> ...]] |
| | 107 | manage <torrent-id> [<torrent-id> ...] --set <key> <value> |
| | 108 | |
| | 109 | torrent-id can be "*" to signify "all torrents" |
| | 110 | |
| | 111 | Possible options that this command can display or show: |
| | 112 | ''' + '\n'.join(option_help) |
| | 113 | |
| | 114 | |
| | 115 | def handle(self, *args, **options): |
| | 116 | self.console = component.get('ConsoleUI') |
| | 117 | if options['set']: |
| | 118 | return self._set_option(*args, **options) |
| | 119 | else: |
| | 120 | return self._get_option(*args, **options) |
| | 121 | |
| | 122 | |
| | 123 | def _get_option(self, *args, **options): |
| | 124 | |
| | 125 | def on_torrents_status(status): |
| | 126 | for torrentid, data in status.items(): |
| | 127 | self.console.write('\n') |
| | 128 | if 'name' in data: |
| | 129 | self.console.write('{!info!}Name: {!input!}%s' % data.get('name')) |
| | 130 | self.console.write('{!info!}ID: {!input!}%s' % torrentid) |
| | 131 | for k, v in data.items(): |
| | 132 | if k != 'name': |
| | 133 | displayname = get_torrent_options.get(k, '???' + k) |
| | 134 | self.console.write('{!info!}%s: {!input!}%s' % (displayname, v)) |
| | 135 | |
| | 136 | def on_torrents_status_fail(reason): |
| | 137 | self.console.write('{!error!}Failed to get torrent data.') |
| | 138 | |
| | 139 | torrent_ids = [] |
| | 140 | request_options = [] |
| | 141 | |
| | 142 | for arg in args: |
| | 143 | if arg in set_torrent_options: |
| | 144 | request_options.append(set_torrent_options[arg][1]) |
| | 145 | else: |
| | 146 | if arg == '*': |
| | 147 | ids = self.console.match_torrent('') |
| | 148 | else: |
| | 149 | ids = self.console.match_torrent(arg) |
| | 150 | if not ids: |
| | 151 | self.console.write("{!error!}The argument '" + arg + "' is not a recognized option nor did it match any torrents") |
| | 152 | return |
| | 153 | torrent_ids.extend(ids) |
| | 154 | |
| | 155 | if not torrent_ids: |
| | 156 | self.console.write('{!error!}No torrents mentioned. To request info on all torrents use "manage * [<key>...]".') |
| | 157 | return |
| | 158 | |
| | 159 | if not request_options: |
| | 160 | request_options = [ opt for opt in get_torrent_options ] |
| | 161 | request_options.append('name') |
| | 162 | |
| | 163 | d = client.core.get_torrents_status({'id': torrent_ids}, request_options) |
| | 164 | d.addCallback(on_torrents_status) |
| | 165 | d.addErrback(on_torrents_status_fail) |
| | 166 | return d |
| | 167 | |
| | 168 | |
| | 169 | def _set_prioritize_first_last_pieces(self, torrent_ids, val): |
| | 170 | |
| | 171 | def on_option_set(status): |
| | 172 | self.console.write('{!success!}Torrent option successfully updated.') |
| | 173 | |
| | 174 | def on_set_option_failed(reason): |
| | 175 | self.console.write("{!error!}Error setting torrent option: %s" % reason) |
| | 176 | |
| | 177 | def on_got_info(status): |
| | 178 | single_ids = [] |
| | 179 | multi_ids = [] |
| | 180 | error_ids = [] |
| | 181 | for tid in torrent_ids: |
| | 182 | if tid not in status: |
| | 183 | error_ids.append(tid) |
| | 184 | elif len(status[tid]['files']) > 1: |
| | 185 | multi_ids.append(tid) |
| | 186 | else: |
| | 187 | single_ids.append(tid) |
| | 188 | if multi_ids: |
| | 189 | torrent_names = [ self.console.get_torrent_name(tid) for tid in multi_ids ] |
| | 190 | self.console.write('{!error!}Not setting prioritize_first_last_pieces to ' + str(val) + |
| | 191 | ' (multiple files in torrent) for torrents: ' + ', '.join(torrent_names)) |
| | 192 | if error_ids: |
| | 193 | torrent_names = [ self.console.get_torrent_name(tid) for tid in error_ids ] |
| | 194 | self.console.write('{!error!}Will try to set prioritize_first_last_pieces to ' + str(val) + |
| | 195 | ', but may fail silently for torrents: ' + ', '.join(torrent_names)) |
| | 196 | |
| | 197 | use_ids = single_ids + error_ids |
| | 198 | if use_ids: |
| | 199 | torrent_names = [ self.console.get_torrent_name(tid) for tid in single_ids ] |
| | 200 | self.console.write('{!info!}Setting prioritize_first_last_pieces to %s for torrent(s): %s' % (val, ', '.join(torrent_names))) |
| | 201 | d = client.core.set_torrent_options(use_ids, {'prioritize_first_last_pieces': val}) |
| | 202 | d.addCallback(on_option_set) |
| | 203 | d.addErrback(on_set_option_failed) |
| | 204 | else: |
| | 205 | self.console.write('{!error!}No valid torrents to set prioritize_first_last_pieces on') |
| | 206 | |
| | 207 | def on_info_failed(reason): |
| | 208 | self.console.write("{!error!}Error getting torrent info (for setting prioritize_last_first_pieces): %s" % reason) |
| | 209 | |
| | 210 | d = client.core.get_torrents_status({"id": torrent_ids}, [ "files" ]) |
| | 211 | d.addCallback(on_got_info) |
| | 212 | d.addErrback(on_info_failed) |
| | 213 | |
| | 214 | def _set_option(self, *args, **options): |
| | 215 | torrent_ids = [] |
| | 216 | for arg in args: |
| | 217 | if arg == '*': |
| | 218 | ids = self.console.match_torrent('') |
| | 219 | else: |
| | 220 | ids = self.console.match_torrent(arg) |
| | 221 | if not ids: |
| | 222 | self.console.write("{!error!}The argument '" + arg + "' did not match any torrents") |
| | 223 | return |
| | 224 | torrent_ids.extend(ids) |
| | 225 | if not torrent_ids: |
| | 226 | self.console.write('{!error!}No torrents mentioned.') |
| | 227 | return |
| | 228 | key = options['set'][0] |
| | 229 | val = options['set'][1] |
| | 230 | |
| | 231 | if key not in set_torrent_options: |
| | 232 | self.console.write("{!error!}The key '%s' is invalid!" % key) |
| | 233 | return |
| | 234 | |
| | 235 | val = set_torrent_options[key][0](val) |
| | 236 | |
| | 237 | if key == 'prioritize_first_last_pieces': |
| | 238 | self._set_prioritize_first_last_pieces(torrent_ids, val) |
| | 239 | return |
| | 240 | |
| | 241 | def on_option_set(result): |
| | 242 | self.console.write('{!success!}Torrent option successfully updated.') |
| | 243 | |
| | 244 | def on_set_option_failed(reason): |
| | 245 | self.console.write("{!error!}Error setting torrent option: %s" % reason) |
| | 246 | |
| | 247 | torrent_names = [ self.console.get_torrent_name(tid) for tid in torrent_ids ] |
| | 248 | self.console.write('{!info!}Setting %s to %s for torrent(s): %s' % (key, val, ', '.join(torrent_names))) |
| | 249 | d = client.core.set_torrent_options(torrent_ids, {key: val}) |
| | 250 | d.addCallback(on_option_set) |
| | 251 | d.addErrback(on_set_option_failed) |
| | 252 | |
| | 253 | def complete(self, text): |
| | 254 | torrents = component.get('ConsoleUI').tab_complete_torrent(text) |
| | 255 | options = [ x for x in set_torrent_options if x.startswith(text) ] |
| | 256 | # This should probably only return options immediately after --set. |
| | 257 | return torrents + options |