521 lines
17 KiB
Python
521 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
|
#
|
|
# Copyright (C) 2008 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.path
|
|
from base64 import b64encode
|
|
|
|
from gi.repository import Gtk
|
|
from gi.repository.GObject import TYPE_UINT64, idle_add
|
|
from twisted.internet.threads import deferToThread
|
|
|
|
import deluge.component as component
|
|
from deluge.common import decode_bytes, get_path_size, is_url, resource_filename
|
|
from deluge.configmanager import ConfigManager
|
|
from deluge.ui.client import client
|
|
|
|
from .edittrackersdialog import (
|
|
last_tier_trackers_from_liststore,
|
|
trackers_tiers_from_text,
|
|
)
|
|
from .torrentview_data_funcs import cell_data_size
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class CreateTorrentDialog(object):
|
|
def __init__(self):
|
|
pass
|
|
|
|
def show(self):
|
|
self.builder = Gtk.Builder()
|
|
|
|
ui_filenames = [
|
|
'create_torrent_dialog.ui',
|
|
'create_torrent_dialog.remote_path.ui',
|
|
'create_torrent_dialog.remote_save.ui',
|
|
'create_torrent_dialog.progress.ui',
|
|
]
|
|
for filename in ui_filenames:
|
|
self.builder.add_from_file(
|
|
resource_filename(__package__, os.path.join('glade', filename))
|
|
)
|
|
|
|
self.config = ConfigManager('gtk3ui.conf')
|
|
|
|
self.dialog = self.builder.get_object('create_torrent_dialog')
|
|
self.dialog.set_transient_for(component.get('MainWindow').window)
|
|
|
|
self.builder.connect_signals(self)
|
|
|
|
# path, icon, size
|
|
self.files_treestore = Gtk.TreeStore(str, str, TYPE_UINT64)
|
|
|
|
column = Gtk.TreeViewColumn(_('Filename'))
|
|
render = Gtk.CellRendererPixbuf()
|
|
column.pack_start(render, False)
|
|
column.add_attribute(render, 'icon-name', 1)
|
|
render = Gtk.CellRendererText()
|
|
column.pack_start(render, True)
|
|
column.add_attribute(render, 'text', 0)
|
|
column.set_expand(True)
|
|
self.builder.get_object('treeview_files').append_column(column)
|
|
|
|
column = Gtk.TreeViewColumn(_('Size'))
|
|
render = Gtk.CellRendererText()
|
|
column.pack_start(render, True)
|
|
column.set_cell_data_func(render, cell_data_size, 2)
|
|
self.builder.get_object('treeview_files').append_column(column)
|
|
|
|
self.builder.get_object('treeview_files').set_model(self.files_treestore)
|
|
self.builder.get_object('treeview_files').set_show_expanders(False)
|
|
|
|
# tier, url
|
|
self.trackers_liststore = Gtk.ListStore(int, str)
|
|
|
|
self.builder.get_object('tracker_treeview').append_column(
|
|
Gtk.TreeViewColumn(_('Tier'), Gtk.CellRendererText(), text=0)
|
|
)
|
|
self.builder.get_object('tracker_treeview').append_column(
|
|
Gtk.TreeViewColumn(_('Tracker'), Gtk.CellRendererText(), text=1)
|
|
)
|
|
|
|
self.builder.get_object('tracker_treeview').set_model(self.trackers_liststore)
|
|
self.trackers_liststore.set_sort_column_id(0, Gtk.SortType.ASCENDING)
|
|
|
|
if not client.is_localhost() and client.connected():
|
|
self.builder.get_object('button_remote_path').show()
|
|
else:
|
|
self.builder.get_object('button_remote_path').hide()
|
|
|
|
self.dialog.show()
|
|
|
|
def parse_piece_size_text(self, value):
|
|
psize, metric = value.split()
|
|
psize = int(psize)
|
|
if psize < 32:
|
|
# This is a MiB value
|
|
psize = psize * 1024 * 1024
|
|
else:
|
|
# This is a KiB value
|
|
psize = psize * 1024
|
|
|
|
return psize
|
|
|
|
def adjust_piece_size(self):
|
|
"""Adjusts the recommended piece based on the file/folder/path selected."""
|
|
size = self.files_treestore[0][2]
|
|
model = self.builder.get_object('combo_piece_size').get_model()
|
|
for index, value in enumerate(model):
|
|
psize = self.parse_piece_size_text(value[0])
|
|
pieces = size // psize
|
|
if pieces < 2048 or (index + 1) == len(model):
|
|
self.builder.get_object('combo_piece_size').set_active(index)
|
|
break
|
|
|
|
def on_button_file_clicked(self, widget):
|
|
log.debug('on_button_file_clicked')
|
|
# Setup the filechooserdialog
|
|
chooser = Gtk.FileChooserDialog(
|
|
_('Choose a file'),
|
|
self.dialog,
|
|
Gtk.FileChooserAction.OPEN,
|
|
buttons=(
|
|
_('_Cancel'),
|
|
Gtk.ResponseType.CANCEL,
|
|
_('_Open'),
|
|
Gtk.ResponseType.OK,
|
|
),
|
|
)
|
|
|
|
chooser.set_transient_for(self.dialog)
|
|
chooser.set_select_multiple(False)
|
|
chooser.set_property('skip-taskbar-hint', True)
|
|
|
|
# Run the dialog
|
|
response = chooser.run()
|
|
|
|
if response == Gtk.ResponseType.OK:
|
|
result = chooser.get_filename()
|
|
else:
|
|
chooser.destroy()
|
|
return
|
|
|
|
path = decode_bytes(result)
|
|
|
|
self.files_treestore.clear()
|
|
self.files_treestore.append(
|
|
None, [result, 'text-x-generic-symbolic', get_path_size(path)]
|
|
)
|
|
self.adjust_piece_size()
|
|
chooser.destroy()
|
|
|
|
def on_button_folder_clicked(self, widget):
|
|
log.debug('on_button_folder_clicked')
|
|
# Setup the filechooserdialog
|
|
chooser = Gtk.FileChooserDialog(
|
|
_('Choose a folder'),
|
|
self.dialog,
|
|
Gtk.FileChooserAction.SELECT_FOLDER,
|
|
buttons=(
|
|
_('_Cancel'),
|
|
Gtk.ResponseType.CANCEL,
|
|
_('_Open'),
|
|
Gtk.ResponseType.OK,
|
|
),
|
|
)
|
|
|
|
chooser.set_transient_for(self.dialog)
|
|
chooser.set_select_multiple(False)
|
|
chooser.set_property('skip-taskbar-hint', True)
|
|
# Run the dialog
|
|
response = chooser.run()
|
|
|
|
if response == Gtk.ResponseType.OK:
|
|
result = chooser.get_filename()
|
|
else:
|
|
chooser.destroy()
|
|
return
|
|
|
|
path = decode_bytes(result)
|
|
|
|
self.files_treestore.clear()
|
|
self.files_treestore.append(
|
|
None, [result, 'document-open-symbolic', get_path_size(path)]
|
|
)
|
|
self.adjust_piece_size()
|
|
chooser.destroy()
|
|
|
|
def on_button_remote_path_clicked(self, widget):
|
|
log.debug('on_button_remote_path_clicked')
|
|
dialog = self.builder.get_object('remote_path_dialog')
|
|
entry = self.builder.get_object('entry_path')
|
|
dialog.set_transient_for(self.dialog)
|
|
entry.set_text('/')
|
|
entry.grab_focus()
|
|
response = dialog.run()
|
|
|
|
if response == Gtk.ResponseType.OK:
|
|
result = entry.get_text()
|
|
|
|
def _on_get_path_size(size):
|
|
log.debug('size: %s', size)
|
|
if size > 0:
|
|
self.files_treestore.clear()
|
|
self.files_treestore.append(
|
|
None, [result, 'network-workgroup-symbolic', size]
|
|
)
|
|
self.adjust_piece_size()
|
|
|
|
client.core.get_path_size(result).addCallback(_on_get_path_size)
|
|
client.force_call(True)
|
|
|
|
dialog.hide()
|
|
|
|
def on_button_cancel_clicked(self, widget):
|
|
log.debug('on_button_cancel_clicked')
|
|
self.dialog.destroy()
|
|
|
|
def on_button_save_clicked(self, widget):
|
|
log.debug('on_button_save_clicked')
|
|
if len(self.files_treestore) == 0:
|
|
return
|
|
|
|
# Get the path
|
|
path = self.files_treestore[0][0].rstrip('\\/')
|
|
torrent_filename = '%s.torrent' % os.path.split(path)[-1]
|
|
|
|
is_remote = 'network' in self.files_treestore[0][1]
|
|
|
|
if is_remote:
|
|
# This is a remote path
|
|
dialog = self.builder.get_object('remote_save_dialog')
|
|
dialog.set_transient_for(self.dialog)
|
|
dialog_save_path = self.builder.get_object('entry_save_path')
|
|
dialog_save_path.set_text(path + '.torrent')
|
|
response = dialog.run()
|
|
if response == Gtk.ResponseType.OK:
|
|
result = dialog_save_path.get_text()
|
|
else:
|
|
dialog.hide()
|
|
return
|
|
dialog.hide()
|
|
else:
|
|
# Setup the filechooserdialog
|
|
chooser = Gtk.FileChooserDialog(
|
|
_('Save .torrent file'),
|
|
self.dialog,
|
|
Gtk.FileChooserAction.SAVE,
|
|
buttons=(
|
|
_('_Cancel'),
|
|
Gtk.ResponseType.CANCEL,
|
|
_('_Save'),
|
|
Gtk.ResponseType.OK,
|
|
),
|
|
)
|
|
|
|
chooser.set_transient_for(self.dialog)
|
|
chooser.set_select_multiple(False)
|
|
chooser.set_property('skip-taskbar-hint', True)
|
|
|
|
# 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)
|
|
|
|
chooser.set_current_name(torrent_filename)
|
|
# Run the dialog
|
|
response = chooser.run()
|
|
|
|
if response == Gtk.ResponseType.OK:
|
|
result = chooser.get_filename()
|
|
else:
|
|
chooser.destroy()
|
|
return
|
|
chooser.destroy()
|
|
|
|
# Fix up torrent filename
|
|
if len(result) < 9:
|
|
result += '.torrent'
|
|
elif result[-8:] != '.torrent':
|
|
result += '.torrent'
|
|
|
|
# Get a list of trackers
|
|
trackers = []
|
|
if not len(self.trackers_liststore):
|
|
tracker = None
|
|
else:
|
|
# Create a list of lists [[tier0, ...], [tier1, ...], ...]
|
|
tier_dict = {}
|
|
for tier, tracker in self.trackers_liststore:
|
|
tier_dict.setdefault(tier, []).append(tracker)
|
|
|
|
trackers = [tier_dict[tier] for tier in sorted(tier_dict)]
|
|
# Get the first tracker in the first tier
|
|
tracker = trackers[0][0]
|
|
|
|
# Get a list of webseeds
|
|
textview_buf = self.builder.get_object('textview_webseeds').get_buffer()
|
|
lines = (
|
|
textview_buf.get_text(
|
|
*textview_buf.get_bounds(), include_hidden_chars=False
|
|
)
|
|
.strip()
|
|
.split('\n')
|
|
)
|
|
webseeds = []
|
|
for line in lines:
|
|
line = line.replace('\\', '/') # Fix any mistyped urls.
|
|
if is_url(line):
|
|
webseeds.append(line)
|
|
# Get the piece length in bytes
|
|
combo = self.builder.get_object('combo_piece_size')
|
|
piece_length = self.parse_piece_size_text(
|
|
combo.get_model()[combo.get_active()][0]
|
|
)
|
|
|
|
author = self.builder.get_object('entry_author').get_text()
|
|
comment = self.builder.get_object('entry_comments').get_text()
|
|
private = self.builder.get_object('chk_private_flag').get_active()
|
|
add_to_session = self.builder.get_object('chk_add_to_session').get_active()
|
|
|
|
if is_remote:
|
|
|
|
def torrent_created():
|
|
self.builder.get_object('progress_dialog').hide()
|
|
client.deregister_event_handler(
|
|
'CreateTorrentProgressEvent', on_create_torrent_progress_event
|
|
)
|
|
|
|
def on_create_torrent_progress_event(piece_count, num_pieces):
|
|
self._on_create_torrent_progress(piece_count, num_pieces)
|
|
if piece_count == num_pieces:
|
|
from twisted.internet import reactor
|
|
|
|
reactor.callLater(0.5, torrent_created)
|
|
|
|
client.register_event_handler(
|
|
'CreateTorrentProgressEvent', on_create_torrent_progress_event
|
|
)
|
|
|
|
client.core.create_torrent(
|
|
decode_bytes(path),
|
|
tracker,
|
|
piece_length,
|
|
comment,
|
|
decode_bytes(result),
|
|
webseeds,
|
|
private,
|
|
author,
|
|
trackers,
|
|
add_to_session,
|
|
)
|
|
|
|
else:
|
|
|
|
def hide_progress(result):
|
|
self.builder.get_object('progress_dialog').hide()
|
|
|
|
deferToThread(
|
|
self.create_torrent,
|
|
decode_bytes(path),
|
|
tracker,
|
|
piece_length,
|
|
self._on_create_torrent_progress,
|
|
comment,
|
|
decode_bytes(result),
|
|
webseeds,
|
|
private,
|
|
author,
|
|
trackers,
|
|
add_to_session,
|
|
).addCallback(hide_progress)
|
|
|
|
# Setup progress dialog
|
|
self.builder.get_object('progress_dialog').set_transient_for(
|
|
component.get('MainWindow').window
|
|
)
|
|
self.builder.get_object('progress_dialog').show_all()
|
|
|
|
self.dialog.destroy()
|
|
|
|
def create_torrent(
|
|
self,
|
|
path,
|
|
tracker,
|
|
piece_length,
|
|
progress,
|
|
comment,
|
|
target,
|
|
webseeds,
|
|
private,
|
|
created_by,
|
|
trackers,
|
|
add_to_session,
|
|
):
|
|
import deluge.metafile
|
|
|
|
deluge.metafile.make_meta_file(
|
|
path,
|
|
tracker,
|
|
piece_length,
|
|
progress=progress,
|
|
comment=comment,
|
|
target=target,
|
|
webseeds=webseeds,
|
|
private=private,
|
|
created_by=created_by,
|
|
trackers=trackers,
|
|
)
|
|
|
|
if add_to_session:
|
|
with open(target, 'rb') as _file:
|
|
filedump = b64encode(_file.read())
|
|
client.core.add_torrent_file_async(
|
|
os.path.split(target)[-1],
|
|
filedump,
|
|
{'download_location': os.path.split(path)[0]},
|
|
)
|
|
|
|
def _on_create_torrent_progress(self, value, num_pieces):
|
|
percent = value / num_pieces
|
|
|
|
def update_pbar_with_gobject(percent):
|
|
pbar = self.builder.get_object('progressbar')
|
|
pbar.set_text('%.2f%%' % (percent * 100))
|
|
pbar.set_fraction(percent)
|
|
return False
|
|
|
|
if percent >= 0 and percent <= 1.0:
|
|
# Make sure there are no threads race conditions that can
|
|
# crash the UI while updating it.
|
|
idle_add(update_pbar_with_gobject, percent)
|
|
|
|
def on_button_up_clicked(self, widget):
|
|
log.debug('on_button_up_clicked')
|
|
row = (
|
|
self.builder.get_object('tracker_treeview')
|
|
.get_selection()
|
|
.get_selected()[1]
|
|
)
|
|
if row is None:
|
|
return
|
|
if self.trackers_liststore[row][0] == 0:
|
|
return
|
|
else:
|
|
self.trackers_liststore[row][0] -= 1
|
|
|
|
def on_button_down_clicked(self, widget):
|
|
log.debug('on_button_down_clicked')
|
|
row = (
|
|
self.builder.get_object('tracker_treeview')
|
|
.get_selection()
|
|
.get_selected()[1]
|
|
)
|
|
if row is None:
|
|
return
|
|
self.trackers_liststore[row][0] += 1
|
|
|
|
def on_button_add_clicked(self, widget):
|
|
log.debug('on_button_add_clicked')
|
|
builder = Gtk.Builder()
|
|
builder.add_from_file(
|
|
resource_filename(
|
|
__package__, os.path.join('glade', 'edit_trackers.add.ui')
|
|
)
|
|
)
|
|
dialog = builder.get_object('add_tracker_dialog')
|
|
dialog.set_transient_for(self.dialog)
|
|
textview = builder.get_object('textview_trackers')
|
|
if self.config['createtorrent.trackers']:
|
|
textview.get_buffer().set_text(
|
|
'\n'.join(self.config['createtorrent.trackers'])
|
|
)
|
|
else:
|
|
textview.get_buffer().set_text('')
|
|
textview.grab_focus()
|
|
response = dialog.run()
|
|
|
|
if response == Gtk.ResponseType.OK:
|
|
# Create a list of trackers from the textview buffer
|
|
textview_buf = textview.get_buffer()
|
|
trackers_text = textview_buf.get_text(
|
|
*textview_buf.get_bounds(), include_hidden_chars=False
|
|
)
|
|
log.debug('Create torrent tracker lines: %s', trackers_text)
|
|
self.config['createtorrent.trackers'] = trackers_text.split('/n')
|
|
|
|
# Append trackers liststore with unique trackers and tiers starting from last tier number.
|
|
last_tier, orig_trackers = last_tier_trackers_from_liststore(
|
|
self.trackers_liststore
|
|
)
|
|
for tracker, tier in trackers_tiers_from_text(trackers_text).items():
|
|
if tracker not in orig_trackers:
|
|
self.trackers_liststore.append([tier + last_tier, tracker])
|
|
|
|
dialog.destroy()
|
|
|
|
def on_button_remove_clicked(self, widget):
|
|
log.debug('on_button_remove_clicked')
|
|
row = (
|
|
self.builder.get_object('tracker_treeview')
|
|
.get_selection()
|
|
.get_selected()[1]
|
|
)
|
|
if row is None:
|
|
return
|
|
self.trackers_liststore.remove(row)
|