Hackathlon

- improvements to add torrent-dialog
 - torrents are stored as dicts between sessions
 - session status panel is contained in bottom half of the window
 - torrents and settings files changed, upgrade should work cleanly
 - 3rd party module pyperclip removed (used for pyefl 1.7 compatibility)
 - many small fixes
This commit is contained in:
Kai Huuhko 2014-06-30 17:02:27 +03:00
parent 4ffbcad8ca
commit d04fdf23f5
19 changed files with 1660 additions and 1815 deletions

3
.gitignore vendored
View File

@ -1,3 +1,2 @@
*.py[co]
epour.sublime-project
epour.sublime-workspace
build/

36
TODO
View File

@ -1,22 +1,26 @@
I18N:
This should wait until most of the features are in and the strings are stable.
☐ Make strings localizable
☐ Create pot file
☐ Generate po files
☐ Make strings localizable
☐ Create pot file
☐ Generate po files
Add Torrent-dialog:
☐ Create a new class to hold a torrent and store its instances between sessions.
Migrate the old dict(ihash->torrent_file) to the new list of torrents
☐ Dialog window
☐ Initial paused-state
☐ File selector
☐ Storage path
☐ Add preferences for initial/automatic values for the above
✔ Use dicts to hold torrent info between sessions. @done (15:57 30.06.2014)
✔ Migrate the old ihash->torrent_file dict to the new list of torrents @done (15:57 30.06.2014)
✔ Dialog window @done (15:57 30.06.2014)
✔ Options @done (15:57 30.06.2014)
✔ File selector @done (15:57 30.06.2014)
✔ Storage path @done (15:57 30.06.2014)
☐ Add preferences for initial/automatic values for the above
Misc:
☐ Torrent tooltips
handle.status()
Using handle.status()
☐ More tooltips in general
☐ More D-Bus controls #epour/Epour.py@EpourDBus
✔ Auto-paste magnet links from clipboard #epour/gui/Main.py@select_torrent @done (13-08-11 21:45)
Note: The pasted text is not filtered.
☐ Construct and populate #epour/gui/Preferences.py@SessionSettings with an Idler
✔ Move proxy settings to its own naviframe page and don't autocollapse the frames @done (13-08-18 18:02)
☐ More D-Bus controls ./epour/Epour.py>EpourDBus
☐ Auto-paste magnet links from clipboard ./epour/gui/Main.py>TorrentSelector
Remake this feature
✘ Construct and populate ./epour/gui/Preferences.py>PreferencesSession with an Idler @cancelled (21:20 01.07.2014)
☐ Moving finished torrents to a different folder
___________________
Archive:
✔ Move proxy settings to its own naviframe page and don't autocollapse the frames @done (13-08-18 18:02) @project(Misc)

View File

@ -1,9 +1,10 @@
#!/usr/bin/env python
#!/usr/bin/python
import sys
import logging
import epour.Epour as Epour
from epour import Epour
epour = Epour.Epour(sys.argv[1:])
epour = Epour()
epour.gui.run()
epour.quit()
logging.shutdown()

2
debian/control vendored
View File

@ -7,6 +7,6 @@ Standards-Version: 3.9.1
Package: epour
Architecture: all
Depends: ${misc:Depends}, ${python:Depends}, python-libtorrent, python-evas, python-elementary, python-edbus, python-ecore
Depends: ${misc:Depends}, ${python:Depends}, python-libtorrent, python-efl
Description: Simple torrent client
Epour is a simple torrent client using EFL and libtorrent.

View File

@ -1,146 +0,0 @@
#!/usr/bin/env python2
#
# Epour - A bittorrent client using EFL and libtorrent
#
# Copyright 2012-2013 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 3 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.
#
import sys
import os
from Globals import conf_dir, conf_path, data_dir
import logging
for d in conf_dir, data_dir:
if not os.path.exists(d):
os.mkdir(d, 0700)
def setup_log():
log = logging.getLogger("epour")
log.propagate = False
log.setLevel(logging.INFO)
ch = logging.StreamHandler()
ch_formatter = logging.Formatter('%(name)s: [%(levelname)s] %(message)s')
ch.setFormatter(ch_formatter)
ch.setLevel(logging.DEBUG)
log.addHandler(ch)
fh = logging.FileHandler(os.path.join(data_dir, "epour.log"))
fh_formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(fh_formatter)
fh.setLevel(logging.ERROR)
log.addHandler(fh)
return log
log = setup_log()
try:
from e_dbus import DBusEcoreMainLoop
except ImportError:
from efl.dbus_mainloop import DBusEcoreMainLoop
import dbus
ml = DBusEcoreMainLoop()
dbus.set_default_main_loop(ml)
import dbus.service
bus = dbus.SessionBus()
dbo = None
try:
dbo = bus.get_object("net.launchpad.epour", "/net/launchpad/epour")
except dbus.exceptions.DBusException:
pass
if dbo:
if sys.argv[1:]:
for f in sys.argv[1:]:
log.info("Sending %s via dbus" % f)
dbo.AddTorrent(f, dbus_interface="net.launchpad.epour")
sys.exit()
from session import Session
from gui import MainInterface
class Epour(object):
def __init__(self, torrents=None):
session = self.session = Session(self)
session.load_state()
self.gui = MainInterface(self, session)
session.load_torrents()
# Add torrents from command line
if torrents:
for t in torrents:
self.session.add_torrent(t)
self.dbusname = dbus.service.BusName(
"net.launchpad.epour", dbus.SessionBus()
)
self.dbo = EpourDBus(self)
self.gui.run()
def quit(self):
session = self.session
session.pause()
try:
session.save_torrents()
except:
log.exception("Saving torrents failed")
try:
session.save_state()
except:
log.exception("Saving session state failed")
try:
session.save_conf()
except:
log.exception("Saving conf failed")
class EpourDBus(dbus.service.Object):
log = logging.getLogger("epour.dbus")
def __init__(self, parent):
self.parent = parent
dbus.service.Object.__init__(self, dbus.SessionBus(),
"/net/launchpad/epour", "net.launchpad.epour")
self.props = {
}
@dbus.service.method(dbus_interface='net.launchpad.epour',
in_signature='s', out_signature='')
def AddTorrent(self, f):
self.log.info("Adding %s from dbus" % f)
self.parent.session.add_torrent(str(f))
if __name__ == "__main__":
log = logging.getLogger("epour")
log.setLevel(logging.DEBUG)
epour = Epour(sys.argv[1:])
logging.shutdown()

View File

@ -1,6 +1,6 @@
import os
version = "0.5.2.0"
version = "0.6.0"
conf_dir = os.path.expanduser(os.path.join("~", ".config", "epour"))
conf_path = os.path.join(conf_dir, "epour.conf")
data_dir = os.path.expanduser(os.path.join("~", ".local", "share", "epour"))

View File

@ -0,0 +1,223 @@
#!/usr/bin/python
#
# Epour - A bittorrent client using EFL and libtorrent
#
# Copyright 2012-2014 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 3 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.
#
import sys
from argparse import ArgumentParser
parser = ArgumentParser(description="A BitTorrent client.")
parser.add_argument(
'-v', '--verbose', action="count", help="max is -vvv")
parser.add_argument(
'--add-with-dialog', action="store_true",
help="Torrents to be added from arguments open a dialog"
)
parser.add_argument(
'torrents', nargs="*", help="file path, magnet uri, or info hash",
metavar="TORRENT")
args = parser.parse_args()
from efl.dbus_mainloop import DBusEcoreMainLoop
import dbus
ml = DBusEcoreMainLoop()
dbus.set_default_main_loop(ml)
import dbus.service
bus = dbus.SessionBus()
try:
dbo = bus.get_object("net.launchpad.epour", "/net/launchpad/epour")
except dbus.exceptions.DBusException:
pass
else:
for f in args.torrents:
print("Sending %s via dbus".format(f))
dbo.AddTorrent(f, dbus_interface="net.launchpad.epour")
sys.exit()
import os
from ConfigParser import SafeConfigParser
from Globals import conf_dir, conf_path, data_dir
import logging
for d in conf_dir, data_dir:
if not os.path.exists(d):
os.mkdir(d, 0700)
from session import Session
from gui import MainInterface
class Epour(object):
def __init__(self):
self.log = self.setup_log()
self.conf = self.setup_conf()
session = self.session = Session(self.conf)
session.load_state()
self.gui = MainInterface(self, session)
self.dbusname = dbus.service.BusName(
"net.launchpad.epour", dbus.SessionBus()
)
self.dbo = EpourDBus(session, self.gui, self.conf)
self.session.load_torrents()
for t in args.torrents:
if args.add_with_dialog:
self.gui.add_torrent(t)
else:
add_dict = {
"save_path": self.conf.get("Settings", "storage_path"),
"flags": 592
}
if os.path.isfile(t):
self.session.add_torrent_from_file(add_dict, t)
elif t.startswith("magnet:"):
self.session.add_torrent_from_magnet(add_dict, t)
else:
self.session.add_torrent_from_hash(add_dict, t)
def setup_log(self):
log_level = logging.ERROR
if args.verbose:
log_level -= 10 * args.verbose
log = logging.getLogger("epour")
log.propagate = False
log.setLevel(log_level)
ch = logging.StreamHandler()
ch_formatter = logging.Formatter(
'%(name)s: [%(levelname)s] %(message)s'
)
ch.setFormatter(ch_formatter)
ch.setLevel(log_level)
log.addHandler(ch)
fh = logging.FileHandler(os.path.join(data_dir, "epour.log"))
fh_formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
fh.setFormatter(fh_formatter)
fh.setLevel(logging.ERROR)
log.addHandler(fh)
return log
def setup_conf(self):
conf = SafeConfigParser({
"storage_path": os.path.expanduser(
os.path.join("~", "Downloads")
),
"confirm_exit": str(False),
"dialog_add_dbus": str(True),
"delete_original": str(False),
"listen_low": str(0),
"listen_high": str(0),
})
conf.read(conf_path)
if not conf.has_section("Settings"):
conf.add_section("Settings")
return conf
def save_conf(self):
with open(conf_path, 'wb') as configfile:
self.conf.write(configfile)
def quit(self):
session = self.session
session.pause()
try:
session.save_torrents()
except:
self.log.exception("Saving torrents failed")
try:
session.save_state()
except:
self.log.exception("Saving session state failed")
try:
self.save_conf()
except:
self.log.exception("Saving conf failed")
class EpourDBus(dbus.service.Object):
log = logging.getLogger("epour.dbus")
def __init__(self, session, gui, conf):
self.session = session
self.gui = gui
self.conf = conf
dbus.service.Object.__init__(
self, dbus.SessionBus(),
"/net/launchpad/epour", "net.launchpad.epour"
)
self.props = {
}
@dbus.service.method(dbus_interface='net.launchpad.epour',
in_signature='s', out_signature='')
def AddTorrent(self, t):
self.log.info("Adding %s from dbus" % t)
#self.session.add_torrent(str(t))
try:
if self.conf.getboolean("Settings", "dialog_add_dbus"):
self.gui.add_torrent(t)
else:
add_dict = {
"save_path": self.conf.get("Settings", "storage_path"),
"flags": 592
}
if os.path.isfile(t):
self.session.add_torrent_from_file(add_dict, t)
elif t.startswith("magnet:"):
self.session.add_torrent_from_magnet(add_dict, t)
else:
self.session.add_torrent_from_hash(add_dict, t)
except Exception:
self.log.exception("Error while adding torrent from dbus")
if __name__ == "__main__":
efllog = logging.getLogger("efl")
efllog.setLevel(logging.INFO)
efllog_formatter = logging.Formatter(
'%(name)s: [%(levelname)s] %(message)s'
)
efllog_handler = logging.StreamHandler()
efllog_handler.setFormatter(efllog_formatter)
efllog.addHandler(efllog_handler)
epour = Epour()
epour.gui.run()
epour.quit()
logging.shutdown()

View File

@ -1,21 +0,0 @@
Copyright (c) 2002-2005 ActiveState Corp.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,530 +0,0 @@
#
# Epour - A bittorrent client using EFL and libtorrent
#
# Copyright 2012-2013 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 3 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.
#
import os
import cgi
import logging
from datetime import timedelta
import libtorrent as lt
try:
from efl.evas import EVAS_ASPECT_CONTROL_VERTICAL, Rectangle
from efl.ecore import Timer
from efl import elementary as elm
from efl.elementary.genlist import Genlist, GenlistItemClass, \
ELM_GENLIST_ITEM_FIELD_TEXT, ELM_GENLIST_ITEM_FIELD_CONTENT, \
ELM_OBJECT_SELECT_MODE_NONE, ELM_LIST_COMPRESS
from efl.elementary.window import StandardWindow
from efl.elementary.icon import Icon
from efl.elementary.box import Box
from efl.elementary.label import Label
from efl.elementary.button import Button
from efl.elementary.innerwindow import InnerWindow
from efl.elementary.frame import Frame
from efl.elementary.fileselector import Fileselector
from efl.elementary.entry import Entry
from efl.elementary.object import ELM_SEL_TYPE_CLIPBOARD, ELM_SEL_FORMAT_TEXT
from efl.elementary.panel import Panel, ELM_PANEL_ORIENT_BOTTOM
from efl.elementary.table import Table
from efl.elementary.separator import Separator
from efl.elementary.menu import Menu
from efl.elementary.configuration import Configuration
from efl.elementary.toolbar import Toolbar, ELM_TOOLBAR_SHRINK_NONE, \
ELM_OBJECT_SELECT_MODE_NONE
except ImportError:
from evas import EVAS_ASPECT_CONTROL_VERTICAL, Rectangle
from ecore import Timer
import elementary as elm
from elementary import Genlist, GenlistItemClass, StandardWindow, Icon, \
Box, Label, Button, ELM_GENLIST_ITEM_FIELD_TEXT, \
ELM_GENLIST_ITEM_FIELD_CONTENT, InnerWindow, Frame, \
Fileselector, Entry, Panel, ELM_PANEL_ORIENT_BOTTOM, \
ELM_OBJECT_SELECT_MODE_NONE, Table, Separator, Menu, Configuration, \
ELM_LIST_COMPRESS, Toolbar
from TorrentInfo import TorrentInfo
from Preferences import PreferencesGeneral, PreferencesProxy, PreferencesSession
from Notify import ConfirmExit, Error, Information
from intrepr import intrepr
class MainInterface(object):
def __init__(self, parent, session):
self.parent = parent
self.session = session
elm_conf = Configuration()
scale = elm_conf.scale
self.log = logging.getLogger("epour.gui")
self.torrentitems = {}
win = self.win = StandardWindow("epour", "Epour")
win.callback_delete_request_add(lambda x: elm.exit())
win.screen_constrain = True
win.size = 480 * scale, 400 * scale
mbox = Box(win)
mbox.size_hint_weight = 1.0, 1.0
win.resize_object_add(mbox)
mbox.show()
tb = Toolbar(win)
tb.homogeneous = False
tb.shrink_mode = ELM_TOOLBAR_SHRINK_NONE
tb.select_mode = ELM_OBJECT_SELECT_MODE_NONE
tb.size_hint_align = -1.0, 0.0
tb.menu_parent = win
item = tb.item_append("document-new", "Add torrent",
lambda t,i: self.select_torrent())
def pause_session(it):
self.session.pause()
it.state_set(it.state_next())
def resume_session(it):
session.resume()
del it.state
item = tb.item_append("media-playback-pause", "Pause Session",
lambda tb, it: pause_session(it))
item.state_add("media-playback-start", "Resume Session",
lambda tb, it: resume_session(it))
item = tb.item_append("preferences-system", "Preferences")
item.menu = True
item.menu.item_add(None, "General", "preferences-system",
lambda o,i: PreferencesGeneral(self, self.session))
item.menu.item_add(None, "Proxy", "preferences-system",
lambda o,i: PreferencesProxy(self, self.session))
item.menu.item_add(None, "Session", "preferences-system",
lambda o,i: PreferencesSession(self, self.session))
item = tb.item_append("application-exit", "Exit",
lambda tb, it: elm.exit())
mbox.pack_start(tb)
tb.show()
self.tlist = tlist = Genlist(win)
tlist.select_mode = ELM_OBJECT_SELECT_MODE_NONE
tlist.mode = ELM_LIST_COMPRESS
tlist.callback_activated_add(self.item_activated_cb)
tlist.homogeneous = True
tlist.size_hint_weight = 1.0, 1.0
tlist.size_hint_align = -1.0, -1.0
tlist.show()
mbox.pack_end(tlist)
pad = Rectangle(win.evas)
pad.size_hint_weight = 1.0, 1.0
p = Panel(win)
p.color = 200,200,200,200
p.size_hint_weight = 1.0, 1.0
p.size_hint_align = -1.0, -1.0
p.orient = ELM_PANEL_ORIENT_BOTTOM
p.content = SessionStatus(win, session)
p.hidden = True
p.show()
topbox = Box(win)
topbox.horizontal = True
topbox.size_hint_weight = 1.0, 1.0
win.resize_object_add(topbox)
topbox.pack_end(pad)
topbox.pack_end(p)
topbox.stack_above(mbox)
topbox.show()
session.alert_manager.callback_add(
"torrent_added_alert", self.torrent_added_cb)
session.alert_manager.callback_add(
"torrent_removed_alert", self.torrent_removed_cb)
for a_name in "torrent_paused_alert", "torrent_resumed_alert":
session.alert_manager.callback_add(a_name, self.update_icon)
session.alert_manager.callback_add(
"state_changed_alert", self.state_changed_cb)
Timer(15.0, lambda: session.alert_manager.callback_add(
"torrent_finished_alert", self.torrent_finished_cb))
def select_torrent(self):
sel = Fileselector(self.win)
sel.expandable = False
sel.path_set(os.path.expanduser("~"))
sel.size_hint_weight_set(1.0, 1.0)
sel.size_hint_align_set(-1.0, -1.0)
sel.show()
sf = Frame(self.win)
sf.size_hint_weight_set(1.0, 1.0)
sf.size_hint_align_set(-1.0, -1.0)
sf.text = "Select torrent file"
sf.content = sel
sf.show()
magnet = Entry(self.win)
magnet.single_line = True
magnet.scrollable = True
if hasattr(magnet, "cnp_selection_get"):
magnet.cnp_selection_get(ELM_SEL_TYPE_CLIPBOARD, ELM_SEL_FORMAT_TEXT)
else:
import pyperclip
t = pyperclip.paste()
if t is not None and t.startswith("magnet:"):
magnet.entry = t
magnet.show()
mf = Frame(self.win)
mf.size_hint_weight_set(1.0, 0.0)
mf.size_hint_align_set(-1.0, 0.0)
mf.text = "Or enter magnet URI here"
mf.content = magnet
mf.show()
mbtn = Button(self.win)
mbtn.text = "Done"
mbtn.show()
mbox = Box(self.win)
mbox.size_hint_weight_set(1.0, 0.0)
mbox.size_hint_align_set(-1.0, 0.0)
mbox.horizontal = True
mbox.pack_end(mf)
mbox.pack_end(mbtn)
mbox.show()
box = Box(self.win)
box.size_hint_weight = (1.0, 1.0)
box.size_hint_align = (-1.0, -1.0)
box.pack_end(sf)
box.pack_end(mbox)
box.show()
inwin = InnerWindow(self.win)
inwin.content = box
sel.callback_done_add(self.add_torrent_cb)
sel.callback_done_add(lambda x, y: inwin.delete())
mbtn.callback_clicked_add(self.add_magnet_uri_cb, magnet)
mbtn.callback_clicked_add(lambda x: inwin.delete())
inwin.activate()
def add_torrent_cb(self, filesel, t):
if t:
self.session.add_torrent(t)
def add_magnet_uri_cb(self, btn, magnet):
self.add_torrent_cb(None, magnet.text)
def state_changed_cb(self, a):
h = a.handle
ihash = str(h.info_hash())
if not h.is_valid():
self.log.debug("State changed for invalid handle.")
return
#elif not self.torrentitems.has_key(ihash):
#self.add_torrent_item(h)
self.update_icon(a)
def run(self):
self.win.show()
self.timer = Timer(1.0, self.update)
elm.run()
self.quit()
def update(self):
for v in self.tlist.realized_items_get():
v.fields_update("*", ELM_GENLIST_ITEM_FIELD_TEXT)
return True
def update_icon(self, a):
h = a.handle
if not h.is_valid(): return
ihash = str(h.info_hash())
if not self.torrentitems.has_key(ihash): return
self.torrentitems[ihash].fields_update(
"elm.swallow.icon", ELM_GENLIST_ITEM_FIELD_CONTENT
)
def torrent_added_cb(self, a):
h = a.handle
self.add_torrent_item(h)
def add_torrent_item(self, h):
ihash = str(h.info_hash())
itc = TorrentClass(self.session, "double_label")
item = self.tlist.item_append(itc, h)
self.torrentitems[ihash] = item
def torrent_removed_cb(self, a):
self.remove_torrent_item(a.info_hash)
def remove_torrent_item(self, info_hash):
it = self.torrentitems.pop(str(info_hash), None)
if it is not None:
it.delete()
def item_activated_cb(self, gl, item):
h = item.data
menu = Menu(self.win)
menu.item_add(
None,
"Resume" if h.is_paused() else "Pause",
None,
self.resume_torrent_cb if h.is_paused() else self.pause_torrent_cb,
h
)
q = menu.item_add(None, "Queue", None, None)
menu.item_add(q, "Up", None, lambda x, y: h.queue_position_up())
menu.item_add(q, "Down", None, lambda x, y: h.queue_position_down())
menu.item_add(q, "Top", None, lambda x, y: h.queue_position_top())
menu.item_add(q, "Bottom", None, lambda x, y: h.queue_position_bottom())
rem = menu.item_add(None, "Remove torrent", None,
self.remove_torrent_cb, item, h, False)
menu.item_add(rem, "and data files", None,
self.remove_torrent_cb, item, h, True)
menu.item_add(None, "Force re-check", None,
self.force_recheck, h)
menu.item_separator_add(None)
menu.item_add(None, "Torrent preferences", None,
self.torrent_preferences_cb, h)
menu.move(*self.win.evas.pointer_canvas_xy_get())
menu.show()
def resume_torrent_cb(self, menu, item, h):
h.resume()
h.auto_managed(True)
def pause_torrent_cb(self, menu, item, h):
h.auto_managed(False)
h.pause()
def force_recheck(self, menu, item, h):
h.force_recheck()
def remove_torrent_cb(self, menu, item, glitem, h, with_data=False):
menu.close()
ihash = self.parent.session.remove_torrent(h, with_data)
def torrent_preferences_cb(self, menu, item, h):
self.i = TorrentInfo(self, h)
def show_error(self, title, text):
Error(self.win, title, text)
def torrent_finished_cb(self, a):
msg = "Torrent {} has finished downloading.".format(
cgi.escape(a.handle.name())
)
self.log.info(msg)
Information(self.win, msg)
def quit(self, *args):
if self.session.conf.getboolean("Settings", "confirmations"):
ConfirmExit(self.win, self.shutdown())
else:
self.shutdown()
def shutdown(self):
elm.shutdown()
self.parent.quit()
class SessionStatus(Table):
log = logging.getLogger("epour.gui.session")
def __init__(self, parent, session):
Table.__init__(self, parent)
self.session = session
s = session.status()
self.padding = 5, 5
ses_pause_ic = self.ses_pause_ic = Icon(parent)
ses_pause_ic.size_hint_align = -1.0, -1.0
try:
if session.is_paused():
ses_pause_ic.standard = "player_pause"
else:
ses_pause_ic.standard = "player_play"
except RuntimeError:
self.log.debug("Setting session ic failed")
self.pack(ses_pause_ic, 1, 0, 1, 1)
ses_pause_ic.show()
title_l = Label(parent)
title_l.text = "<b>Session</b>"
self.pack(title_l, 0, 0, 1, 1)
title_l.show()
d_ic = Icon(parent)
try:
d_ic.standard = "down"
except RuntimeError:
self.log.debug("Setting d_ic failed")
d_ic.size_hint_align = -1.0, -1.0
self.pack(d_ic, 0, 2, 1, 1)
d_ic.show()
d_l = self.d_l = Label(parent)
d_l.text = "{}/s".format(intrepr(s.payload_download_rate))
self.pack(d_l, 1, 2, 1, 1)
d_l.show()
u_ic = Icon(self)
try:
u_ic.standard = "up"
except RuntimeError:
self.log.debug("Setting u_ic failed")
u_ic.size_hint_align = -1.0, -1.0
self.pack(u_ic, 0, 3, 1, 1)
u_ic.show()
u_l = self.u_l = Label(parent)
u_l.text = "{}/s".format(intrepr(s.payload_upload_rate))
self.pack(u_l, 1, 3, 1, 1)
u_l.show()
peer_t = Label(parent)
peer_t.text = "Peers"
self.pack(peer_t, 0, 4, 1, 1)
peer_t.show()
peer_l = self.peer_l = Label(parent)
peer_l.text = str(s.num_peers)
self.pack(peer_l, 1, 4, 1, 1)
peer_l.show()
self.show()
self.update_timer = Timer(1.0, self.update)
def update(self):
s = self.session.status()
self.d_l.text = "{}/s".format(intrepr(s.payload_download_rate))
self.u_l.text = "{}/s".format(intrepr(s.payload_upload_rate))
self.peer_l.text = str(s.num_peers)
if self.session.is_paused():
icon = "player_pause"
else:
icon = "player_play"
try:
self.ses_pause_ic.standard = icon
except RuntimeError:
self.log.debug("")
return True
class TorrentClass(GenlistItemClass):
state_str = ['Queued', 'Checking', 'Downloading metadata', \
'Downloading', 'Finished', 'Seeding', 'Allocating', \
'Checking resume data']
log = logging.getLogger("epour.gui.torrent_list")
def __init__(self, session, *args, **kwargs):
GenlistItemClass.__init__(self, *args, **kwargs)
self.session = session
def text_get(self, obj, part, item_data):
h = item_data
name = h.name()
if part == "elm.text":
return '%s' % (
name
)
elif part == "elm.text.sub":
s = h.status()
return "{:.0%} complete, ETA: {} " \
"(Down: {}/s Up: {}/s Peers: {} Queue: {})".format(
s.progress,
timedelta(seconds=self.get_eta(h)),
intrepr(s.download_payload_rate, precision=0),
intrepr(s.upload_payload_rate, precision=0),
s.num_peers,
h.queue_position(),
)
def content_get(self, obj, part, item_data):
if part == "elm.swallow.icon":
h = item_data
s = h.status()
ic = Icon(obj)
if h.is_paused():
try:
ic.standard = "player_pause"
except RuntimeError:
self.log.debug("Setting torrent ic failed")
elif h.is_seed():
try:
ic.standard = "up"
except RuntimeError:
self.log.debug("Setting torrent ic failed")
else:
try:
ic.standard = "down"
except RuntimeError:
self.log.debug("Setting torrent ic failed")
ic.tooltip_text_set(self.state_str[s.state])
ic.size_hint_aspect_set(EVAS_ASPECT_CONTROL_VERTICAL, 1, 1)
return ic
def get_eta(self, h):
s = h.status()
if False: #self.is_finished and self.options["stop_at_ratio"]:
# We're a seed, so calculate the time to the 'stop_share_ratio'
if not s.upload_payload_rate:
return 0
stop_ratio = self.session.settings().share_ratio_limit
return ((s.all_time_download * stop_ratio) - \
s.all_time_upload) / s.upload_payload_rate
left = s.total_wanted - s.total_wanted_done
if left <= 0 or s.download_payload_rate == 0:
return 0
try:
eta = left / s.download_payload_rate
except ZeroDivisionError:
eta = 0
return eta

View File

@ -1,63 +0,0 @@
#
# Epour - A bittorrent client using EFL and libtorrent
#
# Copyright 2012-2013 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 3 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.
#
try:
from efl.elementary.label import Label
from efl.elementary.notify import Notify
from efl.elementary.popup import Popup
from efl.elementary.button import Button
except ImportError:
from elementary import Label, Notify, Popup, Button
class Information(object):
def __init__(self, canvas, text):
n = Notify(canvas)
l = Label(canvas)
l.text = text
n.content = l
n.timeout = 3
n.show()
class Error(object):
def __init__(self, canvas, title, text):
n = Popup(canvas)
n.part_text_set("title,text", title)
n.text = text
b = Button(canvas)
b.text = "OK"
b.callback_clicked_add(lambda x: n.delete())
n.part_content_set("button1", b)
n.show()
class ConfirmExit(object):
def __init__(self, canvas, exit_func):
n = Popup(canvas)
n.part_text_set("title,text", "Confirm exit")
n.text = "Are you sure you wish to exit Epour?"
b = Button(canvas)
b.text = "Yes"
b.callback_clicked_add(lambda x: exit_func())
n.part_content_set("button1", b)
b = Button(canvas)
b.text = "No"
b.callback_clicked_add(lambda x: n.delete())
n.part_content_set("button2", b)
n.show()

