epour/epour/gui/TorrentProps.py

676 lines
21 KiB
Python

#
# Epour - A bittorrent client using EFL and libtorrent
#
# Copyright 2012-2017 Kai Huuhko <kai.huuhko@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
#
if "long" not in dir(__builtins__):
long = int
import html
import sys
import os
import pipes
from datetime import datetime, timedelta
import logging
log = logging.getLogger("epour.gui")
import libtorrent as lt
from efl import ecore
from efl.evas import EVAS_HINT_EXPAND, EVAS_HINT_FILL
from efl.elementary.genlist import Genlist, GenlistItemClass, \
ELM_GENLIST_ITEM_NONE, ELM_GENLIST_ITEM_TREE, ELM_GENLIST_ITEM_FIELD_TEXT
from efl.elementary.window import DialogWindow
from efl.elementary.button import Button
from efl.elementary.box import Box
from efl.elementary.check import Check
from efl.elementary.label import Label, ELM_WRAP_CHAR
from efl.elementary.entry import Entry
from efl.elementary.frame import Frame
from efl.elementary.object import ELM_SEL_FORMAT_TEXT, \
ELM_SEL_TYPE_CLIPBOARD
from efl.elementary.table import Table
from efl.elementary.progressbar import Progressbar
from efl.elementary.configuration import Configuration
elm_conf = Configuration()
SCALE = elm_conf.scale
from efl.elementary.scroller import Scroller
from efl.elementary.spinner import Spinner
from efl.elementary.fileselector import Fileselector
from efl.elementary.fileselector_entry import FileselectorEntry
from .intrepr import intrepr
from .Widgets import Information, UnitSpinner
EXPAND_BOTH = EVAS_HINT_EXPAND, EVAS_HINT_EXPAND
EXPAND_HORIZ = EVAS_HINT_EXPAND, 0.0
FILL_BOTH = EVAS_HINT_FILL, EVAS_HINT_FILL
FILL_HORIZ = EVAS_HINT_FILL, 0.5
ALIGN_LEFT = 0.0, 0.5
ALIGN_RIGHT = 1.0, 0.5
from collections import defaultdict
FILE_MARKER = '<files>'
class FileClass(GenlistItemClass):
def text_get(self, obj, part, data):
fn, file_entry, n = data
progress = obj.data["progress"][n]
return "{} - {}/{} ({:.0%})".format(
fn,
intrepr(progress),
intrepr(file_entry.size),
float(progress)/float(file_entry.size)
)
def content_get(self, obj, part, data):
fn, file_entry, n = data
h = obj.data["handle"]
if part == "elm.swallow.icon":
check = Check(obj)
check.tooltip_text_set(_("Enable/disable file download"))
check.state = h.file_priority(n)
check.callback_changed_add(lambda x: h.file_priority(n, x.state))
return check
class DirectoryClass(GenlistItemClass):
def text_get(self, obj, part, data):
dir_name, d = data
return "{}".format(
dir_name
)
ITEM_CLASS_FILES = FileClass(item_style="indent")
ITEM_CLASS_DIRS = DirectoryClass()
def attach(branch, file_entry, n, trunk):
"""Insert a branch of directories on its trunk."""
parts = branch.split('/', 1)
if len(parts) == 1: # branch is a file
trunk[FILE_MARKER].append((parts[0], file_entry, n))
else:
node, others = parts
if node not in trunk:
trunk[node] = defaultdict(dict, ((FILE_MARKER, []),))
attach(others, file_entry, n, trunk[node])
class TorrentProps(DialogWindow):
def __init__(self, parent_win, h):
if not h.is_valid():
Information(self, _("Invalid torrent handle."))
return
DialogWindow.__init__(
self, parent_win, "epour", "Epour - %s" % (h.name()),
size=(SCALE * 480, SCALE * 320),
autodel=True
)
scroller = Scroller(self)
self.resize_object_add(scroller)
box = Box(self, size_hint_weight=EXPAND_BOTH)
scroller.content = box
tname = Label(
self, size_hint_align=FILL_HORIZ, line_wrap=ELM_WRAP_CHAR,
ellipsis=True, scale=2.0
)
tname.text = "{}".format(html.escape(h.name()))
tname.show()
box.pack_end(tname)
if h.has_metadata():
f = Frame(self, text=_("Torrent info"), size_hint_align=FILL_HORIZ)
ti = TorrentInfo(
f, h, size_hint_align=FILL_HORIZ)
f.content = ti
box.pack_end(f)
ti.show()
f.show()
f = Frame(self, text=_("Torrent settings"), size_hint_align=FILL_HORIZ)
ts = TorrentSettings(
f, h, size_hint_align=FILL_HORIZ)
f.content = ts
box.pack_end(f)
ts.show()
f.show()
f = Frame(self, text=_("Torrent status"), size_hint_align=FILL_HORIZ)
ts = TorrentStatus(f, h, size_hint_align=FILL_HORIZ)
f.content = ts
box.pack_end(f)
ts.show()
f.show()
magnet_uri = lt.make_magnet_uri(h)
f = Frame(self, size_hint_align=FILL_HORIZ, text=_("Magnet URI"))
me_box = Box(f, horizontal=True)
me = Entry(
me_box, size_hint_align=FILL_HORIZ, size_hint_weight=EXPAND_HORIZ,
editable=False, entry=magnet_uri, line_wrap=ELM_WRAP_CHAR
)
me_box.pack_end(me)
me.show()
me_btn = Button(me_box, text=_("Copy"))
me_btn.callback_clicked_add(
lambda x: me.top_widget.cnp_selection_set(
ELM_SEL_TYPE_CLIPBOARD, ELM_SEL_FORMAT_TEXT, me.text
)
)
me_btn.show()
me_box.pack_end(me_btn)
me_box.show()
f.content = me_box
f.show()
box.pack_end(f)
# xbtn = Button(box, text="Close")
# xbtn.callback_clicked_add(lambda x: self.delete())
# box.pack_end(xbtn)
# xbtn.show()
box.show()
scroller.show()
class TorrentFiles(DialogWindow):
def __init__(self, parent_win, h):
DialogWindow.__init__(
self,
parent_win,
"epour",
_("Epour - Files for torrent: %s") % (h.name()),
size=(SCALE * 480, SCALE * 320),
autodel=True
)
box = Box(self, size_hint_weight=EXPAND_BOTH)
self.resize_object_add(box)
file_dict = defaultdict(dict, ((FILE_MARKER, []),))
filelist = Genlist(
box, size_hint_align=FILL_BOTH, size_hint_weight=EXPAND_BOTH,
homogeneous=True, tree_effect_enabled=True
)
filelist.data["handle"] = h
filelist.data["file_dict"] = file_dict
filelist.data["progress"] = h.file_progress()
def update_progress(h):
#log.debug("File progress TICK")
filelist.data["progress"] = h.file_progress()
for it in filelist.realized_items:
it.fields_update("*", ELM_GENLIST_ITEM_FIELD_TEXT)
return True
timer = ecore.Timer(5.0, update_progress, h)
self.on_del_add(lambda x: timer.delete())
self.callback_withdrawn_add(lambda x: timer.freeze())
self.callback_iconified_add(lambda x: timer.freeze())
self.callback_normal_add(lambda x: timer.thaw())
filelist.callback_expand_request_add(self.expand_request_cb)
filelist.callback_contract_request_add(self.contract_request_cb)
filelist.callback_expanded_add(self.item_expanded_cb)
filelist.callback_contracted_add(self.item_contracted_cb)
filelist.callback_activated_add(self.item_activated_cb)
self.populate(filelist)
filelist.show()
sel_all = Button(self, text=_("Select all"))
sel_all.callback_clicked_add(self.select_all_cb, filelist, h, True)
sel_all.show()
sel_none = Button(self, text=_("Select none"))
sel_none.callback_clicked_add(self.select_all_cb, filelist, h, False)
sel_none.show()
# xbtn = Button(self)
# xbtn.text = "Close"
# xbtn.callback_clicked_add(lambda x: parent.item_pop())
# xbtn.show()
btn_box = Box(self, horizontal=True)
btn_box.pack_end(sel_all)
btn_box.pack_end(sel_none)
#btn_box.pack_end(xbtn)
btn_box.show()
box.pack_end(filelist)
box.pack_end(btn_box)
box.show()
self.show()
def select_all_cb(self, btn, filelist, h, all_selected=True):
priorities = h.file_priorities()
for n, p in enumerate(priorities):
priorities[n] = all_selected
h.prioritize_files(priorities)
filelist.realized_items_update()
def populate(self, filelist):
h = filelist.data["handle"]
file_dict = filelist.data["file_dict"]
filelist.clear()
i = h.get_torrent_info()
entries = i.files()
for n, f in enumerate(entries):
attach(f.path, f, n, file_dict)
def construct(d, parent=None):
"""Construct a genlist tree."""
for key in sorted(d.keys()):
value = d[key]
if key == FILE_MARKER:
if value:
for fn, fe, n in value:
filelist.item_append(
ITEM_CLASS_FILES,
(fn, fe, n),
parent,
ELM_GENLIST_ITEM_NONE)
else:
it = filelist.item_append(
ITEM_CLASS_DIRS,
(key, value),
parent,
ELM_GENLIST_ITEM_TREE)
it.expanded = True
construct(file_dict)
def expand_request_cb(self, gl, item):
item.expanded = True
def contract_request_cb(self, gl, item):
item.expanded = False
def item_expanded_cb(self, gl, item):
dir_node, d = item.data
for key in sorted(d.keys()):
value = d[key]
if key == FILE_MARKER:
if value:
for fn, fe, n in value:
gl.item_append(
ITEM_CLASS_FILES,
(fn, fe, n),
item,
ELM_GENLIST_ITEM_NONE)
else:
gl.item_append(
ITEM_CLASS_DIRS,
(key, value),
item,
ELM_GENLIST_ITEM_TREE)
def item_contracted_cb(self, gl, item):
item.subitems_clear()
def item_activated_cb(self, gl, item):
if item.type != ELM_GENLIST_ITEM_NONE:
return
fn, fe, n = item.data
h = gl.data["handle"]
progress = h.file_progress()[n]
if progress == 0:
log.error("Tried to open a file with size 0")
return
if progress < fe.size:
log.warn("Opening an incomplete file")
path = os.path.join(h.save_path().rstrip("\0"), fe.path)
if sys.platform == 'linux2':
ecore.Exe('xdg-open %s' % pipes.quote(path))
else:
os.startfile(path)
class TorrentInfo(Box):
def __init__(self, parent, h, *args, **kwargs):
Box.__init__(self, parent, *args, **kwargs)
info = h.get_torrent_info()
table = Table(self, homogeneous=True, size_hint_align=FILL_HORIZ)
i = 0
for func in info.comment, info.creation_date, info.creator:
try:
w = func()
except Exception as e:
log.debug(e)
else:
if w:
l = Label(table, size_hint_align=ALIGN_LEFT)
l.text = func.__name__.replace("_", " ").capitalize()
table.pack(l, 0, i, 1, 1)
l.show()
v = Label(table, ellipsis=True, size_hint_align=ALIGN_LEFT)
v.text = html.escape(str(w))
table.pack(v, 1, i, 1, 1)
v.show()
i += 1
tpriv = Check(self)
tpriv.size_hint_align = 0.0, 0.0
tpriv.text = _("Private")
tpriv.tooltip_text_set(
"Whether this torrent is private,<br>"
"i.e. it should not be distributed on the<br>"
"trackerless network (the kademlia DHT)."
)
tpriv.disabled = True
tpriv.state = info.priv()
fl_btn = Button(self, size_hint_align=ALIGN_LEFT)
fl_btn.text = "Files ->"
fl_btn.callback_clicked_add(lambda x: TorrentFiles(self.top_widget, h))
self.pack_end(table)
self.pack_end(tpriv)
self.pack_end(fl_btn)
table.show()
tpriv.show()
fl_btn.show()
class TorrentSettings(Box):
status_checks = {
"super_seeding": "super_seeding",
"sequential_download": "set_sequential_download",
"upload_mode": "set_upload_mode",
"share_mode": "set_share_mode",
"ip_filter_applies": "apply_ip_filter"
}
handle_unit_spinners = {
"upload_limit": "set_upload_limit",
"download_limit": "set_download_limit"
}
handle_spinners = {
"max_uploads": "set_max_uploads",
"max_connections": "set_max_connections"
}
def __init__(self, parent, handle, *args, **kwargs):
super(self.__class__, self).__init__(parent, *args, **kwargs)
s = handle.status()
t = Table(self, size_hint_align=FILL_HORIZ, homogeneous=True)
l = Label(t)
l.size_hint_align = ALIGN_LEFT
l.text = _("Storage path")
t.pack(l, 0, 0, 1, 1)
l.show()
fs = FsEntry(t)
fs.size_hint_align = FILL_HORIZ
fs.text = _("Select")
fs.folder_only = True
fs.expandable = False
fs.path = handle.save_path()
def fs_cb(fs, path):
if not path:
return
if os.path.isdir(path):
handle.move_storage(path)
fs.callback_file_chosen_add(fs_cb)
t.pack(fs, 1, 0, 1, 1)
fs.show()
self.pack_end(t)
t.show()
t = Table(self, size_hint_align=FILL_HORIZ, homogeneous=True)
#Checks (bool)
for i, (getter, setter) in enumerate(self.status_checks.items()):
l = Label(t)
l.size_hint_align = ALIGN_LEFT
l.text = getter.replace("_", " ").capitalize()
t.pack(l, 0, i, 1, 1)
l.show()
w = Check(t)
w.size_hint_align = ALIGN_RIGHT
w.state = getattr(s, getter)
setter = getattr(handle, self.status_checks[getter])
w.callback_changed_add(lambda x, z=setter: z(x.state))
t.pack(w, 1, i, 1, 1)
w.show()
self.pack_end(t)
t.show()
t = Table(self, size_hint_align=FILL_HORIZ, homogeneous=True)
#Unit Spinners (int)
for i, (getter, setter) in enumerate(
self.handle_unit_spinners.items()
):
l = Label(t)
l.size_hint_align = ALIGN_LEFT
l.text = getter.replace("_", " ").capitalize()
t.pack(l, 0, i, 1, 1)
l.show()
w = UnitSpinner(self, "B/s", 1024, UnitSpinner.binary_prefixes)
w.size_hint_align = FILL_HORIZ
w.spinner.special_value_add(0, _("disabled"))
w.set_value(getattr(handle, getter)())
setter = getattr(handle, self.handle_unit_spinners[getter])
w.callback_changed_add(lambda x, y, z=setter: z(y))
t.pack(w, 1, i, 1, 1)
w.show()
self.pack_end(t)
t.show()
t = Table(self, size_hint_align=FILL_HORIZ, homogeneous=True)
#Spinners (int)
for i, (getter, setter) in enumerate(self.handle_spinners.items()):
l = Label(t)
l.size_hint_align = ALIGN_LEFT
l.text = getter.replace("_", " ").capitalize()
t.pack(l, 0, i, 1, 1)
l.show()
w = Spinner(t)
w.size_hint_align = FILL_HORIZ
w.min_max = -1.0, 16777215.0
w.special_value_add(-1.0, "disabled")
w.value = getattr(handle, getter)()
setter = getattr(handle, self.handle_spinners[getter])
w.callback_delay_changed_add(lambda x, z=setter: z(int(x.value)))
t.pack(w, 1, i, 1, 1)
w.show()
self.pack_end(t)
t.show()
class TorrentStatus(Table):
state_str = (
_('Queued'), _('Checking'), _('Downloading metadata'), _('Downloading'),
_('Finished'), _('Seeding'), _('Allocating'), _('Checking resume data')
)
ignored_keys = (
"progress_ppm", "distributed_copies", "states", "auto_managed"
)
byte_values = (
"all_time_download", "all_time_upload", "block_size", "total_done",
"total_wanted", "total_wanted_done", "total_failed_bytes",
"total_payload_download", "total_payload_upload", "total_download",
"total_upload", "total_redundant_bytes"
)
byte_transfer_values = (
"download_rate", "download_payload_rate", "upload_rate",
"upload_payload_rate"
)
timedelta_values = (
"active_time", "seeding_time", "time_since_download",
"time_since_upload", "finished_time"
)
datetime_values = (
"added_time", "last_seen_complete", "completed_time"
)
def __init__(self, parent, h, *args, **kwargs):
Table.__init__(self, parent, *args, **kwargs)
s = h.status()
self.widgets = []
i = 0
for k in dir(s):
if k.startswith("__") or k in self.ignored_keys:
continue
try:
v = getattr(s, k)
except Exception as e:
log.debug(e)
continue
v = self.convert_value(k, v)
w = None
if k == "state":
pass
elif isinstance(v, lt.torrent_status.states):
continue
elif isinstance(v, list):
continue # TODO
elif k == "progress":
w = Progressbar(self)
w.size_hint_align = FILL_HORIZ
elif isinstance(v, bool):
w = Check(self)
w.size_hint_align = ALIGN_RIGHT
w.disabled = True
if not w:
try:
w = Label(self)
w.size_hint_align = ALIGN_RIGHT
except Exception as e:
log.debug(e)
continue
w.data["key"] = k
self.populate(w, v)
self.widgets.append(w)
l = Label(self)
l.size_hint_align = ALIGN_LEFT
l.text = str(k).replace("_", " ").capitalize()
self.pack(l, 0, i, 1, 1)
l.show()
self.pack(w, 1, i, 1, 1)
w.show()
i += 1
def update():
#log.debug("Torrent status TICK")
s = h.status()
for w in self.widgets:
key = w.data["key"]
v = self.convert_value(key, getattr(s, key))
self.populate(w, v)
return True
timer = ecore.Timer(5.0, update)
self.on_del_add(lambda x: timer.delete())
self.top_widget.callback_withdrawn_add(lambda x: timer.freeze())
self.top_widget.callback_iconified_add(lambda x: timer.freeze())
self.top_widget.callback_normal_add(lambda x: timer.thaw())
@staticmethod
def populate(w, v):
if isinstance(w, Label):
w.text = str(v)
elif isinstance(w, Check):
w.state = v
elif isinstance(w, Progressbar):
w.value = v
def convert_value(self, k, v):
if k == "state":
v = str(v).replace("_", " ").capitalize()
elif k in self.datetime_values:
v = datetime.fromtimestamp(v)
elif k in self.timedelta_values:
v = timedelta(seconds=v)
elif isinstance(v, list):
pass
elif isinstance(v, lt.storage_mode_t):
v = str(v).replace("_", " ").capitalize()
elif isinstance(v, (int, long, bytes, str)):
if k in self.byte_values:
v = intrepr(v)
if k in self.byte_transfer_values:
v = intrepr(v) + "/s"
return v
class FsEntry(Fileselector, FileselectorEntry):
def __init__(self, parent, *args, **kwargs):
FileselectorEntry.__init__(self, parent, *args, **kwargs)