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()