View File

@ -1,11 +1,11 @@
#
# Epour - A bittorrent client using EFL and libtorrent
#
# Copyright 2012-2013 Kai Huuhko <kai.huuhko@gmail.com>
# Copyright 2012-2014 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
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
@ -22,11 +22,10 @@
import os
import cgi
import logging
log = logging.getLogger("epour")
log = logging.getLogger("epour.preferences")
import libtorrent as lt
from efl.elementary.icon import Icon
from efl.elementary.box import Box
from efl.elementary.label import Label
from efl.elementary.button import Button
@ -35,39 +34,42 @@ from efl.elementary.entry import Entry
from efl.elementary.check import Check
from efl.elementary.spinner import Spinner
from efl.elementary.hoversel import Hoversel
from efl.elementary.popup import Popup
from efl.elementary.fileselector import Fileselector
from efl.elementary.fileselector_button import FileselectorButton
from efl.elementary.scroller import Scroller, ELM_SCROLLER_POLICY_OFF, \
ELM_SCROLLER_POLICY_AUTO
from efl.elementary.scroller import Scroller, ELM_SCROLLER_POLICY_AUTO
from efl.elementary.separator import Separator
from efl.elementary.slider import Slider
from efl.elementary.actionslider import Actionslider, \
ELM_ACTIONSLIDER_LEFT, ELM_ACTIONSLIDER_CENTER, \
ELM_ACTIONSLIDER_RIGHT, ELM_ACTIONSLIDER_ALL
from efl.elementary.naviframe import Naviframe
from efl.elementary.table import Table
from efl.elementary.configuration import Configuration
from efl.evas import Rectangle
from efl.ecore import Timer
from efl.elementary.window import Window, ELM_WIN_BASIC
from efl.elementary.window import StandardWindow
from efl.elementary.background import Background
import Notify
from efl.evas import Rectangle, EVAS_HINT_EXPAND, EVAS_HINT_FILL
EXPAND_BOTH = 1.0, 1.0
EXPAND_HORIZ = 1.0, 0.0
FILL_BOTH = -1.0, -1.0
FILL_HORIZ = -1.0, 0.5
from Widgets import UnitSpinner, Error, Information
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
SCROLL_BOTH = ELM_SCROLLER_POLICY_AUTO, ELM_SCROLLER_POLICY_AUTO
class PreferencesDialog(Window):
class PreferencesDialog(StandardWindow):
""" Base class for all preferences dialogs """
def __init__(self, title):
def __init__(self, name, title):
elm_conf = Configuration()
scale = elm_conf.scale
Window.__init__(self, title, ELM_WIN_BASIC, title=title, autodel=True)
StandardWindow.__init__(
self, name, title, autodel=True)
self.size = scale * 480, scale * 320
@ -75,11 +77,10 @@ class PreferencesDialog(Window):
self.resize_object_add(bg)
bg.show()
# bt = Button(self, text="Close")
# bt.callback_clicked_add(lambda b: self.delete())
self.scroller = Scroller(self, policy=SCROLL_BOTH,
size_hint_weight=EXPAND_BOTH, size_hint_align=FILL_BOTH)
self.scroller = Scroller(
self, policy=SCROLL_BOTH,
size_hint_weight=EXPAND_BOTH, size_hint_align=FILL_BOTH
)
self.resize_object_add(self.scroller)
self.scroller.show()
@ -87,18 +88,16 @@ class PreferencesDialog(Window):
self.box.size_hint_weight = EXPAND_BOTH
self.scroller.content = self.box
self.show()
# def parent_resize_cb(self, parent):
# (pw, ph) = parent.size
# self.table.size_hint_min = pw * 0.7, ph * 0.7
class PreferencesGeneral(PreferencesDialog):
""" General preference dialog """
def __init__(self, parent, session):
self.session = session
conf = session.conf
PreferencesDialog.__init__(self, "General")
PreferencesDialog.__init__(
self, "epour_prefs_general", "Epour General Preferences")
limits = Limits(self, session)
ports = ListenPorts(self, session)
@ -113,32 +112,44 @@ class PreferencesGeneral(PreferencesDialog):
sep1.horizontal = True
chk1 = Check(self)
chk1.size_hint_align = 0.0, 0.0
chk1.size_hint_align = ALIGN_LEFT
chk1.text = "Delete original .torrent file when added"
chk1.state = conf.getboolean("Settings", "delete_original")
chk1.callback_changed_add(lambda x: conf.set("Settings",
"delete_original", str(bool(chk1.state))))
chk1.callback_changed_add(lambda x: conf.set(
"Settings", "delete_original", str(bool(chk1.state))
))
chk2 = Check(self)
chk2.size_hint_align = 0.0, 0.0
chk2.size_hint_align = ALIGN_LEFT
chk2.text = "Ask for confirmation on exit"
chk2.state = conf.getboolean("Settings", "confirmations")
chk2.callback_changed_add(lambda x: conf.set("Settings",
"confirmations", str(bool(chk2.state))))
chk2.state = conf.getboolean("Settings", "confirm_exit")
chk2.callback_changed_add(lambda x: conf.set(
"Settings", "confirm_exit", str(bool(chk2.state))
))
chk3 = Check(self)
chk3.size_hint_align = ALIGN_LEFT
chk3.text = "Torrents to be added from dbus open a dialog"
chk3.state = conf.getboolean("Settings", "dialog_add_dbus")
chk3.callback_changed_add(lambda x: conf.set(
"Settings", "confirmations", str(bool(chk3.state))
))
sep2 = Separator(self)
sep2.horizontal = True
for w in ports, limits, dlsel, pe, pad, sep1, chk1, chk2, sep2:
for w in ports, limits, dlsel, pe, pad, sep1, chk1, chk2, chk3, sep2:
w.show()
self.box.pack_end(w)
class DataStorageSelector(Frame):
def __init__(self, parent, conf):
Frame.__init__(self, parent)
self.size_hint_align = -1.0, 0.0
self.size_hint_weight = 1.0, 0.0
self.size_hint_align = FILL_HORIZ
self.size_hint_weight = EXPAND_HORIZ
self.text = "Data storage"
self.conf = conf
@ -148,12 +159,10 @@ class DataStorageSelector(Frame):
lbl = self.path_lbl = Label(parent)
lbl.text = conf.get("Settings", "storage_path")
self.dlsel = dlsel = FileselectorButton(self)
dlsel.size_hint_align = -1.0, 0.0
dlsel.inwin_mode = False
dlsel.folder_only = True
dlsel.expandable = False
dlsel.text = "Change path"
self.dlsel = dlsel = FsButton(
self, size_hint_align=FILL_HORIZ, inwin_mode=False,
text="Change path", folder_only=True, expandable=False
)
dlsel.path = conf.get("Settings", "storage_path")
dlsel.callback_file_chosen_add(self.save_dlpath)
@ -169,14 +178,17 @@ class DataStorageSelector(Frame):
return
if not os.path.exists(self.dlsel.path):
p = Notify.Error(self, "Invalid storage path",
Error(
self, "Invalid storage path",
"You have selected an invalid data storage path for torrents.")
return
self.path_lbl.text = path
self.conf.set("Settings", "storage_path", self.dlsel.path)
class ListenPorts(Frame):
def __init__(self, parent, session):
Frame.__init__(self, parent)
@ -185,16 +197,16 @@ class ListenPorts(Frame):
self.size_hint_align = FILL_HORIZ
self.text = "Listen port (range)"
port = session.listen_port()
#port = session.listen_port()
b = Box(parent)
b.size_hint_weight = EXPAND_HORIZ
lp = self.lp = RangeSpinners(
parent,
low = session.conf.getint("Settings", "listen_low"),
high = session.conf.getint("Settings", "listen_high"),
minim = 0, maxim = 65535)
low=session.conf.getint("Settings", "listen_low"),
high=session.conf.getint("Settings", "listen_high"),
minim=0, maxim=65535)
lp.show()
b.pack_end(lp)
@ -215,10 +227,14 @@ class ListenPorts(Frame):
self.session.conf.set("Settings", "listen_low", str(low))
self.session.conf.set("Settings", "listen_high", str(high))
class PreferencesProxy(PreferencesDialog):
""" Proxy preference dialog """
def __init__(self, parent, session):
PreferencesDialog.__init__(self, "Proxy")
PreferencesDialog.__init__(
self, "epour_prefs_proxy", "Epour Proxy Preferences")
proxies = [
["Proxy for torrent peer connections",
@ -236,6 +252,7 @@ class PreferencesProxy(PreferencesDialog):
pg.show()
self.box.pack_end(pg)
class ProxyGroup(Frame):
proxy_types = {
@ -248,21 +265,20 @@ class ProxyGroup(Frame):
}
def __init__(self, parent, title, rfunc, wfunc):
Frame.__init__(self, parent)
self.size_hint_weight = EXPAND_HORIZ
self.size_hint_align = FILL_HORIZ
self.text = title
Frame.__init__(
self, parent, size_hint_weight=EXPAND_HORIZ,
size_hint_align=FILL_HORIZ, text=title
)
t = Table(self, homogeneous=True, padding=(3,3))
t.size_hint_weight = EXPAND_HORIZ
t.size_hint_align = FILL_HORIZ
t = Table(
self, homogeneous=True, padding=(3, 3),
size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ
)
t.show()
l = Label(self, text="Proxy type")
l.size_hint_align = 0.0, 0.5
l = Label(self, text="Proxy type", size_hint_align=ALIGN_LEFT)
l.show()
ptype = Hoversel(parent)
ptype.size_hint_align = -1.0, 0.5
ptype = Hoversel(parent, size_hint_align=FILL_HORIZ)
ptype.text = rfunc().type.name
for n in self.proxy_types.iterkeys():
ptype.item_add(n, callback=lambda x, y, z=n: ptype.text_set(z))
@ -270,52 +286,42 @@ class ProxyGroup(Frame):
t.pack(l, 0, 0, 1, 1)
t.pack(ptype, 1, 0, 1, 1)
l = Label(self, text="Hostname")
l.size_hint_align = 0.0, 0.5
l = Label(self, text="Hostname", size_hint_align=ALIGN_LEFT)
l.show()
phost = Entry(parent)
phost.size_hint_weight = EXPAND_HORIZ
phost.size_hint_align = FILL_HORIZ
phost.single_line = True
phost.scrollable = True
phost = Entry(
parent, size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ,
single_line=True, scrollable=True
)
phost.entry = rfunc().hostname
phost.show()
t.pack(l, 0, 1, 1, 1)
t.pack(phost, 1, 1, 1, 1)
l = Label(self, text="Port")
l.size_hint_align = 0.0, 0.5
l = Label(self, text="Port", size_hint_align=ALIGN_LEFT)
l.show()
pport = Spinner(parent)
pport.size_hint_align = -1.0, 0.5
pport.min_max = 0, 65535
pport = Spinner(parent, size_hint_align=FILL_HORIZ, min_max=(0, 65535))
pport.value = rfunc().port
pport.show()
t.pack(l, 0, 2, 1, 1)
t.pack(pport, 1, 2, 1, 1)
l = Label(self, text="Username")
l.size_hint_align = 0.0, 0.5
l = Label(self, text="Username", size_hint_align=ALIGN_LEFT)
l.show()
puser = Entry(parent)
puser.size_hint_weight = EXPAND_HORIZ
puser.size_hint_align = FILL_HORIZ
puser.single_line = True
puser.scrollable = True
puser = Entry(
parent, size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ,
single_line=True, scrollable=True
)
puser.entry = rfunc().username
puser.show()
t.pack(l, 0, 3, 1, 1)
t.pack(puser, 1, 3, 1, 1)
l = Label(self, text="Password")
l.size_hint_align = 0.0, 0.5
l = Label(self, text="Password", size_hint_align=ALIGN_LEFT)
l.show()
ppass = Entry(parent)
ppass.size_hint_weight = EXPAND_HORIZ
ppass.size_hint_align = FILL_HORIZ
ppass.single_line = True
ppass.scrollable = True
ppass.password = True
ppass = Entry(
parent, size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ,
single_line=True, scrollable=True, password=True
)
ppass.entry = rfunc().password
ppass.show()
t.pack(l, 0, 4, 1, 1)
@ -342,45 +348,52 @@ class ProxyGroup(Frame):
wfunc(p)
class EncryptionSettings(Frame):
def __init__(self, parent, session):
self.session = session
Frame.__init__(self, parent)
self.size_hint_align = -1.0, 0.0
self.size_hint_align = FILL_HORIZ
self.text = "Encryption settings"
pes = self.pes = session.get_pe_settings()
b = Box(parent)
enc_values = lt.enc_policy.disabled, lt.enc_policy.enabled, lt.enc_policy.forced
enc_levels = lt.enc_level.plaintext, lt.enc_level.rc4, lt.enc_level.both
enc_values = lt.enc_policy.disabled, lt.enc_policy.enabled, \
lt.enc_policy.forced
enc_levels = lt.enc_level.plaintext, lt.enc_level.rc4, \
lt.enc_level.both
inc = self.inc = ActSWithLabel(parent,
"Incoming encryption", enc_values, pes.in_enc_policy)
inc = self.inc = ActSWithLabel(
parent, "Incoming encryption", enc_values, pes.in_enc_policy
)
b.pack_end(inc)
inc.show()
out = self.out = ActSWithLabel(parent,
"Outgoing encryption", enc_values, pes.out_enc_policy)
out = self.out = ActSWithLabel(
parent, "Outgoing encryption", enc_values, pes.out_enc_policy
)
b.pack_end(out)
out.show()
lvl = self.lvl = ActSWithLabel(parent,
"Allowed encryption level", enc_levels, pes.allowed_enc_level)
lvl = self.lvl = ActSWithLabel(
parent, "Allowed encryption level", enc_levels,
pes.allowed_enc_level
)
b.pack_end(lvl)
lvl.show()
prf = self.prf = Check(parent)
prf.style = "toggle"
prf.text = "Prefer RC4 ecryption"
prf.state = pes.prefer_rc4
prf = self.prf = Check(
parent, style="toggle", text="Prefer RC4 ecryption",
state=pes.prefer_rc4
)
b.pack_end(prf)
prf.show()
a_btn = Button(parent)
a_btn.text = "Apply"
a_btn = Button(parent, text="Apply")
a_btn.callback_clicked_add(self.apply)
b.pack_end(a_btn)
a_btn.show()
@ -389,17 +402,19 @@ class EncryptionSettings(Frame):
self.content = b
def apply(self, btn):
#TODO: Use callbacks to set these?
# TODO: Use callbacks to set these?
self.pes.in_enc_policy = self.inc.get_value()
self.pes.out_enc_policy = self.out.get_value()
#FIXME: Find out why this isn't saved to the session.
# FIXME: Find out why this isn't saved to the session.
self.pes.allowed_enc_level = self.lvl.get_value()
self.pes.prefer_rc4 = self.prf.state
self.session.set_pe_settings(self.pes)
class ActSWithLabel(Box):
def __init__(self, parent, label_text, values, initial_value):
Box.__init__(self, parent)
@ -410,16 +425,16 @@ class ActSWithLabel(Box):
}
self.horizontal = True
self.size_hint_align = -1.0, 0.0
self.size_hint_weight = 1.0, 0.0
self.size_hint_align = FILL_HORIZ
self.size_hint_weight = EXPAND_HORIZ
l = Label(parent)
l.text = label_text
l.show()
w = self.w = Actionslider(parent)
w.magnet_pos = ELM_ACTIONSLIDER_ALL
w.size_hint_align = -1.0, 0.0
w.size_hint_weight = 1.0, 0.0
w.size_hint_align = FILL_HORIZ
w.size_hint_weight = EXPAND_HORIZ
w.show()
parts = "left", "center", "right"
@ -434,49 +449,92 @@ class ActSWithLabel(Box):
def get_value(self):
return self.vd[self.w.indicator_pos]
class PreferencesSession(PreferencesDialog):
""" Session preference dialog """
def __init__(self, parent, session):
PreferencesDialog.__init__(self, "Session")
# TODO: Construct and populate this with an Idler
class PreferencesSession(PreferencesDialog):
""" Session preference dialog """
def __init__(self, parent, session):
PreferencesDialog.__init__(
self, "epour_prefs_session", "Epour Session Preferences")
self.session = session
widgets = {}
elm_conf = Configuration()
#elm_conf = Configuration()
#scale = elm_conf.scale
s = session.settings()
t = Table(self, padding=(5,5), homogeneous=True,
size_hint_align=FILL_BOTH)
t = Table(
self, padding=(5, 5), homogeneous=True, size_hint_align=FILL_BOTH
)
self.box.pack_end(t)
t.show()
i = 0
INT_MIN = -2147483648
INT_MAX = 2147483647
INT_MAX = 2147483647
scale = elm_conf.scale
import time
t1 = time.time()
for k in dir(s):
if k.startswith("__"): continue
if k.startswith("__"):
continue
try:
if k == "peer_tos" or k == "outgoing_ports":
# XXX: these don't have a C++ -> Python equivalent.
continue
a = getattr(s, k)
if isinstance(a, lt.disk_cache_algo_t):
w = Spinner(t)
w.size_hint_align = FILL_HORIZ
# XXX: lt-rb python bindings don't have all values.
w.min_max = 0, 2 #len(lt.disk_cache_algo_t.values.keys())
w.min_max = 0, len(lt.disk_cache_algo_t.values) - 1
for name, val in lt.disk_cache_algo_t.names.items():
w.special_value_add(val, name)
w.value = a
elif k == "suggest_mode":
w = Spinner(t)
w.size_hint_align = FILL_HORIZ
w.min_max = 0, len(lt.suggest_mode_t.values) - 1
for name, val in lt.suggest_mode_t.names.items():
w.special_value_add(val, name)
w.value = a
elif k == "choking_algorithm":
w = Spinner(t)
w.size_hint_align = FILL_HORIZ
w.min_max = 0, len(lt.choking_algorithm_t.values) - 1
for name, val in lt.choking_algorithm_t.names.items():
w.special_value_add(val, name)
w.value = a
elif k == "seed_choking_algorithm":
w = Spinner(t)
w.size_hint_align = FILL_HORIZ
w.min_max = 0, len(lt.seed_choking_algorithm_t.values) - 1
for name, val in lt.seed_choking_algorithm_t.names.items():
w.special_value_add(val, name)
w.value = a
elif k == "mixed_mode_algorithm":
w = Spinner(t)
w.size_hint_align = FILL_HORIZ
w.min_max = 0, len(lt.bandwidth_mixed_algo_t.values) - 1
for name, val in lt.bandwidth_mixed_algo_t.names.items():
w.special_value_add(val, name)
w.value = a
elif k == "disk_io_write_mode" or k == "disk_io_read_mode":
w = Spinner(t)
w.size_hint_align = FILL_HORIZ
w.min_max = 0, len(lt.io_buffer_mode_t.values) - 1
for name, val in lt.io_buffer_mode_t.names.items():
w.special_value_add(val, name)
w.value = a
elif isinstance(a, bool):
w = Check(t)
w.size_hint_align = 1.0, 0.0
w.style = "toggle"
#w.style = "toggle"
w.size_hint_align = FILL_HORIZ
w.state = a
elif isinstance(a, int):
w = Spinner(t)
@ -493,10 +551,6 @@ class PreferencesSession(PreferencesDialog):
else:
w.min_max = 0.0, 20.0
w.value = a
elif k == "peer_tos":
# XXX: This is an int pair in libtorrent,
# which doesn't have a python equivalent.
continue
elif k == "user_agent":
w = Entry(t)
w.size_hint_align = 1.0, 0.0
@ -505,15 +559,16 @@ class PreferencesSession(PreferencesDialog):
w.editable = False
w.entry = cgi.escape(a)
else:
log.debug("Using string for %s type %s", k, type(a))
w = Entry(t)
w.part_text_set("guide", "Enter here")
w.size_hint_align = FILL_HORIZ
w.size_hint_weight = EXPAND_HORIZ
w.single_line = True
w.entry = cgi.escape(a)
w.part_text_set("guide", "Enter here")
l = Label(t)
l.text = k.replace("_", " ").capitalize()
l.size_hint_align = 0.0, 0.0
l.size_hint_align = ALIGN_LEFT
l.size_hint_weight = EXPAND_HORIZ
l.show()
t.pack(l, 0, i, 1, 1)
@ -522,8 +577,11 @@ class PreferencesSession(PreferencesDialog):
w.show()
widgets[k] = w
i += 1
except TypeError:
pass #print("Error {}".format(k))
except TypeError as e:
log.debug("Error in {}: {}".format(k, e))
t2 = time.time()
log.debug("Session settings time: %f", t2-t1)
save_btn = Button(self)
save_btn.text = "Apply session settings"
@ -552,69 +610,11 @@ class PreferencesSession(PreferencesDialog):
setattr(s, k, v)
session.set_settings(s)
Notify.Information(self, "Session settings saved.")
Information(self, "Session settings saved.")
class UnitSpinner(Box):
def __init__(self, parent, base, units):
self.base = base # the divisor/multiplier for units
self.units = units # a list of strings with the base unit description at index 0
super(UnitSpinner, self).__init__(parent)
self.horizontal = True
self.save_timer = None
s = self.spinner = Spinner(parent)
s.size_hint_weight = EXPAND_HORIZ
s.size_hint_align = FILL_HORIZ
s.min_max = 0, base
s.show()
self.pack_end(s)
hs = self.hoversel = Hoversel(parent)
for u in units:
hs.item_add(u, None, 0, lambda x=hs, y=None, u=u: x.text_set(u))
hs.show()
self.pack_end(hs)
def callback_changed_add(self, func, delay=None):
self.spinner.callback_changed_add(self.changed_cb, func, delay)
self.hoversel.callback_selected_add(self.changed_cb, func, delay)
def changed_cb(self, widget, *args):
func, delay = args[-2:]
if delay:
if self.save_timer is not None:
self.save_timer.delete()
self.save_timer = Timer(2.0, self.save_cb, func)
else:
self.save_cb(func)
def save_cb(self, func):
v = int(self.get_value())
log.debug("Saving value {}.".format(v))
func(v)
return False
def get_value(self):
return self.spinner.value * ( self.base ** self.units.index(self.hoversel.text) )
def set_value(self, v):
i = 0
while v // self.base > 0:
i += 1
v = float(v) / float(self.base)
if i > len(self.units):
i = len(self.units) - 1
self.spinner.value = v
self.hoversel.text = self.units[i]
class RangeSpinners(Box):
def __init__(self, parent, low, high, minim, maxim):
Box.__init__(self, parent)
@ -638,7 +638,9 @@ class RangeSpinners(Box):
self.pack_end(h)
h.show()
class Limits(Frame):
def __init__(self, parent, session):
Frame.__init__(self, parent)
@ -646,14 +648,22 @@ class Limits(Frame):
self.size_hint_align = FILL_HORIZ
base = 1024
units = ( "bytes/s", "KiB/s", "MiB/s", "GiB/s", "TiB/s" )
units = "bytes/s", "KiB/s", "MiB/s", "GiB/s", "TiB/s"
t = Table(parent)
for r, values in enumerate((
("Upload limit", session.upload_rate_limit, session.set_upload_rate_limit),
("Download limit", session.download_rate_limit, session.set_download_rate_limit),
("Upload limit for local connections", session.local_upload_rate_limit, session.set_local_upload_rate_limit),
("Download limit for local connections", session.local_download_rate_limit, session.set_local_download_rate_limit),
("Upload limit",
session.upload_rate_limit,
session.set_upload_rate_limit),
("Download limit",
session.download_rate_limit,
session.set_download_rate_limit),
("Upload limit for local connections",
session.local_upload_rate_limit,
session.set_local_upload_rate_limit),
("Download limit for local connections",
session.local_download_rate_limit,
session.set_local_download_rate_limit),
)):
title, rfunc, wfunc = values
@ -673,6 +683,12 @@ class Limits(Frame):
self.content = t
class FsButton(Fileselector, FileselectorButton):
def __init__(self, parent, *args, **kwargs):
FileselectorButton.__init__(self, parent, *args, **kwargs)
# TODO:
# max uploads?, max conns?, max half open conns?

View File

@ -1,7 +1,7 @@
#
# Epour - A bittorrent client using EFL and libtorrent
#
# Copyright 2012-2013 Kai Huuhko <kai.huuhko@gmail.com>
# Copyright 2012-2014 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
@ -27,108 +27,78 @@ log = logging.getLogger("epour")
import libtorrent as lt
try:
from efl import ecore
from efl.elementary.genlist import Genlist, GenlistItemClass
from efl.elementary.innerwindow import InnerWindow
from efl.elementary.button import Button
from efl.elementary.box import Box
from efl.elementary.hoversel import Hoversel
from efl.elementary.check import Check
from efl.elementary.label import Label, ELM_WRAP_CHAR, ELM_WRAP_WORD
from efl.elementary.entry import Entry
from efl.elementary.naviframe import Naviframe
from efl.elementary.frame import Frame
from efl.elementary.object import ELM_SEL_FORMAT_TEXT, ELM_SEL_TYPE_CLIPBOARD
except ImportError:
import ecore
from elementary import Genlist, GenlistItemClass, InnerWindow, Button, \
Box, Hoversel, Check, Label, ELM_WRAP_CHAR, ELM_WRAP_WORD, Entry, \
Naviframe, Frame
from efl import ecore
from efl.evas import EVAS_HINT_EXPAND, EVAS_HINT_FILL
from efl.elementary.genlist import Genlist, GenlistItemClass
from efl.elementary.window import StandardWindow
from efl.elementary.innerwindow import InnerWindow
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 intrepr import intrepr
from Notify import Information
from Widgets import Information
class TorrentInfo(InnerWindow):
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
class TorrentProps(InnerWindow):
def __init__(self, parent, h):
if not h.is_valid():
Information(parent.win, "Invalid torrent handle.")
return
if not h.has_metadata():
Information(parent.win, "Torrent contains no metadata.")
return
i = h.get_torrent_info()
InnerWindow.__init__(self, parent.win)
InnerWindow.__init__(self, parent)
box = Box(self)
box.size_hint_align = -1.0, -1.0
box.size_hint_weight = 1.0, 1.0
box.size_hint_align = FILL_BOTH
box.size_hint_weight = EXPAND_BOTH
tname = Label(self)
tname.size_hint_align = -1.0, 0.5
tname.size_hint_align = FILL_HORIZ
tname.line_wrap = ELM_WRAP_CHAR
tname.ellipsis = True
tname.text = "{}".format(cgi.escape(i.name()))
tname.text = "{}".format(cgi.escape(h.name()))
tname.scale = 2.0
tname.show()
box.pack_end(tname)
for func in i.comment, i.creation_date, i.creator:
try:
w = func()
except Exception as e:
log.debug(e)
else:
if w:
f = Frame(self)
f.size_hint_align = -1.0, 0.0
f.text = func.__name__.replace("_", " ").capitalize()
l = Label(self)
l.ellipsis = True
l.text = cgi.escape(str(w))
l.show()
f.content = l
f.show()
box.pack_end(f)
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 trackerless network<br> \
(the kademlia DHT)."
)
tpriv.disabled = True
tpriv.state = i.priv()
if h.has_metadata():
ti = TorrentInfo(self, h, size_hint_align=FILL_HORIZ)
box.pack_end(ti)
ti.show()
magnet_uri = lt.make_magnet_uri(h)
f = Frame(self)
f.size_hint_align = -1.0, 0.0
f.size_hint_align = FILL_HORIZ
f.text = "Magnet URI"
me_box = Box(self)
me_box = Box(f)
me_box.horizontal = True
me = Entry(self)
me.size_hint_align = -1.0, 0.0
me.size_hint_weight = 1.0, 0.0
#me.editable = False
me = Entry(me_box)
me.size_hint_align = FILL_HORIZ
me.size_hint_weight = EXPAND_HORIZ
me.editable = False
me.entry = magnet_uri
me_box.pack_end(me)
me.show()
me_btn = Button(self)
me_btn = Button(me_box)
me_btn.text = "Copy"
if hasattr(me, "cnp_selection_set"):
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.callback_clicked_add(
lambda x: me.top_widget.cnp_selection_set(
ELM_SEL_TYPE_CLIPBOARD, ELM_SEL_FORMAT_TEXT, me.text
)
else:
import pyperclip
me_btn.callback_clicked_add(lambda x: pyperclip.copy(magnet_uri))
)
me_btn.show()
me_box.pack_end(me_btn)
me_box.show()
@ -136,38 +106,29 @@ class TorrentInfo(InnerWindow):
f.show()
box.pack_end(f)
fl_btn = Button(self)
fl_btn.text = "Files ->"
fl_btn.callback_clicked_add(self.file_list_cb, h)
xbtn = Button(self)
xbtn = Button(box)
xbtn.text_set("Close")
xbtn.callback_clicked_add(lambda x: self.delete())
for w in tpriv, fl_btn, xbtn:
w.show()
box.pack_end(w)
box.pack_end(xbtn)
xbtn.show()
box.show()
nf = self.nf = Naviframe(self)
nf.item_simple_push(box)
self.content_set(nf)
self.content = box
self.activate()
def file_list_cb(self, btn, h):
self.nf.item_simple_push(TorrentFiles(self.nf, h))
class TorrentFiles(Box):
def __init__(self, parent, h):
Box.__init__(self, parent)
class TorrentFiles(StandardWindow):
def __init__(self, h):
StandardWindow.__init__(self, "epour.files", "Files", size=(600, 400))
self.callback_delete_request_add(lambda x: self.delete())
filelist = Genlist(self)
filelist.size_hint_align = -1.0, -1.0
filelist.size_hint_weight = 1.0, 1.0
box = Box(self, size_hint_weight=EXPAND_BOTH)
self.resize_object_add(box)
filelist = Genlist(box)
filelist.size_hint_align = FILL_BOTH
filelist.size_hint_weight = EXPAND_BOTH
self.populate(filelist, h)
@ -180,24 +141,25 @@ class TorrentFiles(Box):
sel_all.show()
sel_none = Button(self)
sel_none.text ="Select none"
sel_none.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()
# xbtn = Button(self)
# xbtn.text = "Close"
# xbtn.callback_clicked_add(lambda x: parent.item_pop())
# xbtn.show()
btn_box = Box(self)
btn_box.horizontal = True
btn_box.pack_end(sel_all)
btn_box.pack_end(sel_none)
btn_box.pack_end(xbtn)
#btn_box.pack_end(xbtn)
btn_box.show()
self.pack_end(filelist)
self.pack_end(btn_box)
box.pack_end(filelist)
box.pack_end(btn_box)
box.show()
self.show()
def select_all_cb(self, btn, filelist, h, all_selected=True):
@ -232,6 +194,7 @@ class TorrentFiles(Box):
else:
os.startfile(path)
class FileSelectionClass(GenlistItemClass):
def text_get(self, obj, part, data):
file_entry, progress, n, h = data
@ -256,3 +219,58 @@ class FileSelectionClass(GenlistItemClass):
priorities[n] = int(ck.state)
h.prioritize_files(priorities)
class TorrentInfo(Frame):
def __init__(self, parent, h, *args, **kwargs):
Frame.__init__(self, parent, *args, **kwargs)
self.text = "Torrent info"
info = h.get_torrent_info()
box = Box(self, size_hint_weight=EXPAND_HORIZ)
self.content = box
box.show()
table = Table(box, 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 = cgi.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(box, size_hint_align=ALIGN_LEFT)
fl_btn.text = "Files ->"
fl_btn.callback_clicked_add(lambda x: TorrentFiles(h))
box.pack_end(table)
box.pack_end(tpriv)
box.pack_end(fl_btn)
table.show()
tpriv.show()
fl_btn.show()

View File

@ -0,0 +1,264 @@
#
# Epour - A bittorrent client using EFL and libtorrent
#
# Copyright 2012-2014 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 3 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.
#
import os
import logging
from libtorrent import add_torrent_params_flags_t
from efl.evas import EVAS_HINT_EXPAND, EVAS_HINT_FILL
from efl import elementary as elm
from efl.elementary.window import StandardWindow
from efl.elementary.button import Button
from efl.elementary.box import Box
from efl.elementary.frame import Frame
from efl.elementary.fileselector import Fileselector
from efl.elementary.fileselector_entry import FileselectorEntry
from efl.elementary.entry import Entry, markup_to_utf8, utf8_to_markup
from efl.elementary.check import Check
from efl.elementary.scroller import Scroller
from efl.elementary.spinner import Spinner
# from efl.elementary.object import ELM_SEL_TYPE_CLIPBOARD, \
# ELM_SEL_FORMAT_TEXT
from Widgets import 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
from efl.elementary.configuration import Configuration
elm_conf = Configuration()
scale = elm_conf.scale
log = logging.getLogger("epour.gui")
class TorrentSelector(StandardWindow):
def __init__(self, parent, session, t_uri=None):
StandardWindow.__init__(
self, "epour_add_torrent", "Add Torrent",
size=(scale * 400, scale * 400), autodel=True
)
self.add_dict = {}
scrol = Scroller(
self, size_hint_weight=EXPAND_BOTH, size_hint_align=FILL_BOTH
)
self.resize_object_add(scrol)
box = Box(
scrol, size_hint_weight=EXPAND_BOTH, size_hint_align=FILL_BOTH
)
scrol.content = box
sf = Frame(
box, size_hint_weight=EXPAND_BOTH, size_hint_align=FILL_BOTH,
text=u"Select torrent file \u25BC", autocollapse=True
)
t_sel = Fileselector(
sf, size_hint_weight=EXPAND_BOTH, size_hint_align=FILL_BOTH,
expandable=False, buttons_ok_cancel=False, is_save=False
)
if elm.need_efreet():
t_sel.mime_types_filter_append(
["application/x-bittorrent"], "Torrent files")
t_sel.mime_types_filter_append(
["*"], "All files")
if t_uri and os.path.isfile(t_uri):
t_sel.selected = t_uri
t_uri = None
else:
t_sel.path = os.path.expanduser("~")
sf.content = t_sel
box.pack_end(sf)
sf.show()
t_sel.show()
m_entry = Entry(
box, size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ,
single_line=True, scrollable=True
)
m_entry.part_text_set("guide", "or enter magnet URI / info hash")
if t_uri:
m_entry.entry = utf8_to_markup(t_uri)
box.pack_end(m_entry)
m_entry.show()
options = Frame(
self, size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ,
text=u"Options \u25BA", autocollapse=True, collapse=True
)
box.pack_end(options)
options.show()
def toggler(obj1, obj2):
obj2.collapse_go(not obj1.collapse)
if obj1.collapse:
obj2.text = obj2.text.replace(u"\u25BA", u"\u25BC")
obj1.text = obj1.text.replace(u"\u25BC", u"\u25BA")
else:
obj2.text = obj2.text.replace(u"\u25BC", u"\u25BA")
obj1.text = obj1.text.replace(u"\u25BA", u"\u25BC")
sf.callback_clicked_add(toggler, options)
options.callback_clicked_add(toggler, sf)
opt_box = Box(
options, size_hint_weight=EXPAND_BOTH, size_hint_align=FILL_BOTH
)
options.content = opt_box
def entry_cb(e, key):
self.add_dict[key] = e.entry
for n in "name", "trackerid":
e = Entry(opt_box, size_hint_align=FILL_HORIZ)
e.part_text_set("guide", n)
e.callback_changed_user_add(entry_cb, n)
opt_box.pack_end(e)
e.show()
def fs_cb(fs, key):
self.add_dict[key] = fs.selected
save_path = FsEntry(
opt_box, size_hint_align=FILL_HORIZ, text="Save path",
folder_only=True, expandable=False
)
save_path.path = session.conf.get("Settings", "storage_path")
save_path.callback_changed_add(fs_cb, "save_path")
opt_box.pack_end(save_path)
save_path.show()
self.add_dict["flags"] = \
add_torrent_params_flags_t.flag_apply_ip_filter + \
add_torrent_params_flags_t.flag_update_subscribe + \
add_torrent_params_flags_t.flag_auto_managed
def option_flag_cb(c, flag):
self.add_dict["flags"] = self.add_dict["flags"] ^ flag
for name, flag in add_torrent_params_flags_t.names.items():
if name == "flag_auto_managed" or name == "flag_update_subscribe":
continue
c = Check(
opt_box, size_hint_align=(0.0, 0.5),
text=" ".join(name.split("_")[1:]).capitalize(),
state=self.add_dict["flags"] & int(flag)
)
c.callback_changed_add(option_flag_cb, int(flag))
opt_box.pack_end(c)
c.show()
INT_MAX = 2147483647
def spin_cb(s, key):
self.add_dict[key] = s.value
for n in (
"max_uploads", "max_connections"
):
s = Spinner(
opt_box, size_hint_align=FILL_HORIZ,
text=n.replace("_", " ").capitalize(),
min_max=(0, INT_MAX), label_format=n + ": %0.f"
)
s.callback_changed_add(spin_cb, n)
opt_box.pack_end(s)
s.show()
for n in (
"upload_limit", "download_limit"
):
units = "bytes/s", "KiB/s", "MiB/s", "GiB/s", "TiB/s"
s = UnitSpinner(opt_box, 1024, units)
s.size_hint_weight = EXPAND_HORIZ
s.size_hint_align = FILL_HORIZ
s.set_value(0)
s.spinner.label_format = n + ": %0.f"
opt_box.pack_end(s)
s.show()
# TODO:
# list(strings) trackers (list(entry))
# list(tuple(str host, int port), ...) dht_nodes
# storage_mode_t storage_mode
# list(int) file_priorities (list(spinner))
opt_box.show()
bbox = Box(
box, size_hint_weight=EXPAND_HORIZ,
size_hint_align=FILL_HORIZ, horizontal=True
)
ok_btn = Button(bbox, text="Ok", size_hint_align=(1.0, 0.5))
bbox.pack_end(ok_btn)
ok_btn.show()
cancel_btn = Button(bbox, text="Cancel", size_hint_align=(1.0, 0.5))
bbox.pack_end(cancel_btn)
cancel_btn.show()
bbox.show()
box.pack_end(bbox)
box.show()
scrol.show()
def add_torrent_cb(btn, t_sel, m_entry, session, add_dict):
f = t_sel.selected
m = m_entry.entry
if f and os.path.isfile(f):
log.debug("Adding file: %s", f)
session.add_torrent_from_file(add_dict, f)
self.delete()
elif m:
m = markup_to_utf8(m)
if m.startswith("magnet:"):
log.debug("Adding torrent from magnet link: %s", m)
session.add_torrent_from_magnet(add_dict, m)
else:
log.debug("Adding torrent from info hash: %s", m)
session.add_torrent_from_hash(add_dict, m)
self.delete()
else:
return
ok_btn.callback_clicked_add(
add_torrent_cb, t_sel, m_entry, session, self.add_dict
)
cancel_btn.callback_clicked_add(lambda x: self.delete())
self.show()
class FsEntry(Fileselector, FileselectorEntry):
def __init__(self, parent, **kwargs):
FileselectorEntry.__init__(self, parent, **kwargs)

114
epour/gui/Widgets.py Normal file
View File

@ -0,0 +1,114 @@
from efl.ecore import Timer
from efl.elementary.box import Box
from efl.elementary.spinner import Spinner
from efl.elementary.hoversel import Hoversel
from efl.elementary.label import Label
from efl.elementary.notify import Notify
from efl.elementary.popup import Popup
from efl.elementary.button import Button
EXPAND_BOTH = 1.0, 1.0
EXPAND_HORIZ = 1.0, 0.0
FILL_BOTH = -1.0, -1.0
FILL_HORIZ = -1.0, 0.5
class UnitSpinner(Box):
def __init__(self, parent, base, units):
self.base = base # the divisor/multiplier for units
self.units = units # a list of strings with the base unit description
# at index 0
super(UnitSpinner, self).__init__(parent)
self.horizontal = True
self.save_timer = None
s = self.spinner = Spinner(parent)
s.size_hint_weight = EXPAND_HORIZ
s.size_hint_align = FILL_HORIZ
s.min_max = 0, base
s.show()
self.pack_end(s)
hs = self.hoversel = Hoversel(parent)
for u in units:
hs.item_add(u, None, 0, lambda x=hs, y=None, u=u: x.text_set(u))
hs.show()
self.pack_end(hs)
def callback_changed_add(self, func, delay=None):
self.spinner.callback_changed_add(self.changed_cb, func, delay)
self.hoversel.callback_selected_add(self.changed_cb, func, delay)
def changed_cb(self, widget, *args):
func, delay = args[-2:]
if delay:
if self.save_timer is not None:
self.save_timer.delete()
self.save_timer = Timer(2.0, self.save_cb, func)
else:
self.save_cb(func)
def save_cb(self, func):
v = int(self.get_value())
func(v)
return False
def get_value(self):
return self.spinner.value * (
self.base ** self.units.index(self.hoversel.text)
)
def set_value(self, v):
i = 0
while v // self.base > 0:
i += 1
v = float(v) / float(self.base)
if i > len(self.units):
i = len(self.units) - 1
self.spinner.value = v
self.hoversel.text = self.units[i]
class Information(object):
def __init__(self, canvas, text):
n = Notify(canvas)
l = Label(canvas)
l.text = text
n.content = l
n.timeout = 3
n.show()
class Error(object):
def __init__(self, canvas, title, text):
n = Popup(canvas)
n.part_text_set("title,text", title)
n.text = text
b = Button(canvas)
b.text = "OK"
b.callback_clicked_add(lambda x: n.delete())
n.part_content_set("button1", b)
n.show()
class ConfirmExit(object):
def __init__(self, canvas, exit_func):
n = Popup(canvas)
n.part_text_set("title,text", "Confirm exit")
n.text = "Are you sure you wish to exit Epour?"
b = Button(canvas)
b.text = "Yes"
b.callback_clicked_add(lambda x: exit_func())
n.part_content_set("button1", b)
b = Button(canvas)
b.text = "No"
b.callback_clicked_add(lambda x: n.delete())
n.part_content_set("button2", b)
n.show()

View File

@ -1,8 +1,520 @@
from Main import MainInterface
#
# Epour - A bittorrent client using EFL and libtorrent
#
# Copyright 2012-2014 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 3 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.
#
try:
from efl import elementary
except ImportError:
import elementary
import cgi
import logging
from datetime import timedelta
elementary.init()
from efl.evas import EVAS_HINT_EXPAND, EVAS_HINT_FILL, \
EVAS_ASPECT_CONTROL_VERTICAL, Rectangle
from efl.ecore import Timer
from efl import elementary as elm
elm.init()
from efl.elementary.genlist import Genlist, GenlistItemClass, \
ELM_GENLIST_ITEM_FIELD_TEXT, ELM_GENLIST_ITEM_FIELD_CONTENT, \
ELM_OBJECT_SELECT_MODE_NONE, ELM_LIST_COMPRESS
from efl.elementary.window import StandardWindow
from efl.elementary.icon import Icon
from efl.elementary.box import Box
from efl.elementary.label import Label
from efl.elementary.panel import Panel, ELM_PANEL_ORIENT_BOTTOM
from efl.elementary.table import Table
from efl.elementary.menu import Menu
from efl.elementary.configuration import Configuration
from efl.elementary.toolbar import Toolbar, ELM_TOOLBAR_SHRINK_NONE
from Widgets import ConfirmExit, Error, Information
from intrepr import intrepr
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
elm_conf = Configuration()
scale = elm_conf.scale
log = logging.getLogger("epour.gui")
class MainInterface(object):
def __init__(self, parent, session):
self.parent = parent
self.session = session
self.torrentitems = {}
win = self.win = StandardWindow(
"epour", "Epour", size=(480 * scale, 400 * scale),
screen_constrain=True
)
win.callback_delete_request_add(lambda x: self.quit())
mbox = Box(win, size_hint_weight=EXPAND_BOTH)
win.resize_object_add(mbox)
mbox.show()
# --- TOOLBAR ---
tb = Toolbar(
win, size_hint_align=FILL_HORIZ, homogeneous=False,
shrink_mode=ELM_TOOLBAR_SHRINK_NONE,
select_mode=ELM_OBJECT_SELECT_MODE_NONE
)
tb.menu_parent = win
item = tb.item_append(
"document-new", "Add torrent",
lambda x, y: self.add_torrent()
)
def pause_session(it):
self.session.pause()
it.state_set(it.state_next())
def resume_session(it):
session.resume()
del it.state
item = tb.item_append(
"media-playback-pause", "Pause Session",
lambda tb, it: pause_session(it)
)
item.state_add(
"media-playback-start", "Resume Session",
lambda tb, it: resume_session(it)
)
def prefs_general_cb():
from Preferences import PreferencesGeneral
PreferencesGeneral(self, self.session).show()
def prefs_proxy_cb():
from Preferences import PreferencesProxy
PreferencesProxy(self, self.session).show()
def prefs_session_cb():
from Preferences import PreferencesSession
PreferencesSession(self, self.session).show()
item = tb.item_append("preferences-system", "Preferences")
item.menu = True
item.menu.item_add(
None, "General", "preferences-system",
lambda o, i: prefs_general_cb()
)
item.menu.item_add(
None, "Proxy", "preferences-system",
lambda o, i: prefs_proxy_cb()
)
item.menu.item_add(
None, "Session", "preferences-system",
lambda o, i: prefs_session_cb()
)
item = tb.item_append("application-exit", "Exit",
lambda tb, it: elm.exit())
mbox.pack_start(tb)
tb.show()
# --- TORRENT LIST ---
self.tlist = tlist = Genlist(
mbox, select_mode=ELM_OBJECT_SELECT_MODE_NONE,
mode=ELM_LIST_COMPRESS, size_hint_weight=EXPAND_BOTH,
size_hint_align=FILL_BOTH, homogeneous=True
)
def item_activated_cb(gl, item):
h = item.data
ItemMenu(self.win, item, self.session, h)
tlist.callback_activated_add(item_activated_cb)
tlist.show()
mbox.pack_end(tlist)
topbox = Box(win, size_hint_weight=EXPAND_BOTH, horizontal=False)
pad1 = Rectangle(win.evas, size_hint_weight=EXPAND_BOTH)
p = Panel(
topbox, size_hint_weight=EXPAND_BOTH, size_hint_align=FILL_BOTH,
color=(200, 200, 200, 200), orient=ELM_PANEL_ORIENT_BOTTOM
)
p.content = SessionStatus(win, session)
p.hidden = True
p.show()
win.resize_object_add(topbox)
topbox.pack_end(pad1)
topbox.pack_end(p)
topbox.stack_above(mbox)
topbox.show()
def torrent_added_cb(a):
h = a.handle
self.add_torrent_item(h)
def torrent_removed_cb(a):
self.remove_torrent_item(str(a.info_hash))
def torrent_update_cb(a):
self.remove_torrent_item(str(a.old_ih))
new_h = self.session.find_torrent(str(a.new_ih))
self.add_torrent_item(new_h)
session.alert_manager.callback_add(
"torrent_added_alert", torrent_added_cb)
session.alert_manager.callback_add(
"torrent_removed_alert", torrent_removed_cb)
session.alert_manager.callback_add(
"torrent_update_alert", torrent_update_cb)
def torrent_paused_cb(a):
self.update_icon(a.handle)
for a_name in "torrent_paused_alert", "torrent_resumed_alert":
session.alert_manager.callback_add(a_name, torrent_paused_cb)
def state_changed_cb(a):
h = a.handle
if not h.is_valid():
log.debug("State changed for invalid handle.")
return
ihash = str(h.info_hash())
if not ihash in self.torrentitems:
log.debug("%s not in list", str(ihash))
return
self.update_icon(h)
session.alert_manager.callback_add(
"state_changed_alert", state_changed_cb)
def torrent_finished_cb(a):
msg = "Torrent {} has finished downloading.".format(
cgi.escape(a.handle.name())
)
log.info(msg)
Information(self.win, msg)
Timer(15.0, lambda: session.alert_manager.callback_add(
"torrent_finished_alert", torrent_finished_cb))
def add_torrent(self, t_uri=None):
from TorrentSelector import TorrentSelector
TorrentSelector(self.win, self.session, t_uri)
def run(self):
self.win.show()
self.timer = Timer(1.0, self.update)
elm.run()
elm.shutdown()
def update(self):
for v in self.tlist.realized_items_get():
v.fields_update("*", ELM_GENLIST_ITEM_FIELD_TEXT)
return True
def update_icon(self, h):
if not h.is_valid():
return
ihash = str(h.info_hash())
if not ihash in self.torrentitems:
return
self.torrentitems[ihash].fields_update(
"elm.swallow.icon", ELM_GENLIST_ITEM_FIELD_CONTENT
)
def add_torrent_item(self, h):
ihash = str(h.info_hash())
itc = TorrentClass(self.session, "double_label")
item = self.tlist.item_append(itc, h)
self.torrentitems[ihash] = item
def remove_torrent_item(self, info_hash):
it = self.torrentitems.pop(info_hash, None)
if it is not None:
it.delete()
def show_error(self, title, text):
Error(self.win, title, text)
def quit(self, *args):
if self.session.conf.getboolean("Settings", "confirm_exit"):
ConfirmExit(self.win, elm.exit)
else:
elm.exit()
class SessionStatus(Table):
log = logging.getLogger("epour.gui.session")
def __init__(self, parent, session):
Table.__init__(self, parent)
self.session = session
s = session.status()
self.homogeneous = True
self.padding = 5, 2
ses_pause_ic = self.ses_pause_ic = Icon(parent)
ses_pause_ic.size_hint_align = -1.0, -1.0
try:
if session.is_paused():
ses_pause_ic.standard = "player_pause"
else:
ses_pause_ic.standard = "player_play"
except RuntimeError:
log.debug("Setting session ic failed")
self.pack(ses_pause_ic, 1, 0, 1, 1)
ses_pause_ic.show()
title_l = Label(parent, text="<b>Session</b>")
self.pack(title_l, 0, 0, 1, 1)
title_l.show()
d_ic = Icon(parent, size_hint_align=FILL_BOTH)
try:
d_ic.standard = "down"
except RuntimeError:
log.debug("Setting d_ic failed")
self.pack(d_ic, 0, 1, 1, 1)
d_ic.show()
d_l = self.d_l = Label(parent)
d_l.text = "{}/s".format(intrepr(s.payload_download_rate))
self.pack(d_l, 1, 1, 1, 1)
d_l.show()
u_ic = Icon(self, size_hint_align=FILL_BOTH)
try:
u_ic.standard = "up"
except RuntimeError:
log.debug("Setting u_ic failed")
self.pack(u_ic, 0, 2, 1, 1)
u_ic.show()
u_l = self.u_l = Label(parent)
u_l.text = "{}/s".format(intrepr(s.payload_upload_rate))
self.pack(u_l, 1, 2, 1, 1)
u_l.show()
peer_t = Label(parent, text="Peers")
self.pack(peer_t, 0, 3, 1, 1)
peer_t.show()
peer_l = self.peer_l = Label(parent, text=str(s.num_peers))
self.pack(peer_l, 1, 3, 1, 1)
peer_l.show()
listen_t = Label(parent, text="Listening")
self.pack(listen_t, 0, 4, 1, 1)
listen_t.show()
listen_l = self.listen_l = Label(
parent, text=str(session.is_listening()))
self.pack(listen_l, 1, 4, 1, 1)
listen_l.show()
self.show()
self.update_timer = Timer(1.0, self.update)
def update(self):
s = self.session.status()
self.d_l.text = "{}/s".format(intrepr(s.payload_download_rate))
self.u_l.text = "{}/s".format(intrepr(s.payload_upload_rate))
self.peer_l.text = str(s.num_peers)
self.listen_l.text = str(self.session.is_listening())
if self.session.is_paused():
icon = "player_pause"
else:
icon = "player_play"
try:
self.ses_pause_ic.standard = icon
except Exception:
log.debug("")
return True
class TorrentClass(GenlistItemClass):
state_str = [
'Queued', 'Checking', 'Downloading metadata', 'Downloading',
'Finished', 'Seeding', 'Allocating', 'Checking resume data'
]
log = logging.getLogger("epour.gui.torrent_list")
def __init__(self, session, *args, **kwargs):
GenlistItemClass.__init__(self, *args, **kwargs)
self.session = session
def text_get(self, obj, part, item_data):
h = item_data
if part == "elm.text":
name = h.name()
return '%s' % (
name
)
elif part == "elm.text.sub":
if not h.is_valid():
return "Invalid torrent"
s = h.status(0)
qp = h.queue_position()
if qp == -1:
qp = "seeding"
return "{:.0%} complete, ETA: {} " \
"(Down: {}/s Up: {}/s Peers: {} Queue pos: {})".format(
s.progress,
timedelta(seconds=self.get_eta(s)),
intrepr(s.download_payload_rate, precision=0),
intrepr(s.upload_payload_rate, precision=0),
s.num_peers,
qp,
)
def content_get(self, obj, part, item_data):
if part != "elm.swallow.icon":
return
h = item_data
if not h.is_valid():
return
s = h.status(0)
ic = Icon(obj)
try:
if h.is_paused():
ic.standard = "player_pause"
elif h.is_seed():
ic.standard = "up"
else:
ic.standard = "down"
except RuntimeError:
log.debug("Setting torrent ic failed")
ic.tooltip_text_set(self.state_str[s.state])
ic.size_hint_aspect_set(EVAS_ASPECT_CONTROL_VERTICAL, 1, 1)
return ic
def get_eta(self, s):
# if self.is_finished and self.options["stop_at_ratio"]:
# # We're a seed, so calculate the time to the 'stop_share_ratio'
# if not s.upload_payload_rate:
# return 0
# stop_ratio = self.session.settings().share_ratio_limit
# return (
# (s.all_time_download * stop_ratio) -
# s.all_time_upload
# ) / s.upload_payload_rate
left = s.total_wanted - s.total_wanted_done
if left <= 0 or s.download_payload_rate == 0:
return 0
try:
eta = left / s.download_payload_rate
except ZeroDivisionError:
eta = 0
return eta
class ItemMenu(Menu):
def __init__(self, parent, item, session, h):
Menu.__init__(self, parent)
self.item_add(
None,
"Resume" if h.is_paused() else "Pause",
None,
self.resume_torrent_cb if h.is_paused() else self.pause_torrent_cb,
h
)
q = self.item_add(None, "Queue", None, None)
self.item_add(
q, "Up", None, lambda x, y: h.queue_position_up()
)
self.item_add(
q, "Down", None, lambda x, y: h.queue_position_down()
)
self.item_add(
q, "Top", None, lambda x, y: h.queue_position_top()
)
self.item_add(
q, "Bottom", None, lambda x, y: h.queue_position_bottom()
)
rem = self.item_add(
None, "Remove torrent", None,
self.remove_torrent_cb, item, session, h, False
)
self.item_add(
rem, "and data files", None,
self.remove_torrent_cb, item, session, h, True
)
self.item_add(
None, "Force re-check", None,
self.force_recheck, h
)
self.item_separator_add(None)
self.item_add(
None, "Torrent properties", None,
self.torrent_props_cb, h
)
self.move(*parent.evas.pointer_canvas_xy_get())
self.show()
def remove_torrent_cb(
self, menu, item, glitem, session, h, with_data=False
):
menu.close()
session.remove_torrent(h, with_data)
def force_recheck(self, menu, item, h):
h.force_recheck()
def resume_torrent_cb(self, menu, item, h):
h.resume()
h.auto_managed(True)
def pause_torrent_cb(self, menu, item, h):
h.auto_managed(False)
h.pause()
def torrent_props_cb(self, menu, item, h):
from TorrentProps import TorrentProps
TorrentProps(self.top_widget, h)

View File

@ -1,181 +0,0 @@
#!/usr/bin/env python
from __future__ import print_function
# Pyperclip v1.3
# A cross-platform clipboard module for Python. (only handles plain text for now)
# By Al Sweigart al@coffeeghost.net
# Usage:
# import pyperclip
# pyperclip.copy('The text to be copied to the clipboard.')
# spam = pyperclip.paste()
# On Mac, this module makes use of the pbcopy and pbpaste commands, which should come with the os.
# On Linux, this module makes use of the xclip command, which should come with the os. Otherwise run "sudo apt-get install xclip"
# Copyright (c) 2010, Albert Sweigart
# All rights reserved.
#
# BSD-style license:
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the pyperclip nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY Albert Sweigart "AS IS" AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL Albert Sweigart BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# Change Log:
# 1.2 Use the platform module to help determine OS.
# 1.3 Changed ctypes.windll.user32.OpenClipboard(None) to ctypes.windll.user32.OpenClipboard(0), after some people ran into some TypeError
# 1.4 Use python-which library instead of os.system, removing a bunch of noise
# 1.5 add Cygwin support, command line interface & cleaned up command usage
import platform, os, subprocess, sys, re
def winGetClipboard():
ctypes.windll.user32.OpenClipboard(0)
pcontents = ctypes.windll.user32.GetClipboardData(1) # 1 is CF_TEXT
data = ctypes.c_char_p(pcontents).value
#ctypes.windll.kernel32.GlobalUnlock(pcontents)
ctypes.windll.user32.CloseClipboard()
return data
def winSetClipboard(text):
GMEM_DDESHARE = 0x2000
ctypes.windll.user32.OpenClipboard(0)
ctypes.windll.user32.EmptyClipboard()
try:
# works on Python 2 (bytes() only takes one argument)
hCd = ctypes.windll.kernel32.GlobalAlloc(GMEM_DDESHARE, len(bytes(text))+1)
except TypeError:
# works on Python 3 (bytes() requires an encoding)
hCd = ctypes.windll.kernel32.GlobalAlloc(GMEM_DDESHARE, len(bytes(text, 'ascii'))+1)
pchData = ctypes.windll.kernel32.GlobalLock(hCd)
try:
# works on Python 2 (bytes() only takes one argument)
ctypes.cdll.msvcrt.strcpy(ctypes.c_char_p(pchData), bytes(text))
except TypeError:
# works on Python 3 (bytes() requires an encoding)
ctypes.cdll.msvcrt.strcpy(ctypes.c_char_p(pchData), bytes(text, 'ascii'))
ctypes.windll.kernel32.GlobalUnlock(hCd)
ctypes.windll.user32.SetClipboardData(1,hCd)
ctypes.windll.user32.CloseClipboard()
def gtkGetClipboard():
return gtk.Clipboard().wait_for_text()
def gtkSetClipboard(text):
cb = gtk.Clipboard()
cb.set_text(text)
cb.store()
def qtGetClipboard():
return str(cb.text())
def qtSetClipboard(text):
cb.setText(text)
def has_command(cmd):
from which import which, WhichError
try:
which(cmd)
return True
except WhichError as e:
return False
class CommandClipboard(object):
def __init__(self, copy, paste):
self._copy = copy
self._paste = paste
self.required_cmds = set([copy[0], paste[0]])
@property
def available(self):
return all(map(has_command, self.required_cmds))
def copy(self, data):
p = subprocess.Popen(self._copy, stdin=subprocess.PIPE)
if sys.version_info > (3,):
data = data.encode('utf-8')
out, err = p.communicate(data)
assert p.returncode == 0
def paste(self):
p = subprocess.Popen(self._paste, stdout=subprocess.PIPE)
out, err = p.communicate()
assert p.returncode == 0
if sys.version_info > (3,):
out = out.decode('utf-8')
return out
def use(self):
global getcb, setcb
getcb = self.paste
setcb = self.copy
if os.name == 'nt' or platform.system() == 'Windows':
import ctypes
getcb = winGetClipboard
setcb = winSetClipboard
elif os.name == 'mac' or platform.system() == 'Darwin':
CommandClipboard(copy=['pbcopy'],paste=['pbpaste']).use()
else:
possible_impls = [
CommandClipboard(copy = ['putclip'], paste=['getclip']), # cygwin
CommandClipboard(copy = ['xsel','-ib'], paste=['xsel','-b']),
CommandClipboard(copy = ['xclip', '-selection', 'clipboard','-i'], paste=['xsel', '-selection', 'clipboard', '-o'])
]
for impl in possible_impls:
if impl.available:
impl.use()
break
else:
try:
import gtk
getcb = gtkGetClipboard
setcb = gtkSetClipboard
except ImportError:
try:
import PyQt4.QtCore
import PyQt4.QtGui
cb = PyQt4.QtGui.QApplication.clipboard()
getcb = qtGetClipboard
setcb = qtSetClipboard
except ImportError:
raise ImportError('Pyperclip requires the gtk or PyQt4 module installed, or some sort of xclip / xsel / getclip command.')
copy = setcb
paste = getcb
if __name__ == '__main__':
from optparse import OptionParser
p = OptionParser()
p.add_option('-i','--copy', action='store_const', const=copy, dest='action', default=paste)
p.add_option('-o','--paste', action='store_const', const=paste, dest='action')
opts, args = p.parse_args()
assert len(args) == 0
args = []
if opts.action is copy:
data = sys.stdin.read()
data = re.sub('\r?\n?$', '', data) # trim trailing NL
args.append(data)
ret = opts.action(*args)
if ret is not None:
print(ret)

View File

@ -1,342 +0,0 @@
#!/usr/bin/env python
# Copyright (c) 2002-2007 ActiveState Software Inc.
# See LICENSE.txt for license details.
# Author:
# Trent Mick (TrentM@ActiveState.com)
# Home:
# http://trentm.com/projects/which/
r"""Find the full path to commands.
which(command, path=None, verbose=0, exts=None)
Return the full path to the first match of the given command on the
path.
whichall(command, path=None, verbose=0, exts=None)
Return a list of full paths to all matches of the given command on
the path.
whichgen(command, path=None, verbose=0, exts=None)
Return a generator which will yield full paths to all matches of the
given command on the path.
By default the PATH environment variable is searched (as well as, on
Windows, the AppPaths key in the registry), but a specific 'path' list
to search may be specified as well. On Windows, the PATHEXT environment
variable is applied as appropriate.
If "verbose" is true then a tuple of the form
(<fullpath>, <matched-where-description>)
is returned for each match. The latter element is a textual description
of where the match was found. For example:
from PATH element 0
from HKLM\SOFTWARE\...\perl.exe
"""
_cmdlnUsage = """
Show the full path of commands.
Usage:
which [<options>...] [<command-name>...]
Options:
-h, --help Print this help and exit.
-V, --version Print the version info and exit.
-a, --all Print *all* matching paths.
-v, --verbose Print out how matches were located and
show near misses on stderr.
-q, --quiet Just print out matches. I.e., do not print out
near misses.
-p <altpath>, --path=<altpath>
An alternative path (list of directories) may
be specified for searching.
-e <exts>, --exts=<exts>
Specify a list of extensions to consider instead
of the usual list (';'-separate list, Windows
only).
Show the full path to the program that would be run for each given
command name, if any. Which, like GNU's which, returns the number of
failed arguments, or -1 when no <command-name> was given.
Near misses include duplicates, non-regular files and (on Un*x)
files without executable access.
"""
__revision__ = "$Id: which.py 1448 2007-02-28 19:13:06Z trentm $"
__version_info__ = (1, 1, 3)
__version__ = '.'.join(map(str, __version_info__))
__all__ = ["which", "whichall", "whichgen", "WhichError"]
import os
import sys
import getopt
import stat
#---- exceptions
class WhichError(Exception):
pass
#---- internal support stuff
def _getRegisteredExecutable(exeName):
"""Windows allow application paths to be registered in the registry."""
registered = None
if sys.platform.startswith('win'):
if os.path.splitext(exeName)[1].lower() != '.exe':
exeName += '.exe'
import _winreg
try:
key = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\" +\
exeName
value = _winreg.QueryValue(_winreg.HKEY_LOCAL_MACHINE, key)
registered = (value, "from HKLM\\"+key)
except _winreg.error:
pass
if registered and not os.path.exists(registered[0]):
registered = None
return registered
def _samefile(fname1, fname2):
if sys.platform.startswith('win'):
return ( os.path.normpath(os.path.normcase(fname1)) ==\
os.path.normpath(os.path.normcase(fname2)) )
else:
return os.path.samefile(fname1, fname2)
def _cull(potential, matches, verbose=0):
"""Cull inappropriate matches. Possible reasons:
- a duplicate of a previous match
- not a disk file
- not executable (non-Windows)
If 'potential' is approved it is returned and added to 'matches'.
Otherwise, None is returned.
"""
for match in matches: # don't yield duplicates
if _samefile(potential[0], match[0]):
if verbose:
sys.stderr.write("duplicate: %s (%s)\n" % potential)
return None
else:
if not stat.S_ISREG(os.stat(potential[0]).st_mode):
if verbose:
sys.stderr.write("not a regular file: %s (%s)\n" % potential)
elif sys.platform != "win32" \
and not os.access(potential[0], os.X_OK):
if verbose:
sys.stderr.write("no executable access: %s (%s)\n"\
% potential)
else:
matches.append(potential)
return potential
#---- module API
def whichgen(command, path=None, verbose=0, exts=None):
"""Return a generator of full paths to the given command.
"command" is a the name of the executable to search for.
"path" is an optional alternate path list to search. The default it
to use the PATH environment variable.
"verbose", if true, will cause a 2-tuple to be returned for each
match. The second element is a textual description of where the
match was found.
"exts" optionally allows one to specify a list of extensions to use
instead of the standard list for this system. This can
effectively be used as an optimization to, for example, avoid
stat's of "foo.vbs" when searching for "foo" and you know it is
not a VisualBasic script but ".vbs" is on PATHEXT. This option
is only supported on Windows.
This method returns a generator which yields either full paths to
the given command or, if verbose, tuples of the form (<path to
command>, <where path found>).
"""
matches = []
if path is None:
usingGivenPath = 0
path = os.environ.get("PATH", "").split(os.pathsep)
if sys.platform.startswith("win"):
path.insert(0, os.curdir) # implied by Windows shell
else:
usingGivenPath = 1
# Windows has the concept of a list of extensions (PATHEXT env var).
if sys.platform.startswith("win"):
if exts is None:
exts = os.environ.get("PATHEXT", "").split(os.pathsep)
# If '.exe' is not in exts then obviously this is Win9x and
# or a bogus PATHEXT, then use a reasonable default.
for ext in exts:
if ext.lower() == ".exe":
break
else:
exts = ['.COM', '.EXE', '.BAT']
elif not isinstance(exts, list):
raise TypeError("'exts' argument must be a list or None")
else:
if exts is not None:
raise WhichError("'exts' argument is not supported on "\
"platform '%s'" % sys.platform)
exts = []
# File name cannot have path separators because PATH lookup does not
# work that way.
if os.sep in command or os.altsep and os.altsep in command:
if os.path.exists(command):
match = _cull((command, "explicit path given"), matches, verbose)
if verbose:
yield match
else:
yield match[0]
else:
for i in range(len(path)):
dirName = path[i]
# On windows the dirName *could* be quoted, drop the quotes
if sys.platform.startswith("win") and len(dirName) >= 2\
and dirName[0] == '"' and dirName[-1] == '"':
dirName = dirName[1:-1]
for ext in ['']+exts:
absName = os.path.abspath(
os.path.normpath(os.path.join(dirName, command+ext)))
if os.path.isfile(absName):
if usingGivenPath:
fromWhere = "from given path element %d" % i
elif not sys.platform.startswith("win"):
fromWhere = "from PATH element %d" % i
elif i == 0:
fromWhere = "from current directory"
else:
fromWhere = "from PATH element %d" % (i-1)
match = _cull((absName, fromWhere), matches, verbose)
if match:
if verbose:
yield match
else:
yield match[0]
match = _getRegisteredExecutable(command)
if match is not None:
match = _cull(match, matches, verbose)
if match:
if verbose:
yield match
else:
yield match[0]
def which(command, path=None, verbose=0, exts=None):
"""Return the full path to the first match of the given command on
the path.
"command" is a the name of the executable to search for.
"path" is an optional alternate path list to search. The default it
to use the PATH environment variable.
"verbose", if true, will cause a 2-tuple to be returned. The second
element is a textual description of where the match was found.
"exts" optionally allows one to specify a list of extensions to use
instead of the standard list for this system. This can
effectively be used as an optimization to, for example, avoid
stat's of "foo.vbs" when searching for "foo" and you know it is
not a VisualBasic script but ".vbs" is on PATHEXT. This option
is only supported on Windows.
If no match is found for the command, a WhichError is raised.
"""
try:
match = whichgen(command, path, verbose, exts).next()
except StopIteration:
raise WhichError("Could not find '%s' on the path." % command)
return match
def whichall(command, path=None, verbose=0, exts=None):
"""Return a list of full paths to all matches of the given command
on the path.
"command" is a the name of the executable to search for.
"path" is an optional alternate path list to search. The default it
to use the PATH environment variable.
"verbose", if true, will cause a 2-tuple to be returned for each
match. The second element is a textual description of where the
match was found.
"exts" optionally allows one to specify a list of extensions to use
instead of the standard list for this system. This can
effectively be used as an optimization to, for example, avoid
stat's of "foo.vbs" when searching for "foo" and you know it is
not a VisualBasic script but ".vbs" is on PATHEXT. This option
is only supported on Windows.
"""
return list( whichgen(command, path, verbose, exts) )
#---- mainline
def main(argv):
all = 0
verbose = 0
altpath = None
exts = None
try:
optlist, args = getopt.getopt(argv[1:], 'haVvqp:e:',
['help', 'all', 'version', 'verbose', 'quiet', 'path=', 'exts='])
except getopt.GetoptError, msg:
sys.stderr.write("which: error: %s. Your invocation was: %s\n"\
% (msg, argv))
sys.stderr.write("Try 'which --help'.\n")
return 1
for opt, optarg in optlist:
if opt in ('-h', '--help'):
print _cmdlnUsage
return 0
elif opt in ('-V', '--version'):
print "which %s" % __version__
return 0
elif opt in ('-a', '--all'):
all = 1
elif opt in ('-v', '--verbose'):
verbose = 1
elif opt in ('-q', '--quiet'):
verbose = 0
elif opt in ('-p', '--path'):
if optarg:
altpath = optarg.split(os.pathsep)
else:
altpath = []
elif opt in ('-e', '--exts'):
if optarg:
exts = optarg.split(os.pathsep)
else:
exts = []
if len(args) == 0:
return -1
failures = 0
for arg in args:
#print "debug: search for %r" % arg
nmatches = 0
for match in whichgen(arg, path=altpath, verbose=verbose, exts=exts):
if verbose:
print "%s (%s)" % match
else:
print match
nmatches += 1
if not all:
break
if not nmatches:
failures += 1
return failures
if __name__ == "__main__":
sys.exit( main(sys.argv) )

View File

@ -1,8 +1,7 @@
#!/usr/bin/env python2
#
# Epour - A bittorrent client using EFL and libtorrent
#
# Copyright 2012-2013 Kai Huuhko <kai.huuhko@gmail.com>
# Copyright 2012-2014 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
@ -22,51 +21,49 @@
import os
import mimetypes
from ConfigParser import SafeConfigParser
import cPickle
import urlparse
import urllib
import HTMLParser
import logging
import shutil
import cgi
import cPickle
from collections import OrderedDict
import libtorrent as lt
try:
from efl.ecore import Timer
except:
from ecore import Timer
from efl.ecore import Timer
import logging
from Globals import data_dir
def torrent_path_get(ihash):
return os.path.join(data_dir, "{}.torrent".format(ihash))
from Globals import conf_dir, conf_path, data_dir
class Session(lt.session):
def __init__(self, parent):
self.parent = parent
def __init__(self, conf):
self.conf = conf
self.log = logging.getLogger("epour.session")
from Globals import version
ver_ints = []
for s in version.split("."):
ver_ints.append(int(s))
ver_ints.append(0)
fp = lt.fingerprint("EP", *ver_ints)
self.log.debug("peer-id: {}".format(fp))
lt.session.__init__(
self,
fingerprint = fp,
# flags = \
#lt.session_flags_t.add_default_plugins | \
fingerprint=fp,
# flags=
#lt.session_flags_t.add_default_plugins|
#lt.session_flags_t.start_default_features
)
self.log.info("Session started")
self.torrents = {}
self.torrents = OrderedDict()
#sdpipsdtsppe
#theprtertoer
@ -78,8 +75,6 @@ class Session(lt.session):
mask = 0b000001000001
self.set_alert_mask(mask)
conf = self.conf = self.setup_conf()
self.listen_on(
conf.getint("Settings", "listen_low"),
conf.getint("Settings", "listen_high")
@ -87,66 +82,44 @@ class Session(lt.session):
self.alert_manager = AlertManager(self)
self.alert_manager.callback_add(
"metadata_received_alert", self.metadata_received)
"add_torrent_alert", self._add_torrent_cb)
self.alert_manager.callback_add(
"metadata_received_alert", self._metadata_received_cb)
def metadata_received(self, a):
def _add_torrent_cb(self, a):
e = a.error
if e.value() > 0:
self.log.error("Adding torrent failed: %r" % (e.message()))
return
h = a.handle
ihash = str(h.info_hash())
self.torrents[ihash] = a.params
self.log.debug("Torrent added.")
def _metadata_received_cb(self, a):
h = a.handle
ihash = str(h.info_hash())
self.log.debug("Metadata received.")
t_path = self.write_torrent(h)
self.torrents[ihash] = t_path
def write_torrent(self, h):
t_info = h.get_torrent_info()
ihash = str(h.info_hash())
self.log.debug("Writing torrent file {}".format(ihash))
md = lt.bdecode(t_info.metadata())
t = {}
t["info"] = md
t_path = os.path.join(
data_dir, "{}.torrent".format(ihash)
)
with open(t_path, "wb") as f:
f.write(lt.bencode(t))
return t_path
def setup_conf(self):
conf = SafeConfigParser({
"storage_path": os.path.expanduser(
os.path.join("~", "Downloads")
),
"confirmations": str(False),
"delete_original": str(False),
"listen_low": str(0),
"listen_high": str(0),
})
conf.read(conf_path)
if not conf.has_section("Settings"):
conf.add_section("Settings")
return conf
def save_conf(self):
with open(conf_path, 'wb') as configfile:
self.conf.write(configfile)
self.write_torrent(h)
self.torrents[ihash]["ti"] = h.get_torrent_info()
def load_state(self):
try:
with open(os.path.join(data_dir, "session"), 'rb') as f:
state = lt.bdecode(f.read())
lt.session.load_state(self, state)
except:
except Exception as e:
self.log.debug("Could not load previous session state.")
self.log.debug(e)
else:
self.log.info("Session restored from disk.")
settings = self.settings()
from Globals import version
settings.user_agent = "Epour/{} libtorrent/{}".format(version, lt.version)
version += ".0"
ver_s = "Epour/{} libtorrent/{}".format(version, lt.version)
settings.user_agent = ver_s
self.log.debug("User agent: {}".format(ver_s))
self.set_settings(settings)
def save_state(self):
@ -169,29 +142,58 @@ class Session(lt.session):
self.log.warning("Could not open the list of torrents.")
else:
try:
paths = cPickle.load(pkl_file)
torrents = cPickle.load(pkl_file)
except EOFError:
self.log.exception("Opening the list of torrents failed.")
else:
self.log.debug(
"List of torrents opened, "
"restoring {} torrents.".format(len(paths))
"restoring {} torrents.".format(len(torrents))
)
for k, v in paths.iteritems():
for i, t in torrents.items():
try:
self.log.debug("Adding {}".format(v))
self.add_torrent(v)
except:
self.log.exception(
"Restoring torrent {0} failed".format(v)
)
if isinstance(t, dict):
for k, v in t.items():
if k == "ti":
with open(v, "rb") as f:
torrents[i][k] = \
lt.torrent_info(lt.bdecode(f.read()))
elif k == "info_hash":
torrents[i][k] = lt.big_number(v)
else:
# Upgrade from older versions where t is file path
p = t
t = {}
with open(p, "rb") as f:
t["ti"] = \
lt.torrent_info(lt.bdecode(f.read()))
rp = os.path.join(data_dir, i + ".fastresume")
if os.path.exists(rp):
with open(rp, "rb") as f:
t["resume_data"] = f.read()
except Exception:
self.log.exception("Opening torrent file %s failed", v)
continue
self.async_add_torrent(t)
finally:
pkl_file.close()
def save_torrents(self):
self.log.debug("Saving {} torrents.".format(len(self.torrents)))
for i, t in self.torrents.items():
for k, v in t.items():
if k == "ti":
self.torrents[i][k] = torrent_path_get(i)
elif k == "info_hash":
if v.is_all_zeros():
del self.torrents[i][k]
else:
self.torrents[i][k] = v.to_string()
with open(os.path.join(data_dir, "torrents"), 'wb') as f:
cPickle.dump(self.torrents, f)
cPickle.dump(self.torrents, f, protocol=2)
self.log.debug("List of torrents saved.")
@ -201,92 +203,30 @@ class Session(lt.session):
continue
data = lt.bencode(h.write_resume_data())
with open(os.path.join(
data_dir, '{}.fastresume'.format(h.info_hash())
), 'wb'
) as f:
data_dir, str(h.info_hash()) + ".fastresume"
), 'wb') as f:
f.write(data)
self.log.debug("Fast resume data saved.")
def add_torrent(self, t_uri):
if not t_uri:
return
storage_path = self.conf.get("Settings", "storage_path")
if not t_uri.startswith("magnet"):
mimetype = mimetypes.guess_type(t_uri)[0]
if not mimetype == "application/x-bittorrent":
self.log.error("Invalid file")
return
if t_uri.startswith("file://"):
t_uri = urllib.unquote(urlparse.urlsplit(t_uri).path)
with open(t_uri, 'rb') as t:
t_raw = lt.bdecode(t.read())
info = lt.torrent_info(t_raw)
rd = None
try:
with open(os.path.join(
data_dir, "{}.fastresume".format(info.info_hash())
), "rb"
) as f:
rd = lt.bdecode(f.read())
except:
try:
with open(os.path.join(
data_dir, "{}.fastresume".format(info.name())
), "rb"
) as f:
rd = lt.bdecode(f.read())
except:
self.log.debug("Invalid resume data")
h = lt.session.add_torrent(
self, info, storage_path, resume_data=rd)
ihash = str(h.info_hash())
new_uri = os.path.join(
data_dir, "{}.torrent".format(ihash)
)
if t_uri == new_uri:
pass
else:
shutil.copy(t_uri, new_uri)
if self.conf.getboolean("Settings", "delete_original"):
self.log.debug("Deleting original torrent file {}".format(t_uri))
os.remove(t_uri)
t_uri = new_uri
else:
t_uri = urllib.unquote(t_uri)
t_uri = str(HTMLParser.HTMLParser().unescape(t_uri))
h = lt.add_magnet_uri(
self, t_uri,
{ "save_path": str(storage_path) }
)
if not h.is_valid():
self.log.error("Invalid torrent handle")
def write_torrent(self, h):
if h is None:
self.log.debug("Tried to write torrent while handle was empty.")
return
t_info = h.get_torrent_info()
ihash = str(h.info_hash())
self.torrents[ihash] = t_uri
self.log.debug("Writing torrent file {}".format(ihash))
if not hasattr(lt, "torrent_added_alert"):
class torrent_added_alert(object):
def __init__(self, h):
self.handle = h
md = lt.bdecode(t_info.metadata())
t = {}
t["info"] = md
t_path = torrent_path_get(ihash)
with open(t_path, "wb") as f:
f.write(lt.bencode(t))
a = torrent_added_alert(h)
self.alert_manager.signal(a)
return t_path
def remove_torrent(self, h, with_data=False):
ihash = str(h.info_hash())
@ -294,24 +234,26 @@ class Session(lt.session):
fr_path = os.path.join(
data_dir, "{}.fastresume".format(ihash)
)
t_path = self.torrents[ihash]
#t_dict = self.torrents[ihash]
del self.torrents[ihash]
lt.session.remove_torrent(self, h, option=with_data)
try:
with open(fr_path): pass
with open(fr_path):
pass
except IOError:
self.log.debug("Could not remove fast resume data.")
else:
os.remove(fr_path)
try:
with open(t_path): pass
with open(torrent_path_get(ihash)):
pass
except IOError:
self.log.debug("Could not remove torrent file.")
else:
os.remove(t_path)
os.remove(torrent_path_get(ihash))
if not hasattr(lt, "torrent_removed_alert"):
class torrent_removed_alert(object):
@ -325,29 +267,64 @@ class Session(lt.session):
return ihash
def add_torrent_from_file(self, add_dict, t_uri):
mimetype = mimetypes.guess_type(t_uri)[0]
if not mimetype == "application/x-bittorrent":
self.log.error("Invalid file")
return
class TorrentDict(dict):
if t_uri.startswith("file://"):
t_uri = urllib.unquote(urlparse.urlsplit(t_uri).path)
# Required keys are save_path and either ti or info_hash
with open(t_uri, 'rb') as t:
t_raw = lt.bdecode(t.read())
# torrent_info ti
# string tracker_url
# string info_hash
# string name
# string save_path
# string resume_data
# storage_mode_t storage_mode
# bool paused
# bool auto_managed
# bool duplicate_is_error
# storage?
# object userdata?
# bool seed_mode
# bool override_resume_data
# bool upload_mode
info = lt.torrent_info(t_raw)
add_dict["ti"] = info
rd = None
try:
with open(os.path.join(
data_dir, "{}.fastresume".format(info.info_hash())
), "rb"
) as f:
rd = f.read()
except Exception:
self.log.debug("Invalid resume data")
else:
add_dict["resume_data"] = rd
ihash = str(info.info_hash())
new_uri = os.path.join(
data_dir, "{}.torrent".format(ihash)
)
if t_uri == new_uri:
pass
else:
shutil.copy(t_uri, new_uri)
if self.conf.getboolean("Settings", "delete_original"):
self.log.debug(
"Deleting original torrent file {}".format(t_uri))
os.remove(t_uri)
t_uri = new_uri
self.async_add_torrent(add_dict)
def add_torrent_from_magnet(self, add_dict, t_uri):
self.log.debug("Adding %r", t_uri)
tmp_dict = lt.parse_magnet_uri(bytes(t_uri))
tmp_dict.update(add_dict)
self.async_add_torrent(tmp_dict)
def add_torrent_from_hash(self, add_dict, t_uri):
add_dict["info_hash"] = lt.info_hash(bytes(t_uri))
self.log.debug("Adding %s", t_uri)
self.async_add_torrent(add_dict)
def __init__(self, **kwargs):
dict.__init__(self, **kwargs)
class AlertManager(object):
@ -361,7 +338,7 @@ class AlertManager(object):
self.timer = Timer(self.update_interval, self.update)
def callback_add(self, alert_type, cb, *args, **kwargs):
if not self.alerts.has_key(alert_type):
if not alert_type in self.alerts:
self.alerts[alert_type] = []
self.alerts[alert_type].append((cb, args, kwargs))
@ -373,7 +350,7 @@ class AlertManager(object):
def signal(self, a):
a_name = type(a).__name__
if not self.alerts.has_key(a_name):
if not a_name in self.alerts:
self.log.debug("No handler: {} | {}".format(a_name, a))
return
@ -386,9 +363,9 @@ class AlertManager(object):
def update(self):
while 1:
a = self.session.pop_alert()
if not a: break
if not a:
break
self.signal(a)
return True

View File

@ -4,24 +4,24 @@ from DistUtilsExtra.auto import setup
from epour.Globals import version
setup(name='epour',
version=version,
author='Kai Huuhko',
author_email='kai.huuhko@gmail.com',
maintainer='Kai Huuhko',
maintainer_email='kai.huuhko@gmail.com',
description='Simple torrent client',
long_description='Epour is a simple torrent client using EFL and libtorrent.',
#url='',
#download_url='',
license='GNU GPL',
platforms='linux',
setup(
name='epour',
version=version,
author='Kai Huuhko',
author_email='kai.huuhko@gmail.com',
maintainer='Kai Huuhko',
maintainer_email='kai.huuhko@gmail.com',
description='A torrent client',
long_description=(
'Epour is a torrent client based on EFL and rb-libtorrent.'
),
#url='',
#download_url='',
license='GNU GPL',
platforms='linux',
requires=[
'libtorrent',
'evas',
'ecore',
'elementary',
'e_dbus',
'efl',
],
provides=[
'epour',