1056 lines
39 KiB
Python
1056 lines
39 KiB
Python
# -*- coding: utf-8 -*-
|
|
#
|
|
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
|
|
#
|
|
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
|
|
# the additional special exception to link portions of this program with the OpenSSL library.
|
|
# See LICENSE for more details.
|
|
#
|
|
|
|
from __future__ import division, unicode_literals
|
|
|
|
import logging
|
|
import os
|
|
from base64 import b64decode, b64encode
|
|
from xml.sax.saxutils import escape as xml_escape
|
|
from xml.sax.saxutils import unescape as xml_unescape
|
|
|
|
from gi.repository import Gtk
|
|
from gi.repository.GObject import TYPE_INT64, TYPE_UINT64
|
|
|
|
import deluge.common
|
|
import deluge.component as component
|
|
from deluge.configmanager import ConfigManager
|
|
from deluge.httpdownloader import download_file
|
|
from deluge.ui.client import client
|
|
from deluge.ui.common import TorrentInfo
|
|
|
|
from .common import get_clipboard_text, listview_replace_treestore, reparent_iter
|
|
from .dialogs import ErrorDialog
|
|
from .edittrackersdialog import trackers_tiers_from_text
|
|
from .path_chooser import PathChooser
|
|
from .torrentview_data_funcs import cell_data_size
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class AddTorrentDialog(component.Component):
|
|
def __init__(self):
|
|
component.Component.__init__(self, 'AddTorrentDialog')
|
|
self.builder = Gtk.Builder()
|
|
# The base dialog
|
|
self.builder.add_from_file(
|
|
deluge.common.resource_filename(
|
|
__package__, os.path.join('glade', 'add_torrent_dialog.ui')
|
|
)
|
|
)
|
|
# The infohash dialog
|
|
self.builder.add_from_file(
|
|
deluge.common.resource_filename(
|
|
__package__, os.path.join('glade', 'add_torrent_dialog.infohash.ui')
|
|
)
|
|
)
|
|
# The url dialog
|
|
self.builder.add_from_file(
|
|
deluge.common.resource_filename(
|
|
__package__, os.path.join('glade', 'add_torrent_dialog.url.ui')
|
|
)
|
|
)
|
|
|
|
self.dialog = self.builder.get_object('dialog_add_torrent')
|
|
|
|
self.dialog.connect('delete-event', self._on_delete_event)
|
|
|
|
self.builder.connect_signals(self)
|
|
|
|
# download?, path, filesize, sequence number, inconsistent?
|
|
self.files_treestore = Gtk.TreeStore(
|
|
bool, str, TYPE_UINT64, TYPE_INT64, bool, str
|
|
)
|
|
self.files_treestore.set_sort_column_id(1, Gtk.SortType.ASCENDING)
|
|
|
|
# Holds the files info
|
|
self.files = {}
|
|
self.infos = {}
|
|
self.core_config = {}
|
|
self.options = {}
|
|
|
|
self.previous_selected_torrent = None
|
|
|
|
self.listview_torrents = self.builder.get_object('listview_torrents')
|
|
self.listview_files = self.builder.get_object('listview_files')
|
|
|
|
self.prefetching_magnets = []
|
|
|
|
render = Gtk.CellRendererText()
|
|
render.connect('edited', self._on_torrent_name_edit)
|
|
render.set_property('editable', True)
|
|
column = Gtk.TreeViewColumn(_('Torrent'), render, text=1)
|
|
self.listview_torrents.append_column(column)
|
|
|
|
render = Gtk.CellRendererToggle()
|
|
render.connect('toggled', self._on_file_toggled)
|
|
column = Gtk.TreeViewColumn(None, render, active=0, inconsistent=4)
|
|
self.listview_files.append_column(column)
|
|
|
|
column = Gtk.TreeViewColumn(_('Filename'))
|
|
render = Gtk.CellRendererPixbuf()
|
|
column.pack_start(render, False)
|
|
column.add_attribute(render, 'stock-id', 5)
|
|
render = Gtk.CellRendererText()
|
|
render.set_property('editable', True)
|
|
render.connect('edited', self._on_filename_edited)
|
|
column.pack_start(render, True)
|
|
column.add_attribute(render, 'text', 1)
|
|
column.set_expand(True)
|
|
self.listview_files.append_column(column)
|
|
|
|
render = Gtk.CellRendererText()
|
|
column = Gtk.TreeViewColumn(_('Size'))
|
|
column.pack_start(render, True)
|
|
column.set_cell_data_func(render, cell_data_size, 2)
|
|
self.listview_files.append_column(column)
|
|
|
|
self.torrent_liststore = Gtk.ListStore(str, str, str)
|
|
self.listview_torrents.set_model(self.torrent_liststore)
|
|
self.listview_torrents.set_tooltip_column(2)
|
|
self.listview_files.set_model(self.files_treestore)
|
|
|
|
self.listview_files.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
|
|
self.listview_torrents.get_selection().connect(
|
|
'changed', self._on_torrent_changed
|
|
)
|
|
self.torrent_liststore.connect('row-inserted', self.update_dialog_title_count)
|
|
self.torrent_liststore.connect('row-deleted', self.update_dialog_title_count)
|
|
|
|
self.setup_move_completed_path_chooser()
|
|
self.setup_download_location_path_chooser()
|
|
|
|
# Get default config values from the core
|
|
self.core_keys = [
|
|
'pre_allocate_storage',
|
|
'max_connections_per_torrent',
|
|
'max_upload_slots_per_torrent',
|
|
'max_upload_speed_per_torrent',
|
|
'max_download_speed_per_torrent',
|
|
'prioritize_first_last_pieces',
|
|
'sequential_download',
|
|
'add_paused',
|
|
'download_location',
|
|
'download_location_paths_list',
|
|
'move_completed',
|
|
'move_completed_path',
|
|
'move_completed_paths_list',
|
|
'super_seeding',
|
|
]
|
|
# self.core_keys += self.move_completed_path_chooser.get_config_keys()
|
|
self.builder.get_object('notebook1').connect(
|
|
'switch-page', self._on_switch_page
|
|
)
|
|
|
|
def start(self):
|
|
self.update_core_config()
|
|
|
|
def show(self, focus=False):
|
|
self.update_core_config(True, focus)
|
|
|
|
def _show(self, focus=False):
|
|
if component.get('MainWindow').is_on_active_workspace():
|
|
self.dialog.set_transient_for(component.get('MainWindow').window)
|
|
else:
|
|
self.dialog.set_transient_for(None)
|
|
|
|
self.dialog.present()
|
|
if focus:
|
|
self.dialog.window.focus()
|
|
|
|
def hide(self):
|
|
self.dialog.hide()
|
|
self.files = {}
|
|
self.infos = {}
|
|
self.options = {}
|
|
self.previous_selected_torrent = None
|
|
self.torrent_liststore.clear()
|
|
self.files_treestore.clear()
|
|
self.prefetching_magnets = []
|
|
self.dialog.set_transient_for(component.get('MainWindow').window)
|
|
|
|
def _on_config_values(self, config, show=False, focus=False):
|
|
self.core_config = config
|
|
if self.core_config:
|
|
self.set_default_options()
|
|
if show:
|
|
self._show(focus)
|
|
|
|
def update_core_config(self, show=False, focus=False):
|
|
# Send requests to the core for these config values
|
|
d = client.core.get_config_values(self.core_keys)
|
|
d.addCallback(self._on_config_values, show, focus)
|
|
|
|
def _add_torrent_liststore(self, info_hash, name, filename, files, filedata):
|
|
"""Add a torrent to torrent_liststore."""
|
|
if info_hash in self.files:
|
|
return False
|
|
|
|
torrent_row = [info_hash, name, xml_escape(filename)]
|
|
row_iter = self.torrent_liststore.append(torrent_row)
|
|
self.files[info_hash] = files
|
|
self.infos[info_hash] = filedata
|
|
self.listview_torrents.get_selection().select_iter(row_iter)
|
|
|
|
self.set_default_options()
|
|
self.save_torrent_options(row_iter)
|
|
|
|
return row_iter
|
|
|
|
def update_dialog_title_count(self, *args):
|
|
"""Update the AddTorrent dialog title with current torrent count."""
|
|
self.dialog.set_title(_('Add Torrents (%d)') % len(self.torrent_liststore))
|
|
|
|
def show_already_added_dialog(self, count):
|
|
"""Show a message about trying to add duplicate torrents."""
|
|
log.debug('Tried to add %d duplicate torrents!', count)
|
|
ErrorDialog(
|
|
_('Duplicate torrent(s)'),
|
|
_(
|
|
'You cannot add the same torrent twice.'
|
|
' %d torrents were already added.' % count
|
|
),
|
|
self.dialog,
|
|
).run()
|
|
|
|
def add_from_files(self, filenames):
|
|
already_added = 0
|
|
|
|
for filename in filenames:
|
|
# Get the torrent data from the torrent file
|
|
try:
|
|
info = TorrentInfo(filename)
|
|
except Exception as ex:
|
|
log.debug('Unable to open torrent file: %s', ex)
|
|
ErrorDialog(_('Invalid File'), ex, self.dialog).run()
|
|
continue
|
|
|
|
if not self._add_torrent_liststore(
|
|
info.info_hash, info.name, filename, info.files, info.filedata
|
|
):
|
|
already_added += 1
|
|
|
|
if already_added:
|
|
self.show_already_added_dialog(already_added)
|
|
|
|
def _on_uri_metadata(self, result, uri):
|
|
"""Process prefetched metadata to allow file priority selection."""
|
|
info_hash, b64_metadata = result
|
|
log.debug('on_uri_metadata for %s (%s)', uri, info_hash)
|
|
if info_hash not in self.prefetching_magnets:
|
|
return
|
|
|
|
if b64_metadata:
|
|
metadata = b64decode(b64_metadata)
|
|
info = TorrentInfo(metadata=metadata)
|
|
self.files[info_hash] = info.files
|
|
self.infos[info_hash] = info.filedata
|
|
else:
|
|
log.info('Unable to fetch metadata for magnet: %s', uri)
|
|
self.prefetching_magnets.remove(info_hash)
|
|
self._on_torrent_changed(self.listview_torrents.get_selection())
|
|
|
|
def prefetch_waiting_message(self, torrent_id, files):
|
|
"""Show magnet files fetching or failed message above files list."""
|
|
if torrent_id in self.prefetching_magnets:
|
|
self.builder.get_object('prefetch_label').set_text(
|
|
_('Please wait for files...')
|
|
)
|
|
self.builder.get_object('prefetch_spinner').show()
|
|
self.builder.get_object('prefetch_hbox').show()
|
|
elif not files:
|
|
self.builder.get_object('prefetch_label').set_text(
|
|
_('Unable to download files for this magnet')
|
|
)
|
|
self.builder.get_object('prefetch_spinner').hide()
|
|
self.builder.get_object('prefetch_hbox').show()
|
|
else:
|
|
self.builder.get_object('prefetch_hbox').hide()
|
|
|
|
def add_from_magnets(self, uris):
|
|
"""Add a list of magnet uris to torrent_liststore."""
|
|
already_added = 0
|
|
|
|
for uri in uris:
|
|
magnet = deluge.common.get_magnet_info(uri)
|
|
if not magnet:
|
|
log.error('Invalid magnet: %s', uri)
|
|
continue
|
|
|
|
torrent_id = magnet['info_hash']
|
|
files = magnet['files_tree']
|
|
if not self._add_torrent_liststore(
|
|
torrent_id, magnet['name'], xml_escape(uri), files, None
|
|
):
|
|
already_added += 1
|
|
continue
|
|
|
|
if files:
|
|
continue
|
|
|
|
d = client.core.prefetch_magnet_metadata(uri)
|
|
d.addCallback(self._on_uri_metadata, uri)
|
|
self.prefetching_magnets.append(magnet['info_hash'])
|
|
self.prefetch_waiting_message(torrent_id, None)
|
|
|
|
if already_added:
|
|
self.show_already_added_dialog(already_added)
|
|
|
|
def _on_torrent_changed(self, treeselection):
|
|
(model, row) = treeselection.get_selected()
|
|
if row is None or not model.iter_is_valid(row):
|
|
self.files_treestore.clear()
|
|
self.previous_selected_torrent = None
|
|
return
|
|
|
|
if model[row][0] not in self.files:
|
|
self.files_treestore.clear()
|
|
self.previous_selected_torrent = None
|
|
return
|
|
|
|
# Save the previous torrents options
|
|
self.save_torrent_options()
|
|
|
|
torrent_id = model.get_value(row, 0)
|
|
# Update files list
|
|
files_list = self.files[torrent_id]
|
|
self.prepare_file_store(files_list)
|
|
|
|
if self.core_config == {}:
|
|
self.update_core_config()
|
|
|
|
# Update the options frame
|
|
self.update_torrent_options(torrent_id)
|
|
# Update magnet prefetch message
|
|
self.prefetch_waiting_message(torrent_id, files_list)
|
|
|
|
self.previous_selected_torrent = row
|
|
|
|
def _on_torrent_name_edit(self, w, row, new_name):
|
|
# TODO: Update torrent name
|
|
pass
|
|
|
|
def _on_switch_page(self, widget, page, page_num):
|
|
# Save the torrent options when switching notebook pages
|
|
self.save_torrent_options()
|
|
|
|
def prepare_file_store(self, files):
|
|
with listview_replace_treestore(self.listview_files):
|
|
split_files = {}
|
|
for idx, _file in enumerate(files):
|
|
self.prepare_file(
|
|
_file, _file['path'], idx, _file.get('download', True), split_files
|
|
)
|
|
self.add_files(None, split_files)
|
|
root = Gtk.TreePath.new_first()
|
|
self.listview_files.expand_row(root, False)
|
|
|
|
def prepare_file(self, _file, file_name, file_num, download, files_storage):
|
|
first_slash_index = file_name.find(os.path.sep)
|
|
if first_slash_index == -1:
|
|
files_storage[file_name] = (file_num, _file, download)
|
|
else:
|
|
file_name_chunk = file_name[: first_slash_index + 1]
|
|
if file_name_chunk not in files_storage:
|
|
files_storage[file_name_chunk] = {}
|
|
self.prepare_file(
|
|
_file,
|
|
file_name[first_slash_index + 1 :],
|
|
file_num,
|
|
download,
|
|
files_storage[file_name_chunk],
|
|
)
|
|
|
|
def add_files(self, parent_iter, split_files):
|
|
ret = 0
|
|
for key, value in split_files.items():
|
|
if key.endswith(os.path.sep):
|
|
chunk_iter = self.files_treestore.append(
|
|
parent_iter, [True, key, 0, -1, False, Gtk.STOCK_DIRECTORY]
|
|
)
|
|
chunk_size = self.add_files(chunk_iter, value)
|
|
self.files_treestore.set(chunk_iter, 2, chunk_size)
|
|
ret += chunk_size
|
|
else:
|
|
self.files_treestore.append(
|
|
parent_iter,
|
|
[value[2], key, value[1]['size'], value[0], False, Gtk.STOCK_FILE],
|
|
)
|
|
ret += value[1]['size']
|
|
if parent_iter and self.files_treestore.iter_has_child(parent_iter):
|
|
# Iterate through the children and see what we should label the
|
|
# folder, download true, download false or inconsistent.
|
|
itr = self.files_treestore.iter_children(parent_iter)
|
|
download = []
|
|
download_value = False
|
|
inconsistent = False
|
|
while itr:
|
|
download.append(self.files_treestore.get_value(itr, 0))
|
|
itr = self.files_treestore.iter_next(itr)
|
|
|
|
if sum(download) == len(download):
|
|
download_value = True
|
|
elif sum(download) == 0:
|
|
download_value = False
|
|
else:
|
|
inconsistent = True
|
|
|
|
self.files_treestore.set_value(parent_iter, 0, download_value)
|
|
self.files_treestore.set_value(parent_iter, 4, inconsistent)
|
|
return ret
|
|
|
|
def load_path_choosers_data(self):
|
|
self.move_completed_path_chooser.set_text(
|
|
self.core_config['move_completed_path'], cursor_end=False, default_text=True
|
|
)
|
|
self.download_location_path_chooser.set_text(
|
|
self.core_config['download_location'], cursor_end=False, default_text=True
|
|
)
|
|
self.builder.get_object('chk_move_completed').set_active(
|
|
self.core_config['move_completed']
|
|
)
|
|
|
|
def setup_move_completed_path_chooser(self):
|
|
self.move_completed_hbox = self.builder.get_object(
|
|
'hbox_move_completed_chooser'
|
|
)
|
|
self.move_completed_path_chooser = PathChooser('move_completed_paths_list')
|
|
self.move_completed_hbox.add(self.move_completed_path_chooser)
|
|
self.move_completed_hbox.show_all()
|
|
|
|
def setup_download_location_path_chooser(self):
|
|
self.download_location_hbox = self.builder.get_object(
|
|
'hbox_download_location_chooser'
|
|
)
|
|
self.download_location_path_chooser = PathChooser(
|
|
'download_location_paths_list'
|
|
)
|
|
self.download_location_hbox.add(self.download_location_path_chooser)
|
|
self.download_location_hbox.show_all()
|
|
|
|
def update_torrent_options(self, torrent_id):
|
|
if torrent_id not in self.options:
|
|
self.set_default_options()
|
|
return
|
|
|
|
options = self.options[torrent_id]
|
|
|
|
self.download_location_path_chooser.set_text(
|
|
options['download_location'], cursor_end=True
|
|
)
|
|
self.move_completed_path_chooser.set_text(
|
|
options['move_completed_path'], cursor_end=True
|
|
)
|
|
|
|
self.builder.get_object('spin_maxdown').set_value(options['max_download_speed'])
|
|
self.builder.get_object('spin_maxup').set_value(options['max_upload_speed'])
|
|
self.builder.get_object('spin_maxconnections').set_value(
|
|
options['max_connections']
|
|
)
|
|
self.builder.get_object('spin_maxupslots').set_value(
|
|
options['max_upload_slots']
|
|
)
|
|
self.builder.get_object('chk_paused').set_active(options['add_paused'])
|
|
self.builder.get_object('chk_pre_alloc').set_active(
|
|
options['pre_allocate_storage']
|
|
)
|
|
self.builder.get_object('chk_prioritize').set_active(
|
|
options['prioritize_first_last_pieces']
|
|
)
|
|
self.builder.get_object('chk_sequential_download').set_active(
|
|
options['sequential_download']
|
|
)
|
|
self.builder.get_object('chk_move_completed').set_active(
|
|
options['move_completed']
|
|
)
|
|
self.builder.get_object('chk_super_seeding').set_active(
|
|
options['super_seeding']
|
|
)
|
|
|
|
def save_torrent_options(self, row=None):
|
|
# Keeps the torrent options dictionary up-to-date with what the user has
|
|
# selected.
|
|
if row is None:
|
|
if self.previous_selected_torrent and self.torrent_liststore.iter_is_valid(
|
|
self.previous_selected_torrent
|
|
):
|
|
row = self.previous_selected_torrent
|
|
else:
|
|
return
|
|
|
|
torrent_id = self.torrent_liststore.get_value(row, 0)
|
|
|
|
if torrent_id in self.options:
|
|
options = self.options[torrent_id]
|
|
else:
|
|
options = {}
|
|
|
|
options['download_location'] = self.download_location_path_chooser.get_text()
|
|
options['move_completed_path'] = self.move_completed_path_chooser.get_text()
|
|
options['pre_allocate_storage'] = self.builder.get_object(
|
|
'chk_pre_alloc'
|
|
).get_active()
|
|
options['move_completed'] = self.builder.get_object(
|
|
'chk_move_completed'
|
|
).get_active()
|
|
options['max_download_speed'] = self.builder.get_object(
|
|
'spin_maxdown'
|
|
).get_value()
|
|
options['max_upload_speed'] = self.builder.get_object('spin_maxup').get_value()
|
|
options['max_connections'] = self.builder.get_object(
|
|
'spin_maxconnections'
|
|
).get_value_as_int()
|
|
options['max_upload_slots'] = self.builder.get_object(
|
|
'spin_maxupslots'
|
|
).get_value_as_int()
|
|
options['add_paused'] = self.builder.get_object('chk_paused').get_active()
|
|
options['prioritize_first_last_pieces'] = self.builder.get_object(
|
|
'chk_prioritize'
|
|
).get_active()
|
|
options['sequential_download'] = (
|
|
self.builder.get_object('chk_sequential_download').get_active() or False
|
|
)
|
|
options['move_completed'] = self.builder.get_object(
|
|
'chk_move_completed'
|
|
).get_active()
|
|
options['seed_mode'] = self.builder.get_object('chk_seed_mode').get_active()
|
|
options['super_seeding'] = self.builder.get_object(
|
|
'chk_super_seeding'
|
|
).get_active()
|
|
|
|
self.options[torrent_id] = options
|
|
|
|
# Save the file priorities
|
|
files_priorities = self.build_priorities(
|
|
self.files_treestore.get_iter_first(), {}
|
|
)
|
|
|
|
if len(files_priorities) > 0:
|
|
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):
|
|
self.build_priorities(
|
|
self.files_treestore.iter_children(_iter), priorities
|
|
)
|
|
elif not self.files_treestore.get_value(_iter, 1).endswith(os.path.sep):
|
|
priorities[
|
|
self.files_treestore.get_value(_iter, 3)
|
|
] = self.files_treestore.get_value(_iter, 0)
|
|
_iter = self.files_treestore.iter_next(_iter)
|
|
return priorities
|
|
|
|
def set_default_options(self):
|
|
if not self.core_config:
|
|
# update_core_config will call this method again.
|
|
self.update_core_config()
|
|
return
|
|
|
|
self.load_path_choosers_data()
|
|
|
|
self.builder.get_object('chk_pre_alloc').set_active(
|
|
self.core_config['pre_allocate_storage']
|
|
)
|
|
self.builder.get_object('spin_maxdown').set_value(
|
|
self.core_config['max_download_speed_per_torrent']
|
|
)
|
|
self.builder.get_object('spin_maxup').set_value(
|
|
self.core_config['max_upload_speed_per_torrent']
|
|
)
|
|
self.builder.get_object('spin_maxconnections').set_value(
|
|
self.core_config['max_connections_per_torrent']
|
|
)
|
|
self.builder.get_object('spin_maxupslots').set_value(
|
|
self.core_config['max_upload_slots_per_torrent']
|
|
)
|
|
self.builder.get_object('chk_paused').set_active(self.core_config['add_paused'])
|
|
self.builder.get_object('chk_prioritize').set_active(
|
|
self.core_config['prioritize_first_last_pieces']
|
|
)
|
|
self.builder.get_object('chk_sequential_download').set_active(
|
|
self.core_config['sequential_download']
|
|
)
|
|
self.builder.get_object('chk_move_completed').set_active(
|
|
self.core_config['move_completed']
|
|
)
|
|
self.builder.get_object('chk_seed_mode').set_active(False)
|
|
self.builder.get_object('chk_super_seeding').set_active(
|
|
self.core_config['super_seeding']
|
|
)
|
|
|
|
def get_file_priorities(self, torrent_id):
|
|
# A list of priorities
|
|
files_list = []
|
|
|
|
for file_dict in self.files[torrent_id]:
|
|
if not file_dict['download']:
|
|
files_list.append(0)
|
|
else:
|
|
# Default lt file priority is 4
|
|
files_list.append(4)
|
|
|
|
return files_list
|
|
|
|
def _on_file_toggled(self, render, path):
|
|
(model, paths) = self.listview_files.get_selection().get_selected_rows()
|
|
if len(paths) > 1:
|
|
for path in paths:
|
|
row = model.get_iter(path)
|
|
self.toggle_iter(row)
|
|
else:
|
|
row = model.get_iter(path)
|
|
self.toggle_iter(row)
|
|
self.update_treeview_toggles(self.files_treestore.get_iter_first())
|
|
|
|
def toggle_iter(self, _iter, toggle_to=None):
|
|
if toggle_to is None:
|
|
toggle_to = not self.files_treestore.get_value(_iter, 0)
|
|
self.files_treestore.set_value(_iter, 0, toggle_to)
|
|
if self.files_treestore.iter_has_child(_iter):
|
|
child = self.files_treestore.iter_children(_iter)
|
|
while child is not None:
|
|
self.toggle_iter(child, toggle_to)
|
|
child = self.files_treestore.iter_next(child)
|
|
|
|
def update_treeview_toggles(self, _iter):
|
|
toggle_inconsistent = -1
|
|
this_level_toggle = None
|
|
while _iter is not None:
|
|
if self.files_treestore.iter_has_child(_iter):
|
|
toggle = self.update_treeview_toggles(
|
|
self.files_treestore.iter_children(_iter)
|
|
)
|
|
if toggle == toggle_inconsistent:
|
|
self.files_treestore.set_value(_iter, 4, True)
|
|
else:
|
|
self.files_treestore.set_value(_iter, 0, toggle)
|
|
# set inconsistent to false
|
|
self.files_treestore.set_value(_iter, 4, False)
|
|
else:
|
|
toggle = self.files_treestore.get_value(_iter, 0)
|
|
if this_level_toggle is None:
|
|
this_level_toggle = toggle
|
|
elif this_level_toggle != toggle:
|
|
this_level_toggle = toggle_inconsistent
|
|
_iter = self.files_treestore.iter_next(_iter)
|
|
return this_level_toggle
|
|
|
|
def on_button_file_clicked(self, widget):
|
|
log.debug('on_button_file_clicked')
|
|
# Setup the filechooserdialog
|
|
chooser = Gtk.FileChooserDialog(
|
|
_('Choose a .torrent file'),
|
|
None,
|
|
Gtk.FileChooserAction.OPEN,
|
|
buttons=(
|
|
Gtk.STOCK_CANCEL,
|
|
Gtk.ResponseType.CANCEL,
|
|
Gtk.STOCK_OPEN,
|
|
Gtk.ResponseType.OK,
|
|
),
|
|
)
|
|
|
|
chooser.set_transient_for(self.dialog)
|
|
chooser.set_select_multiple(True)
|
|
chooser.set_property('skip-taskbar-hint', True)
|
|
chooser.set_local_only(False)
|
|
|
|
# Add .torrent and * file filters
|
|
file_filter = Gtk.FileFilter()
|
|
file_filter.set_name(_('Torrent files'))
|
|
file_filter.add_pattern('*.' + 'torrent')
|
|
chooser.add_filter(file_filter)
|
|
file_filter = Gtk.FileFilter()
|
|
file_filter.set_name(_('All files'))
|
|
file_filter.add_pattern('*')
|
|
chooser.add_filter(file_filter)
|
|
|
|
# Load the 'default_load_path' from the config
|
|
self.config = ConfigManager('gtk3ui.conf')
|
|
if (
|
|
'default_load_path' in self.config
|
|
and self.config['default_load_path'] is not None
|
|
):
|
|
chooser.set_current_folder(self.config['default_load_path'])
|
|
|
|
# Run the dialog
|
|
response = chooser.run()
|
|
|
|
if response == Gtk.ResponseType.OK:
|
|
result = chooser.get_filenames()
|
|
self.config['default_load_path'] = chooser.get_current_folder()
|
|
else:
|
|
chooser.destroy()
|
|
return
|
|
|
|
chooser.destroy()
|
|
self.add_from_files(result)
|
|
|
|
def on_button_url_clicked(self, widget):
|
|
log.debug('on_button_url_clicked')
|
|
dialog = self.builder.get_object('url_dialog')
|
|
entry = self.builder.get_object('entry_url')
|
|
|
|
dialog.set_default_response(Gtk.ResponseType.OK)
|
|
dialog.set_transient_for(self.dialog)
|
|
entry.grab_focus()
|
|
|
|
text = get_clipboard_text()
|
|
if text and deluge.common.is_url(text) or deluge.common.is_magnet(text):
|
|
entry.set_text(text)
|
|
|
|
dialog.show_all()
|
|
response = dialog.run()
|
|
|
|
if response == Gtk.ResponseType.OK:
|
|
url = deluge.common.decode_bytes(entry.get_text())
|
|
else:
|
|
url = None
|
|
|
|
entry.set_text('')
|
|
dialog.hide()
|
|
|
|
# This is where we need to fetch the .torrent file from the URL and
|
|
# add it to the list.
|
|
log.debug('url: %s', url)
|
|
if url:
|
|
if deluge.common.is_url(url):
|
|
self.add_from_url(url)
|
|
elif deluge.common.is_magnet(url):
|
|
self.add_from_magnets([url])
|
|
else:
|
|
ErrorDialog(
|
|
_('Invalid URL'),
|
|
'%s %s' % (url, _('is not a valid URL.')),
|
|
self.dialog,
|
|
).run()
|
|
|
|
def add_from_url(self, url):
|
|
dialog = Gtk.Dialog(
|
|
_('Downloading...'),
|
|
flags=Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
|
|
parent=self.dialog,
|
|
)
|
|
dialog.set_transient_for(self.dialog)
|
|
|
|
pb = Gtk.ProgressBar()
|
|
dialog.vbox.pack_start(pb, True, True, 0)
|
|
dialog.show_all()
|
|
|
|
# Create a tmp file path
|
|
import tempfile
|
|
|
|
tmp_fd, tmp_file = tempfile.mkstemp(prefix='deluge_url.', suffix='.torrent')
|
|
|
|
def on_part(data, current_length, total_length):
|
|
if total_length:
|
|
percent = current_length / total_length
|
|
pb.set_fraction(percent)
|
|
pb.set_text(
|
|
'%.2f%% (%s / %s)'
|
|
% (
|
|
percent * 100,
|
|
deluge.common.fsize(current_length),
|
|
deluge.common.fsize(total_length),
|
|
)
|
|
)
|
|
else:
|
|
pb.pulse()
|
|
pb.set_text('%s' % deluge.common.fsize(current_length))
|
|
|
|
def on_download_success(result):
|
|
self.add_from_files([result])
|
|
dialog.destroy()
|
|
|
|
def on_download_fail(result):
|
|
log.debug('Download failed: %s', result)
|
|
dialog.destroy()
|
|
ErrorDialog(
|
|
_('Download Failed'),
|
|
'%s %s' % (_('Failed to download:'), url),
|
|
details=result.getErrorMessage(),
|
|
parent=self.dialog,
|
|
).run()
|
|
return result
|
|
|
|
d = download_file(url, tmp_file, on_part)
|
|
os.close(tmp_fd)
|
|
d.addCallbacks(on_download_success, on_download_fail)
|
|
|
|
def on_button_hash_clicked(self, widget):
|
|
log.debug('on_button_hash_clicked')
|
|
dialog = self.builder.get_object('dialog_infohash')
|
|
entry = self.builder.get_object('entry_hash')
|
|
textview = self.builder.get_object('text_trackers')
|
|
|
|
dialog.set_default_response(Gtk.ResponseType.OK)
|
|
dialog.set_transient_for(self.dialog)
|
|
entry.grab_focus()
|
|
|
|
text = get_clipboard_text()
|
|
if deluge.common.is_infohash(text):
|
|
entry.set_text(text)
|
|
|
|
dialog.show_all()
|
|
response = dialog.run()
|
|
infohash = entry.get_text().strip()
|
|
if response == Gtk.ResponseType.OK and deluge.common.is_infohash(infohash):
|
|
# Create a list of trackers from the textview buffer
|
|
tview_buf = textview.get_buffer()
|
|
trackers_text = tview_buf.get_text(
|
|
*tview_buf.get_bounds(), include_hidden_chars=False
|
|
)
|
|
log.debug('Create torrent tracker lines: %s', trackers_text)
|
|
trackers = list(trackers_tiers_from_text(trackers_text).keys())
|
|
|
|
# Convert the information to a magnet uri, this is just easier to
|
|
# handle this way.
|
|
log.debug('trackers: %s', trackers)
|
|
magnet = deluge.common.create_magnet_uri(infohash, infohash, trackers)
|
|
log.debug('magnet uri: %s', magnet)
|
|
self.add_from_magnets([magnet])
|
|
|
|
entry.set_text('')
|
|
textview.get_buffer().set_text('')
|
|
dialog.hide()
|
|
|
|
def on_button_remove_clicked(self, widget):
|
|
log.debug('on_button_remove_clicked')
|
|
(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]
|
|
|
|
def on_button_trackers_clicked(self, widget):
|
|
log.debug('on_button_trackers_clicked')
|
|
|
|
def on_button_cancel_clicked(self, widget):
|
|
log.debug('on_button_cancel_clicked')
|
|
self.hide()
|
|
|
|
def on_button_add_clicked(self, widget):
|
|
log.debug('on_button_add_clicked')
|
|
self.add_torrents()
|
|
self.hide()
|
|
|
|
def add_torrents(self):
|
|
(model, row) = self.listview_torrents.get_selection().get_selected()
|
|
if row is not None:
|
|
self.save_torrent_options(row)
|
|
|
|
torrents_to_add = []
|
|
|
|
row = self.torrent_liststore.get_iter_first()
|
|
while row is not None:
|
|
torrent_id = self.torrent_liststore.get_value(row, 0)
|
|
filename = xml_unescape(self.torrent_liststore.get_value(row, 2))
|
|
try:
|
|
options = self.options[torrent_id]
|
|
except KeyError:
|
|
options = None
|
|
|
|
file_priorities = self.get_file_priorities(torrent_id)
|
|
if options is not None:
|
|
options['file_priorities'] = file_priorities
|
|
|
|
if self.infos[torrent_id]:
|
|
torrents_to_add.append(
|
|
(
|
|
os.path.split(filename)[-1],
|
|
b64encode(self.infos[torrent_id]),
|
|
options,
|
|
)
|
|
)
|
|
elif deluge.common.is_magnet(filename):
|
|
client.core.add_torrent_magnet(filename, options)
|
|
|
|
row = self.torrent_liststore.iter_next(row)
|
|
|
|
def on_torrents_added(errors):
|
|
if errors:
|
|
log.info(
|
|
'Failed to add %d out of %d torrents.',
|
|
len(errors),
|
|
len(torrents_to_add),
|
|
)
|
|
for e in errors:
|
|
log.info('Torrent add failed: %s', e)
|
|
else:
|
|
log.info('Successfully added %d torrents.', len(torrents_to_add))
|
|
|
|
client.core.add_torrent_files(torrents_to_add).addCallback(on_torrents_added)
|
|
|
|
def on_button_apply_clicked(self, widget):
|
|
log.debug('on_button_apply_clicked')
|
|
(model, row) = self.listview_torrents.get_selection().get_selected()
|
|
if row is None:
|
|
return
|
|
|
|
self.save_torrent_options(row)
|
|
|
|
# The options, except file renames, we want all the torrents to have
|
|
options = self.options[model.get_value(row, 0)].copy()
|
|
options.pop('mapped_files', None)
|
|
|
|
# Set all the torrent options
|
|
row = model.get_iter_first()
|
|
while row is not None:
|
|
torrent_id = model.get_value(row, 0)
|
|
self.options[torrent_id].update(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()
|
|
|
|
def on_chk_move_completed_toggled(self, widget):
|
|
value = widget.get_active()
|
|
self.move_completed_path_chooser.set_sensitive(value)
|
|
|
|
def _on_delete_event(self, widget, event):
|
|
self.hide()
|
|
return True
|
|
|
|
def get_file_path(self, row, path=''):
|
|
if not row:
|
|
return path
|
|
|
|
path = self.files_treestore[row][1] + path
|
|
return self.get_file_path(self.files_treestore.iter_parent(row), path)
|
|
|
|
def _on_filename_edited(self, renderer, path, new_text):
|
|
index = self.files_treestore[path][3]
|
|
|
|
new_text = new_text.strip(os.path.sep).strip()
|
|
|
|
# Return if the text hasn't changed
|
|
if new_text == self.files_treestore[path][1]:
|
|
return
|
|
|
|
# Get the tree iter
|
|
itr = self.files_treestore.get_iter(path)
|
|
|
|
# Get the torrent_id
|
|
(model, row) = self.listview_torrents.get_selection().get_selected()
|
|
torrent_id = model[row][0]
|
|
|
|
if 'mapped_files' not in self.options[torrent_id]:
|
|
self.options[torrent_id]['mapped_files'] = {}
|
|
|
|
if index > -1:
|
|
# We're renaming a file! Yay! That's easy!
|
|
if not new_text:
|
|
return
|
|
parent = self.files_treestore.iter_parent(itr)
|
|
file_path = os.path.join(self.get_file_path(parent), new_text)
|
|
# Don't rename if filename exists
|
|
if parent:
|
|
for row in self.files_treestore[parent].iterchildren():
|
|
if new_text == row[1]:
|
|
return
|
|
if os.path.sep in new_text:
|
|
# There are folders in this path, so we need to create them
|
|
# and then move the file iter to top
|
|
split_text = new_text.split(os.path.sep)
|
|
for s in split_text[:-1]:
|
|
parent = self.files_treestore.append(
|
|
parent, [True, s, 0, -1, False, Gtk.STOCK_DIRECTORY]
|
|
)
|
|
|
|
self.files_treestore[itr][1] = split_text[-1]
|
|
reparent_iter(self.files_treestore, itr, parent)
|
|
else:
|
|
# Update the row's text
|
|
self.files_treestore[itr][1] = new_text
|
|
|
|
# Update the mapped_files dict in the options with the index and new
|
|
# file path.
|
|
# We'll send this to the core when adding the torrent so it knows
|
|
# what to rename before adding.
|
|
self.options[torrent_id]['mapped_files'][index] = file_path
|
|
self.files[torrent_id][index]['path'] = file_path
|
|
else:
|
|
# Folder!
|
|
def walk_tree(row):
|
|
if not row:
|
|
return
|
|
|
|
# Get the file path base once, since it will be the same for
|
|
# all siblings
|
|
file_path_base = self.get_file_path(
|
|
self.files_treestore.iter_parent(row)
|
|
)
|
|
|
|
# Iterate through all the siblings at this level
|
|
while row:
|
|
# We recurse if there are children
|
|
if self.files_treestore.iter_has_child(row):
|
|
walk_tree(self.files_treestore.iter_children(row))
|
|
|
|
index = self.files_treestore[row][3]
|
|
|
|
if index > -1:
|
|
# Get the new full path for this file
|
|
file_path = file_path_base + self.files_treestore[row][1]
|
|
|
|
# Update the file path in the mapped_files dict
|
|
self.options[torrent_id]['mapped_files'][index] = file_path
|
|
self.files[torrent_id][index]['path'] = file_path
|
|
|
|
# Get the next siblings iter
|
|
row = self.files_treestore.iter_next(row)
|
|
|
|
# Update the treestore row first so that when walking the tree
|
|
# we can construct the new proper paths
|
|
|
|
# We need to check if this folder has been split
|
|
if os.path.sep in new_text:
|
|
# It's been split, so we need to add new folders and then re-parent
|
|
# itr.
|
|
parent = self.files_treestore.iter_parent(itr)
|
|
split_text = new_text.split(os.path.sep)
|
|
for s in split_text[:-1]:
|
|
# We don't iterate over the last item because we'll just use
|
|
# the existing itr and change the text
|
|
parent = self.files_treestore.append(
|
|
parent,
|
|
[True, s + os.path.sep, 0, -1, False, Gtk.STOCK_DIRECTORY],
|
|
)
|
|
|
|
self.files_treestore[itr][1] = split_text[-1] + os.path.sep
|
|
|
|
# Now re-parent itr to parent
|
|
reparent_iter(self.files_treestore, itr, parent)
|
|
itr = parent
|
|
|
|
# We need to re-expand the view because it might contracted
|
|
# if we change the root iter
|
|
root = Gtk.TreePath.new_first()
|
|
self.listview_files.expand_row(root, False)
|
|
else:
|
|
# This was a simple folder rename without any splits, so just
|
|
# change the path for itr
|
|
self.files_treestore[itr][1] = new_text + os.path.sep
|
|
|
|
# Walk through the tree from 'itr' and add all the new file paths
|
|
# to the 'mapped_files' option
|
|
walk_tree(itr)
|