forked from enlightenment/epour
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:
parent
4ffbcad8ca
commit
d04fdf23f5
|
@ -1,3 +1,2 @@
|
|||
*.py[co]
|
||||
epour.sublime-project
|
||||
epour.sublime-workspace
|
||||
build/
|
||||
|
|
36
TODO
36
TODO
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
|
146
epour/Epour.py
146
epour/Epour.py
|
@ -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()
|
|
@ -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"))
|
||||
|
|
|
@ -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()
|
|
@ -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.
|
||||
|
|
@ -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
|
|
@ -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()
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -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)
|
|
@ -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()
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -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) )
|
||||
|
||||
|
339
epour/session.py
339
epour/session.py
|
@ -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
|
||||
|
||||
|
|
32
setup.py
32
setup.py
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue