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]
|
*.py[co]
|
||||||
epour.sublime-project
|
build/
|
||||||
epour.sublime-workspace
|
|
||||||
|
|
28
TODO
28
TODO
|
@ -4,19 +4,23 @@ I18N:
|
||||||
☐ Create pot file
|
☐ Create pot file
|
||||||
☐ Generate po files
|
☐ Generate po files
|
||||||
Add Torrent-dialog:
|
Add Torrent-dialog:
|
||||||
☐ Create a new class to hold a torrent and store its instances between sessions.
|
✔ Use dicts to hold torrent info between sessions. @done (15:57 30.06.2014)
|
||||||
Migrate the old dict(ihash->torrent_file) to the new list of torrents
|
✔ Migrate the old ihash->torrent_file dict to the new list of torrents @done (15:57 30.06.2014)
|
||||||
☐ Dialog window
|
✔ Dialog window @done (15:57 30.06.2014)
|
||||||
☐ Initial paused-state
|
✔ Options @done (15:57 30.06.2014)
|
||||||
☐ File selector
|
✔ File selector @done (15:57 30.06.2014)
|
||||||
☐ Storage path
|
✔ Storage path @done (15:57 30.06.2014)
|
||||||
☐ Add preferences for initial/automatic values for the above
|
☐ Add preferences for initial/automatic values for the above
|
||||||
Misc:
|
Misc:
|
||||||
☐ Torrent tooltips
|
☐ Torrent tooltips
|
||||||
handle.status()
|
Using handle.status()
|
||||||
☐ More tooltips in general
|
☐ More tooltips in general
|
||||||
☐ More D-Bus controls #epour/Epour.py@EpourDBus
|
☐ 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)
|
☐ Auto-paste magnet links from clipboard ./epour/gui/Main.py>TorrentSelector
|
||||||
Note: The pasted text is not filtered.
|
Remake this feature
|
||||||
☐ Construct and populate #epour/gui/Preferences.py@SessionSettings with an Idler
|
✘ Construct and populate ./epour/gui/Preferences.py>PreferencesSession with an Idler @cancelled (21:20 01.07.2014)
|
||||||
✔ Move proxy settings to its own naviframe page and don't autocollapse the frames @done (13-08-18 18:02)
|
☐ 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 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()
|
logging.shutdown()
|
||||||
|
|
|
@ -7,6 +7,6 @@ Standards-Version: 3.9.1
|
||||||
|
|
||||||
Package: epour
|
Package: epour
|
||||||
Architecture: all
|
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
|
Description: Simple torrent client
|
||||||
Epour is a simple torrent client using EFL and libtorrent.
|
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
|
import os
|
||||||
|
|
||||||
version = "0.5.2.0"
|
version = "0.6.0"
|
||||||
conf_dir = os.path.expanduser(os.path.join("~", ".config", "epour"))
|
conf_dir = os.path.expanduser(os.path.join("~", ".config", "epour"))
|
||||||
conf_path = os.path.join(conf_dir, "epour.conf")
|
conf_path = os.path.join(conf_dir, "epour.conf")
|
||||||
data_dir = os.path.expanduser(os.path.join("~", ".local", "share", "epour"))
|
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
|
# 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
|
# 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
|
# 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.
|
# (at your option) any later version.
|
||||||
#
|
#
|
||||||
# This program is distributed in the hope that it will be useful,
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
@ -22,11 +22,10 @@
|
||||||
import os
|
import os
|
||||||
import cgi
|
import cgi
|
||||||
import logging
|
import logging
|
||||||
log = logging.getLogger("epour")
|
log = logging.getLogger("epour.preferences")
|
||||||
|
|
||||||
import libtorrent as lt
|
import libtorrent as lt
|
||||||
|
|
||||||
from efl.elementary.icon import Icon
|
|
||||||
from efl.elementary.box import Box
|
from efl.elementary.box import Box
|
||||||
from efl.elementary.label import Label
|
from efl.elementary.label import Label
|
||||||
from efl.elementary.button import Button
|
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.check import Check
|
||||||
from efl.elementary.spinner import Spinner
|
from efl.elementary.spinner import Spinner
|
||||||
from efl.elementary.hoversel import Hoversel
|
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.fileselector_button import FileselectorButton
|
||||||
from efl.elementary.scroller import Scroller, ELM_SCROLLER_POLICY_OFF, \
|
from efl.elementary.scroller import Scroller, ELM_SCROLLER_POLICY_AUTO
|
||||||
ELM_SCROLLER_POLICY_AUTO
|
|
||||||
from efl.elementary.separator import Separator
|
from efl.elementary.separator import Separator
|
||||||
from efl.elementary.slider import Slider
|
from efl.elementary.slider import Slider
|
||||||
from efl.elementary.actionslider import Actionslider, \
|
from efl.elementary.actionslider import Actionslider, \
|
||||||
ELM_ACTIONSLIDER_LEFT, ELM_ACTIONSLIDER_CENTER, \
|
ELM_ACTIONSLIDER_LEFT, ELM_ACTIONSLIDER_CENTER, \
|
||||||
ELM_ACTIONSLIDER_RIGHT, ELM_ACTIONSLIDER_ALL
|
ELM_ACTIONSLIDER_RIGHT, ELM_ACTIONSLIDER_ALL
|
||||||
from efl.elementary.naviframe import Naviframe
|
|
||||||
from efl.elementary.table import Table
|
from efl.elementary.table import Table
|
||||||
from efl.elementary.configuration import Configuration
|
from efl.elementary.configuration import Configuration
|
||||||
from efl.evas import Rectangle
|
from efl.elementary.window import StandardWindow
|
||||||
from efl.ecore import Timer
|
|
||||||
from efl.elementary.window import Window, ELM_WIN_BASIC
|
|
||||||
from efl.elementary.background import Background
|
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
|
from Widgets import UnitSpinner, Error, Information
|
||||||
EXPAND_HORIZ = 1.0, 0.0
|
|
||||||
FILL_BOTH = -1.0, -1.0
|
EXPAND_BOTH = EVAS_HINT_EXPAND, EVAS_HINT_EXPAND
|
||||||
FILL_HORIZ = -1.0, 0.5
|
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
|
SCROLL_BOTH = ELM_SCROLLER_POLICY_AUTO, ELM_SCROLLER_POLICY_AUTO
|
||||||
|
|
||||||
class PreferencesDialog(Window):
|
|
||||||
|
class PreferencesDialog(StandardWindow):
|
||||||
|
|
||||||
""" Base class for all preferences dialogs """
|
""" Base class for all preferences dialogs """
|
||||||
def __init__(self, title):
|
|
||||||
|
def __init__(self, name, title):
|
||||||
|
|
||||||
elm_conf = Configuration()
|
elm_conf = Configuration()
|
||||||
scale = elm_conf.scale
|
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
|
self.size = scale * 480, scale * 320
|
||||||
|
|
||||||
|
@ -75,11 +77,10 @@ class PreferencesDialog(Window):
|
||||||
self.resize_object_add(bg)
|
self.resize_object_add(bg)
|
||||||
bg.show()
|
bg.show()
|
||||||
|
|
||||||
# bt = Button(self, text="Close")
|
self.scroller = Scroller(
|
||||||
# bt.callback_clicked_add(lambda b: self.delete())
|
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.resize_object_add(self.scroller)
|
||||||
self.scroller.show()
|
self.scroller.show()
|
||||||
|
|
||||||
|
@ -87,18 +88,16 @@ class PreferencesDialog(Window):
|
||||||
self.box.size_hint_weight = EXPAND_BOTH
|
self.box.size_hint_weight = EXPAND_BOTH
|
||||||
self.scroller.content = self.box
|
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):
|
class PreferencesGeneral(PreferencesDialog):
|
||||||
|
|
||||||
""" General preference dialog """
|
""" General preference dialog """
|
||||||
|
|
||||||
def __init__(self, parent, session):
|
def __init__(self, parent, session):
|
||||||
self.session = session
|
self.session = session
|
||||||
conf = session.conf
|
conf = session.conf
|
||||||
PreferencesDialog.__init__(self, "General")
|
PreferencesDialog.__init__(
|
||||||
|
self, "epour_prefs_general", "Epour General Preferences")
|
||||||
|
|
||||||
limits = Limits(self, session)
|
limits = Limits(self, session)
|
||||||
ports = ListenPorts(self, session)
|
ports = ListenPorts(self, session)
|
||||||
|
@ -113,32 +112,44 @@ class PreferencesGeneral(PreferencesDialog):
|
||||||
sep1.horizontal = True
|
sep1.horizontal = True
|
||||||
|
|
||||||
chk1 = Check(self)
|
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.text = "Delete original .torrent file when added"
|
||||||
chk1.state = conf.getboolean("Settings", "delete_original")
|
chk1.state = conf.getboolean("Settings", "delete_original")
|
||||||
chk1.callback_changed_add(lambda x: conf.set("Settings",
|
chk1.callback_changed_add(lambda x: conf.set(
|
||||||
"delete_original", str(bool(chk1.state))))
|
"Settings", "delete_original", str(bool(chk1.state))
|
||||||
|
))
|
||||||
|
|
||||||
chk2 = Check(self)
|
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.text = "Ask for confirmation on exit"
|
||||||
chk2.state = conf.getboolean("Settings", "confirmations")
|
chk2.state = conf.getboolean("Settings", "confirm_exit")
|
||||||
chk2.callback_changed_add(lambda x: conf.set("Settings",
|
chk2.callback_changed_add(lambda x: conf.set(
|
||||||
"confirmations", str(bool(chk2.state))))
|
"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 = Separator(self)
|
||||||
sep2.horizontal = True
|
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()
|
w.show()
|
||||||
self.box.pack_end(w)
|
self.box.pack_end(w)
|
||||||
|
|
||||||
|
|
||||||
class DataStorageSelector(Frame):
|
class DataStorageSelector(Frame):
|
||||||
|
|
||||||
def __init__(self, parent, conf):
|
def __init__(self, parent, conf):
|
||||||
Frame.__init__(self, parent)
|
Frame.__init__(self, parent)
|
||||||
|
|
||||||
self.size_hint_align = -1.0, 0.0
|
self.size_hint_align = FILL_HORIZ
|
||||||
self.size_hint_weight = 1.0, 0.0
|
self.size_hint_weight = EXPAND_HORIZ
|
||||||
self.text = "Data storage"
|
self.text = "Data storage"
|
||||||
|
|
||||||
self.conf = conf
|
self.conf = conf
|
||||||
|
@ -148,12 +159,10 @@ class DataStorageSelector(Frame):
|
||||||
lbl = self.path_lbl = Label(parent)
|
lbl = self.path_lbl = Label(parent)
|
||||||
lbl.text = conf.get("Settings", "storage_path")
|
lbl.text = conf.get("Settings", "storage_path")
|
||||||
|
|
||||||
self.dlsel = dlsel = FileselectorButton(self)
|
self.dlsel = dlsel = FsButton(
|
||||||
dlsel.size_hint_align = -1.0, 0.0
|
self, size_hint_align=FILL_HORIZ, inwin_mode=False,
|
||||||
dlsel.inwin_mode = False
|
text="Change path", folder_only=True, expandable=False
|
||||||
dlsel.folder_only = True
|
)
|
||||||
dlsel.expandable = False
|
|
||||||
dlsel.text = "Change path"
|
|
||||||
dlsel.path = conf.get("Settings", "storage_path")
|
dlsel.path = conf.get("Settings", "storage_path")
|
||||||
dlsel.callback_file_chosen_add(self.save_dlpath)
|
dlsel.callback_file_chosen_add(self.save_dlpath)
|
||||||
|
|
||||||
|
@ -169,14 +178,17 @@ class DataStorageSelector(Frame):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not os.path.exists(self.dlsel.path):
|
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.")
|
"You have selected an invalid data storage path for torrents.")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.path_lbl.text = path
|
self.path_lbl.text = path
|
||||||
self.conf.set("Settings", "storage_path", self.dlsel.path)
|
self.conf.set("Settings", "storage_path", self.dlsel.path)
|
||||||
|
|
||||||
|
|
||||||
class ListenPorts(Frame):
|
class ListenPorts(Frame):
|
||||||
|
|
||||||
def __init__(self, parent, session):
|
def __init__(self, parent, session):
|
||||||
Frame.__init__(self, parent)
|
Frame.__init__(self, parent)
|
||||||
|
|
||||||
|
@ -185,16 +197,16 @@ class ListenPorts(Frame):
|
||||||
self.size_hint_align = FILL_HORIZ
|
self.size_hint_align = FILL_HORIZ
|
||||||
self.text = "Listen port (range)"
|
self.text = "Listen port (range)"
|
||||||
|
|
||||||
port = session.listen_port()
|
#port = session.listen_port()
|
||||||
|
|
||||||
b = Box(parent)
|
b = Box(parent)
|
||||||
b.size_hint_weight = EXPAND_HORIZ
|
b.size_hint_weight = EXPAND_HORIZ
|
||||||
|
|
||||||
lp = self.lp = RangeSpinners(
|
lp = self.lp = RangeSpinners(
|
||||||
parent,
|
parent,
|
||||||
low = session.conf.getint("Settings", "listen_low"),
|
low=session.conf.getint("Settings", "listen_low"),
|
||||||
high = session.conf.getint("Settings", "listen_high"),
|
high=session.conf.getint("Settings", "listen_high"),
|
||||||
minim = 0, maxim = 65535)
|
minim=0, maxim=65535)
|
||||||
lp.show()
|
lp.show()
|
||||||
b.pack_end(lp)
|
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_low", str(low))
|
||||||
self.session.conf.set("Settings", "listen_high", str(high))
|
self.session.conf.set("Settings", "listen_high", str(high))
|
||||||
|
|
||||||
|
|
||||||
class PreferencesProxy(PreferencesDialog):
|
class PreferencesProxy(PreferencesDialog):
|
||||||
|
|
||||||
""" Proxy preference dialog """
|
""" Proxy preference dialog """
|
||||||
|
|
||||||
def __init__(self, parent, session):
|
def __init__(self, parent, session):
|
||||||
PreferencesDialog.__init__(self, "Proxy")
|
PreferencesDialog.__init__(
|
||||||
|
self, "epour_prefs_proxy", "Epour Proxy Preferences")
|
||||||
|
|
||||||
proxies = [
|
proxies = [
|
||||||
["Proxy for torrent peer connections",
|
["Proxy for torrent peer connections",
|
||||||
|
@ -236,6 +252,7 @@ class PreferencesProxy(PreferencesDialog):
|
||||||
pg.show()
|
pg.show()
|
||||||
self.box.pack_end(pg)
|
self.box.pack_end(pg)
|
||||||
|
|
||||||
|
|
||||||
class ProxyGroup(Frame):
|
class ProxyGroup(Frame):
|
||||||
|
|
||||||
proxy_types = {
|
proxy_types = {
|
||||||
|
@ -248,21 +265,20 @@ class ProxyGroup(Frame):
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, parent, title, rfunc, wfunc):
|
def __init__(self, parent, title, rfunc, wfunc):
|
||||||
Frame.__init__(self, parent)
|
Frame.__init__(
|
||||||
self.size_hint_weight = EXPAND_HORIZ
|
self, parent, size_hint_weight=EXPAND_HORIZ,
|
||||||
self.size_hint_align = FILL_HORIZ
|
size_hint_align=FILL_HORIZ, text=title
|
||||||
self.text = title
|
)
|
||||||
|
|
||||||
t = Table(self, homogeneous=True, padding=(3,3))
|
t = Table(
|
||||||
t.size_hint_weight = EXPAND_HORIZ
|
self, homogeneous=True, padding=(3, 3),
|
||||||
t.size_hint_align = FILL_HORIZ
|
size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ
|
||||||
|
)
|
||||||
t.show()
|
t.show()
|
||||||
|
|
||||||
l = Label(self, text="Proxy type")
|
l = Label(self, text="Proxy type", size_hint_align=ALIGN_LEFT)
|
||||||
l.size_hint_align = 0.0, 0.5
|
|
||||||
l.show()
|
l.show()
|
||||||
ptype = Hoversel(parent)
|
ptype = Hoversel(parent, size_hint_align=FILL_HORIZ)
|
||||||
ptype.size_hint_align = -1.0, 0.5
|
|
||||||
ptype.text = rfunc().type.name
|
ptype.text = rfunc().type.name
|
||||||
for n in self.proxy_types.iterkeys():
|
for n in self.proxy_types.iterkeys():
|
||||||
ptype.item_add(n, callback=lambda x, y, z=n: ptype.text_set(z))
|
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(l, 0, 0, 1, 1)
|
||||||
t.pack(ptype, 1, 0, 1, 1)
|
t.pack(ptype, 1, 0, 1, 1)
|
||||||
|
|
||||||
l = Label(self, text="Hostname")
|
l = Label(self, text="Hostname", size_hint_align=ALIGN_LEFT)
|
||||||
l.size_hint_align = 0.0, 0.5
|
|
||||||
l.show()
|
l.show()
|
||||||
phost = Entry(parent)
|
phost = Entry(
|
||||||
phost.size_hint_weight = EXPAND_HORIZ
|
parent, size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ,
|
||||||
phost.size_hint_align = FILL_HORIZ
|
single_line=True, scrollable=True
|
||||||
phost.single_line = True
|
)
|
||||||
phost.scrollable = True
|
|
||||||
phost.entry = rfunc().hostname
|
phost.entry = rfunc().hostname
|
||||||
phost.show()
|
phost.show()
|
||||||
t.pack(l, 0, 1, 1, 1)
|
t.pack(l, 0, 1, 1, 1)
|
||||||
t.pack(phost, 1, 1, 1, 1)
|
t.pack(phost, 1, 1, 1, 1)
|
||||||
|
|
||||||
l = Label(self, text="Port")
|
l = Label(self, text="Port", size_hint_align=ALIGN_LEFT)
|
||||||
l.size_hint_align = 0.0, 0.5
|
|
||||||
l.show()
|
l.show()
|
||||||
pport = Spinner(parent)
|
pport = Spinner(parent, size_hint_align=FILL_HORIZ, min_max=(0, 65535))
|
||||||
pport.size_hint_align = -1.0, 0.5
|
|
||||||
pport.min_max = 0, 65535
|
|
||||||
pport.value = rfunc().port
|
pport.value = rfunc().port
|
||||||
pport.show()
|
pport.show()
|
||||||
t.pack(l, 0, 2, 1, 1)
|
t.pack(l, 0, 2, 1, 1)
|
||||||
t.pack(pport, 1, 2, 1, 1)
|
t.pack(pport, 1, 2, 1, 1)
|
||||||
|
|
||||||
l = Label(self, text="Username")
|
l = Label(self, text="Username", size_hint_align=ALIGN_LEFT)
|
||||||
l.size_hint_align = 0.0, 0.5
|
|
||||||
l.show()
|
l.show()
|
||||||
puser = Entry(parent)
|
puser = Entry(
|
||||||
puser.size_hint_weight = EXPAND_HORIZ
|
parent, size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ,
|
||||||
puser.size_hint_align = FILL_HORIZ
|
single_line=True, scrollable=True
|
||||||
puser.single_line = True
|
)
|
||||||
puser.scrollable = True
|
|
||||||
puser.entry = rfunc().username
|
puser.entry = rfunc().username
|
||||||
puser.show()
|
puser.show()
|
||||||
t.pack(l, 0, 3, 1, 1)
|
t.pack(l, 0, 3, 1, 1)
|
||||||
t.pack(puser, 1, 3, 1, 1)
|
t.pack(puser, 1, 3, 1, 1)
|
||||||
|
|
||||||
l = Label(self, text="Password")
|
l = Label(self, text="Password", size_hint_align=ALIGN_LEFT)
|
||||||
l.size_hint_align = 0.0, 0.5
|
|
||||||
l.show()
|
l.show()
|
||||||
ppass = Entry(parent)
|
ppass = Entry(
|
||||||
ppass.size_hint_weight = EXPAND_HORIZ
|
parent, size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ,
|
||||||
ppass.size_hint_align = FILL_HORIZ
|
single_line=True, scrollable=True, password=True
|
||||||
ppass.single_line = True
|
)
|
||||||
ppass.scrollable = True
|
|
||||||
ppass.password = True
|
|
||||||
ppass.entry = rfunc().password
|
ppass.entry = rfunc().password
|
||||||
ppass.show()
|
ppass.show()
|
||||||
t.pack(l, 0, 4, 1, 1)
|
t.pack(l, 0, 4, 1, 1)
|
||||||
|
@ -342,45 +348,52 @@ class ProxyGroup(Frame):
|
||||||
|
|
||||||
wfunc(p)
|
wfunc(p)
|
||||||
|
|
||||||
|
|
||||||
class EncryptionSettings(Frame):
|
class EncryptionSettings(Frame):
|
||||||
|
|
||||||
def __init__(self, parent, session):
|
def __init__(self, parent, session):
|
||||||
self.session = session
|
self.session = session
|
||||||
|
|
||||||
Frame.__init__(self, parent)
|
Frame.__init__(self, parent)
|
||||||
self.size_hint_align = -1.0, 0.0
|
self.size_hint_align = FILL_HORIZ
|
||||||
self.text = "Encryption settings"
|
self.text = "Encryption settings"
|
||||||
|
|
||||||
pes = self.pes = session.get_pe_settings()
|
pes = self.pes = session.get_pe_settings()
|
||||||
|
|
||||||
b = Box(parent)
|
b = Box(parent)
|
||||||
|
|
||||||
enc_values = lt.enc_policy.disabled, lt.enc_policy.enabled, lt.enc_policy.forced
|
enc_values = lt.enc_policy.disabled, lt.enc_policy.enabled, \
|
||||||
enc_levels = lt.enc_level.plaintext, lt.enc_level.rc4, lt.enc_level.both
|
lt.enc_policy.forced
|
||||||
|
enc_levels = lt.enc_level.plaintext, lt.enc_level.rc4, \
|
||||||
|
lt.enc_level.both
|
||||||
|
|
||||||
inc = self.inc = ActSWithLabel(parent,
|
inc = self.inc = ActSWithLabel(
|
||||||
"Incoming encryption", enc_values, pes.in_enc_policy)
|
parent, "Incoming encryption", enc_values, pes.in_enc_policy
|
||||||
|
)
|
||||||
b.pack_end(inc)
|
b.pack_end(inc)
|
||||||
inc.show()
|
inc.show()
|
||||||
|
|
||||||
out = self.out = ActSWithLabel(parent,
|
out = self.out = ActSWithLabel(
|
||||||
"Outgoing encryption", enc_values, pes.out_enc_policy)
|
parent, "Outgoing encryption", enc_values, pes.out_enc_policy
|
||||||
|
)
|
||||||
b.pack_end(out)
|
b.pack_end(out)
|
||||||
out.show()
|
out.show()
|
||||||
|
|
||||||
lvl = self.lvl = ActSWithLabel(parent,
|
lvl = self.lvl = ActSWithLabel(
|
||||||
"Allowed encryption level", enc_levels, pes.allowed_enc_level)
|
parent, "Allowed encryption level", enc_levels,
|
||||||
|
pes.allowed_enc_level
|
||||||
|
)
|
||||||
b.pack_end(lvl)
|
b.pack_end(lvl)
|
||||||
lvl.show()
|
lvl.show()
|
||||||
|
|
||||||
prf = self.prf = Check(parent)
|
prf = self.prf = Check(
|
||||||
prf.style = "toggle"
|
parent, style="toggle", text="Prefer RC4 ecryption",
|
||||||
prf.text = "Prefer RC4 ecryption"
|
state=pes.prefer_rc4
|
||||||
prf.state = pes.prefer_rc4
|
)
|
||||||
b.pack_end(prf)
|
b.pack_end(prf)
|
||||||
prf.show()
|
prf.show()
|
||||||
|
|
||||||
a_btn = Button(parent)
|
a_btn = Button(parent, text="Apply")
|
||||||
a_btn.text = "Apply"
|
|
||||||
a_btn.callback_clicked_add(self.apply)
|
a_btn.callback_clicked_add(self.apply)
|
||||||
b.pack_end(a_btn)
|
b.pack_end(a_btn)
|
||||||
a_btn.show()
|
a_btn.show()
|
||||||
|
@ -389,17 +402,19 @@ class EncryptionSettings(Frame):
|
||||||
self.content = b
|
self.content = b
|
||||||
|
|
||||||
def apply(self, btn):
|
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.in_enc_policy = self.inc.get_value()
|
||||||
self.pes.out_enc_policy = self.out.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.allowed_enc_level = self.lvl.get_value()
|
||||||
self.pes.prefer_rc4 = self.prf.state
|
self.pes.prefer_rc4 = self.prf.state
|
||||||
|
|
||||||
self.session.set_pe_settings(self.pes)
|
self.session.set_pe_settings(self.pes)
|
||||||
|
|
||||||
|
|
||||||
class ActSWithLabel(Box):
|
class ActSWithLabel(Box):
|
||||||
|
|
||||||
def __init__(self, parent, label_text, values, initial_value):
|
def __init__(self, parent, label_text, values, initial_value):
|
||||||
Box.__init__(self, parent)
|
Box.__init__(self, parent)
|
||||||
|
|
||||||
|
@ -410,16 +425,16 @@ class ActSWithLabel(Box):
|
||||||
}
|
}
|
||||||
|
|
||||||
self.horizontal = True
|
self.horizontal = True
|
||||||
self.size_hint_align = -1.0, 0.0
|
self.size_hint_align = FILL_HORIZ
|
||||||
self.size_hint_weight = 1.0, 0.0
|
self.size_hint_weight = EXPAND_HORIZ
|
||||||
|
|
||||||
l = Label(parent)
|
l = Label(parent)
|
||||||
l.text = label_text
|
l.text = label_text
|
||||||
l.show()
|
l.show()
|
||||||
w = self.w = Actionslider(parent)
|
w = self.w = Actionslider(parent)
|
||||||
w.magnet_pos = ELM_ACTIONSLIDER_ALL
|
w.magnet_pos = ELM_ACTIONSLIDER_ALL
|
||||||
w.size_hint_align = -1.0, 0.0
|
w.size_hint_align = FILL_HORIZ
|
||||||
w.size_hint_weight = 1.0, 0.0
|
w.size_hint_weight = EXPAND_HORIZ
|
||||||
w.show()
|
w.show()
|
||||||
|
|
||||||
parts = "left", "center", "right"
|
parts = "left", "center", "right"
|
||||||
|
@ -434,23 +449,27 @@ class ActSWithLabel(Box):
|
||||||
def get_value(self):
|
def get_value(self):
|
||||||
return self.vd[self.w.indicator_pos]
|
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
|
self.session = session
|
||||||
|
|
||||||
widgets = {}
|
widgets = {}
|
||||||
|
|
||||||
elm_conf = Configuration()
|
#elm_conf = Configuration()
|
||||||
|
#scale = elm_conf.scale
|
||||||
|
|
||||||
s = session.settings()
|
s = session.settings()
|
||||||
|
|
||||||
t = Table(self, padding=(5,5), homogeneous=True,
|
t = Table(
|
||||||
size_hint_align=FILL_BOTH)
|
self, padding=(5, 5), homogeneous=True, size_hint_align=FILL_BOTH
|
||||||
|
)
|
||||||
self.box.pack_end(t)
|
self.box.pack_end(t)
|
||||||
t.show()
|
t.show()
|
||||||
|
|
||||||
|
@ -459,24 +478,63 @@ class PreferencesSession(PreferencesDialog):
|
||||||
INT_MIN = -2147483648
|
INT_MIN = -2147483648
|
||||||
INT_MAX = 2147483647
|
INT_MAX = 2147483647
|
||||||
|
|
||||||
scale = elm_conf.scale
|
import time
|
||||||
|
t1 = time.time()
|
||||||
|
|
||||||
for k in dir(s):
|
for k in dir(s):
|
||||||
if k.startswith("__"): continue
|
if k.startswith("__"):
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
|
if k == "peer_tos" or k == "outgoing_ports":
|
||||||
|
# XXX: these don't have a C++ -> Python equivalent.
|
||||||
|
continue
|
||||||
a = getattr(s, k)
|
a = getattr(s, k)
|
||||||
if isinstance(a, lt.disk_cache_algo_t):
|
if isinstance(a, lt.disk_cache_algo_t):
|
||||||
w = Spinner(t)
|
w = Spinner(t)
|
||||||
w.size_hint_align = FILL_HORIZ
|
w.size_hint_align = FILL_HORIZ
|
||||||
# XXX: lt-rb python bindings don't have all values.
|
w.min_max = 0, len(lt.disk_cache_algo_t.values) - 1
|
||||||
w.min_max = 0, 2 #len(lt.disk_cache_algo_t.values.keys())
|
|
||||||
for name, val in lt.disk_cache_algo_t.names.items():
|
for name, val in lt.disk_cache_algo_t.names.items():
|
||||||
w.special_value_add(val, name)
|
w.special_value_add(val, name)
|
||||||
w.value = a
|
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):
|
elif isinstance(a, bool):
|
||||||
w = Check(t)
|
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
|
w.state = a
|
||||||
elif isinstance(a, int):
|
elif isinstance(a, int):
|
||||||
w = Spinner(t)
|
w = Spinner(t)
|
||||||
|
@ -493,10 +551,6 @@ class PreferencesSession(PreferencesDialog):
|
||||||
else:
|
else:
|
||||||
w.min_max = 0.0, 20.0
|
w.min_max = 0.0, 20.0
|
||||||
w.value = a
|
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":
|
elif k == "user_agent":
|
||||||
w = Entry(t)
|
w = Entry(t)
|
||||||
w.size_hint_align = 1.0, 0.0
|
w.size_hint_align = 1.0, 0.0
|
||||||
|
@ -505,15 +559,16 @@ class PreferencesSession(PreferencesDialog):
|
||||||
w.editable = False
|
w.editable = False
|
||||||
w.entry = cgi.escape(a)
|
w.entry = cgi.escape(a)
|
||||||
else:
|
else:
|
||||||
|
log.debug("Using string for %s type %s", k, type(a))
|
||||||
w = Entry(t)
|
w = Entry(t)
|
||||||
w.part_text_set("guide", "Enter here")
|
|
||||||
w.size_hint_align = FILL_HORIZ
|
w.size_hint_align = FILL_HORIZ
|
||||||
w.size_hint_weight = EXPAND_HORIZ
|
w.size_hint_weight = EXPAND_HORIZ
|
||||||
w.single_line = True
|
w.single_line = True
|
||||||
w.entry = cgi.escape(a)
|
w.entry = cgi.escape(a)
|
||||||
|
w.part_text_set("guide", "Enter here")
|
||||||
l = Label(t)
|
l = Label(t)
|
||||||
l.text = k.replace("_", " ").capitalize()
|
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.size_hint_weight = EXPAND_HORIZ
|
||||||
l.show()
|
l.show()
|
||||||
t.pack(l, 0, i, 1, 1)
|
t.pack(l, 0, i, 1, 1)
|
||||||
|
@ -522,8 +577,11 @@ class PreferencesSession(PreferencesDialog):
|
||||||
w.show()
|
w.show()
|
||||||
widgets[k] = w
|
widgets[k] = w
|
||||||
i += 1
|
i += 1
|
||||||
except TypeError:
|
except TypeError as e:
|
||||||
pass #print("Error {}".format(k))
|
log.debug("Error in {}: {}".format(k, e))
|
||||||
|
|
||||||
|
t2 = time.time()
|
||||||
|
log.debug("Session settings time: %f", t2-t1)
|
||||||
|
|
||||||
save_btn = Button(self)
|
save_btn = Button(self)
|
||||||
save_btn.text = "Apply session settings"
|
save_btn.text = "Apply session settings"
|
||||||
|
@ -552,69 +610,11 @@ class PreferencesSession(PreferencesDialog):
|
||||||
setattr(s, k, v)
|
setattr(s, k, v)
|
||||||
|
|
||||||
session.set_settings(s)
|
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):
|
class RangeSpinners(Box):
|
||||||
|
|
||||||
def __init__(self, parent, low, high, minim, maxim):
|
def __init__(self, parent, low, high, minim, maxim):
|
||||||
|
|
||||||
Box.__init__(self, parent)
|
Box.__init__(self, parent)
|
||||||
|
@ -638,7 +638,9 @@ class RangeSpinners(Box):
|
||||||
self.pack_end(h)
|
self.pack_end(h)
|
||||||
h.show()
|
h.show()
|
||||||
|
|
||||||
|
|
||||||
class Limits(Frame):
|
class Limits(Frame):
|
||||||
|
|
||||||
def __init__(self, parent, session):
|
def __init__(self, parent, session):
|
||||||
Frame.__init__(self, parent)
|
Frame.__init__(self, parent)
|
||||||
|
|
||||||
|
@ -646,14 +648,22 @@ class Limits(Frame):
|
||||||
self.size_hint_align = FILL_HORIZ
|
self.size_hint_align = FILL_HORIZ
|
||||||
|
|
||||||
base = 1024
|
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)
|
t = Table(parent)
|
||||||
for r, values in enumerate((
|
for r, values in enumerate((
|
||||||
("Upload limit", session.upload_rate_limit, session.set_upload_rate_limit),
|
("Upload limit",
|
||||||
("Download limit", session.download_rate_limit, session.set_download_rate_limit),
|
session.upload_rate_limit,
|
||||||
("Upload limit for local connections", session.local_upload_rate_limit, session.set_local_upload_rate_limit),
|
session.set_upload_rate_limit),
|
||||||
("Download limit for local connections", session.local_download_rate_limit, session.set_local_download_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
|
title, rfunc, wfunc = values
|
||||||
|
|
||||||
|
@ -673,6 +683,12 @@ class Limits(Frame):
|
||||||
|
|
||||||
self.content = t
|
self.content = t
|
||||||
|
|
||||||
|
|
||||||
|
class FsButton(Fileselector, FileselectorButton):
|
||||||
|
|
||||||
|
def __init__(self, parent, *args, **kwargs):
|
||||||
|
FileselectorButton.__init__(self, parent, *args, **kwargs)
|
||||||
|
|
||||||
# TODO:
|
# TODO:
|
||||||
# max uploads?, max conns?, max half open conns?
|
# max uploads?, max conns?, max half open conns?
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#
|
#
|
||||||
# Epour - A bittorrent client using EFL and libtorrent
|
# 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
|
# 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
|
# 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
|
import libtorrent as lt
|
||||||
|
|
||||||
try:
|
from efl import ecore
|
||||||
from efl import ecore
|
from efl.evas import EVAS_HINT_EXPAND, EVAS_HINT_FILL
|
||||||
from efl.elementary.genlist import Genlist, GenlistItemClass
|
from efl.elementary.genlist import Genlist, GenlistItemClass
|
||||||
from efl.elementary.innerwindow import InnerWindow
|
from efl.elementary.window import StandardWindow
|
||||||
from efl.elementary.button import Button
|
from efl.elementary.innerwindow import InnerWindow
|
||||||
from efl.elementary.box import Box
|
from efl.elementary.button import Button
|
||||||
from efl.elementary.hoversel import Hoversel
|
from efl.elementary.box import Box
|
||||||
from efl.elementary.check import Check
|
from efl.elementary.check import Check
|
||||||
from efl.elementary.label import Label, ELM_WRAP_CHAR, ELM_WRAP_WORD
|
from efl.elementary.label import Label, ELM_WRAP_CHAR
|
||||||
from efl.elementary.entry import Entry
|
from efl.elementary.entry import Entry
|
||||||
from efl.elementary.naviframe import Naviframe
|
from efl.elementary.frame import Frame
|
||||||
from efl.elementary.frame import Frame
|
from efl.elementary.object import ELM_SEL_FORMAT_TEXT, \
|
||||||
from efl.elementary.object import ELM_SEL_FORMAT_TEXT, ELM_SEL_TYPE_CLIPBOARD
|
ELM_SEL_TYPE_CLIPBOARD
|
||||||
except ImportError:
|
from efl.elementary.table import Table
|
||||||
import ecore
|
|
||||||
from elementary import Genlist, GenlistItemClass, InnerWindow, Button, \
|
|
||||||
Box, Hoversel, Check, Label, ELM_WRAP_CHAR, ELM_WRAP_WORD, Entry, \
|
|
||||||
Naviframe, Frame
|
|
||||||
|
|
||||||
from intrepr import intrepr
|
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):
|
def __init__(self, parent, h):
|
||||||
if not h.is_valid():
|
if not h.is_valid():
|
||||||
Information(parent.win, "Invalid torrent handle.")
|
Information(parent.win, "Invalid torrent handle.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not h.has_metadata():
|
InnerWindow.__init__(self, parent)
|
||||||
Information(parent.win, "Torrent contains no metadata.")
|
|
||||||
return
|
|
||||||
|
|
||||||
i = h.get_torrent_info()
|
|
||||||
|
|
||||||
InnerWindow.__init__(self, parent.win)
|
|
||||||
|
|
||||||
box = Box(self)
|
box = Box(self)
|
||||||
box.size_hint_align = -1.0, -1.0
|
box.size_hint_align = FILL_BOTH
|
||||||
box.size_hint_weight = 1.0, 1.0
|
box.size_hint_weight = EXPAND_BOTH
|
||||||
|
|
||||||
tname = Label(self)
|
tname = Label(self)
|
||||||
tname.size_hint_align = -1.0, 0.5
|
tname.size_hint_align = FILL_HORIZ
|
||||||
tname.line_wrap = ELM_WRAP_CHAR
|
tname.line_wrap = ELM_WRAP_CHAR
|
||||||
tname.ellipsis = True
|
tname.ellipsis = True
|
||||||
tname.text = "{}".format(cgi.escape(i.name()))
|
tname.text = "{}".format(cgi.escape(h.name()))
|
||||||
|
tname.scale = 2.0
|
||||||
tname.show()
|
tname.show()
|
||||||
box.pack_end(tname)
|
box.pack_end(tname)
|
||||||
|
|
||||||
for func in i.comment, i.creation_date, i.creator:
|
if h.has_metadata():
|
||||||
try:
|
ti = TorrentInfo(self, h, size_hint_align=FILL_HORIZ)
|
||||||
w = func()
|
box.pack_end(ti)
|
||||||
except Exception as e:
|
ti.show()
|
||||||
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()
|
|
||||||
|
|
||||||
magnet_uri = lt.make_magnet_uri(h)
|
magnet_uri = lt.make_magnet_uri(h)
|
||||||
|
|
||||||
f = Frame(self)
|
f = Frame(self)
|
||||||
f.size_hint_align = -1.0, 0.0
|
f.size_hint_align = FILL_HORIZ
|
||||||
f.text = "Magnet URI"
|
f.text = "Magnet URI"
|
||||||
me_box = Box(self)
|
me_box = Box(f)
|
||||||
me_box.horizontal = True
|
me_box.horizontal = True
|
||||||
me = Entry(self)
|
me = Entry(me_box)
|
||||||
me.size_hint_align = -1.0, 0.0
|
me.size_hint_align = FILL_HORIZ
|
||||||
me.size_hint_weight = 1.0, 0.0
|
me.size_hint_weight = EXPAND_HORIZ
|
||||||
#me.editable = False
|
me.editable = False
|
||||||
me.entry = magnet_uri
|
me.entry = magnet_uri
|
||||||
me_box.pack_end(me)
|
me_box.pack_end(me)
|
||||||
me.show()
|
me.show()
|
||||||
me_btn = Button(self)
|
me_btn = Button(me_box)
|
||||||
me_btn.text = "Copy"
|
me_btn.text = "Copy"
|
||||||
if hasattr(me, "cnp_selection_set"):
|
|
||||||
me_btn.callback_clicked_add(
|
me_btn.callback_clicked_add(
|
||||||
lambda x: me.top_widget.cnp_selection_set(
|
lambda x: me.top_widget.cnp_selection_set(
|
||||||
ELM_SEL_TYPE_CLIPBOARD, ELM_SEL_FORMAT_TEXT, me.text
|
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_btn.show()
|
||||||
me_box.pack_end(me_btn)
|
me_box.pack_end(me_btn)
|
||||||
me_box.show()
|
me_box.show()
|
||||||
|
@ -136,38 +106,29 @@ class TorrentInfo(InnerWindow):
|
||||||
f.show()
|
f.show()
|
||||||
box.pack_end(f)
|
box.pack_end(f)
|
||||||
|
|
||||||
|
xbtn = Button(box)
|
||||||
fl_btn = Button(self)
|
|
||||||
fl_btn.text = "Files ->"
|
|
||||||
fl_btn.callback_clicked_add(self.file_list_cb, h)
|
|
||||||
|
|
||||||
xbtn = Button(self)
|
|
||||||
xbtn.text_set("Close")
|
xbtn.text_set("Close")
|
||||||
xbtn.callback_clicked_add(lambda x: self.delete())
|
xbtn.callback_clicked_add(lambda x: self.delete())
|
||||||
|
box.pack_end(xbtn)
|
||||||
|
xbtn.show()
|
||||||
for w in tpriv, fl_btn, xbtn:
|
|
||||||
w.show()
|
|
||||||
box.pack_end(w)
|
|
||||||
|
|
||||||
box.show()
|
box.show()
|
||||||
|
|
||||||
nf = self.nf = Naviframe(self)
|
self.content = box
|
||||||
nf.item_simple_push(box)
|
|
||||||
|
|
||||||
self.content_set(nf)
|
|
||||||
self.activate()
|
self.activate()
|
||||||
|
|
||||||
def file_list_cb(self, btn, h):
|
|
||||||
self.nf.item_simple_push(TorrentFiles(self.nf, h))
|
|
||||||
|
|
||||||
class TorrentFiles(Box):
|
class TorrentFiles(StandardWindow):
|
||||||
def __init__(self, parent, h):
|
def __init__(self, h):
|
||||||
Box.__init__(self, parent)
|
StandardWindow.__init__(self, "epour.files", "Files", size=(600, 400))
|
||||||
|
self.callback_delete_request_add(lambda x: self.delete())
|
||||||
|
|
||||||
filelist = Genlist(self)
|
box = Box(self, size_hint_weight=EXPAND_BOTH)
|
||||||
filelist.size_hint_align = -1.0, -1.0
|
self.resize_object_add(box)
|
||||||
filelist.size_hint_weight = 1.0, 1.0
|
|
||||||
|
filelist = Genlist(box)
|
||||||
|
filelist.size_hint_align = FILL_BOTH
|
||||||
|
filelist.size_hint_weight = EXPAND_BOTH
|
||||||
|
|
||||||
self.populate(filelist, h)
|
self.populate(filelist, h)
|
||||||
|
|
||||||
|
@ -180,24 +141,25 @@ class TorrentFiles(Box):
|
||||||
sel_all.show()
|
sel_all.show()
|
||||||
|
|
||||||
sel_none = Button(self)
|
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.callback_clicked_add(self.select_all_cb, filelist, h, False)
|
||||||
sel_none.show()
|
sel_none.show()
|
||||||
|
|
||||||
xbtn = Button(self)
|
# xbtn = Button(self)
|
||||||
xbtn.text = "Close"
|
# xbtn.text = "Close"
|
||||||
xbtn.callback_clicked_add(lambda x: parent.item_pop())
|
# xbtn.callback_clicked_add(lambda x: parent.item_pop())
|
||||||
xbtn.show()
|
# xbtn.show()
|
||||||
|
|
||||||
btn_box = Box(self)
|
btn_box = Box(self)
|
||||||
btn_box.horizontal = True
|
btn_box.horizontal = True
|
||||||
btn_box.pack_end(sel_all)
|
btn_box.pack_end(sel_all)
|
||||||
btn_box.pack_end(sel_none)
|
btn_box.pack_end(sel_none)
|
||||||
btn_box.pack_end(xbtn)
|
#btn_box.pack_end(xbtn)
|
||||||
btn_box.show()
|
btn_box.show()
|
||||||
|
|
||||||
self.pack_end(filelist)
|
box.pack_end(filelist)
|
||||||
self.pack_end(btn_box)
|
box.pack_end(btn_box)
|
||||||
|
box.show()
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
def select_all_cb(self, btn, filelist, h, all_selected=True):
|
def select_all_cb(self, btn, filelist, h, all_selected=True):
|
||||||
|
@ -232,6 +194,7 @@ class TorrentFiles(Box):
|
||||||
else:
|
else:
|
||||||
os.startfile(path)
|
os.startfile(path)
|
||||||
|
|
||||||
|
|
||||||
class FileSelectionClass(GenlistItemClass):
|
class FileSelectionClass(GenlistItemClass):
|
||||||
def text_get(self, obj, part, data):
|
def text_get(self, obj, part, data):
|
||||||
file_entry, progress, n, h = data
|
file_entry, progress, n, h = data
|
||||||
|
@ -256,3 +219,58 @@ class FileSelectionClass(GenlistItemClass):
|
||||||
priorities[n] = int(ck.state)
|
priorities[n] = int(ck.state)
|
||||||
h.prioritize_files(priorities)
|
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:
|
import cgi
|
||||||
from efl import elementary
|
import logging
|
||||||
except ImportError:
|
from datetime import timedelta
|
||||||
import elementary
|
|
||||||
|
|
||||||
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
|
# 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
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -22,51 +21,49 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import mimetypes
|
import mimetypes
|
||||||
from ConfigParser import SafeConfigParser
|
|
||||||
import cPickle
|
|
||||||
import urlparse
|
import urlparse
|
||||||
import urllib
|
import urllib
|
||||||
import HTMLParser
|
import logging
|
||||||
|
|
||||||
import shutil
|
import shutil
|
||||||
import cgi
|
import cPickle
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
import libtorrent as lt
|
import libtorrent as lt
|
||||||
|
|
||||||
try:
|
from efl.ecore import Timer
|
||||||
from efl.ecore import Timer
|
|
||||||
except:
|
|
||||||
from 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):
|
class Session(lt.session):
|
||||||
def __init__(self, parent):
|
def __init__(self, conf):
|
||||||
self.parent = parent
|
self.conf = conf
|
||||||
|
|
||||||
self.log = logging.getLogger("epour.session")
|
self.log = logging.getLogger("epour.session")
|
||||||
|
|
||||||
from Globals import version
|
from Globals import version
|
||||||
ver_ints = []
|
ver_ints = []
|
||||||
for s in version.split("."):
|
for s in version.split("."):
|
||||||
ver_ints.append(int(s))
|
ver_ints.append(int(s))
|
||||||
|
ver_ints.append(0)
|
||||||
|
|
||||||
fp = lt.fingerprint("EP", *ver_ints)
|
fp = lt.fingerprint("EP", *ver_ints)
|
||||||
self.log.debug("peer-id: {}".format(fp))
|
self.log.debug("peer-id: {}".format(fp))
|
||||||
|
|
||||||
lt.session.__init__(
|
lt.session.__init__(
|
||||||
self,
|
self,
|
||||||
fingerprint = fp,
|
fingerprint=fp,
|
||||||
# flags = \
|
# flags=
|
||||||
#lt.session_flags_t.add_default_plugins | \
|
#lt.session_flags_t.add_default_plugins|
|
||||||
#lt.session_flags_t.start_default_features
|
#lt.session_flags_t.start_default_features
|
||||||
)
|
)
|
||||||
|
|
||||||
self.log.info("Session started")
|
self.log.info("Session started")
|
||||||
|
|
||||||
self.torrents = {}
|
self.torrents = OrderedDict()
|
||||||
|
|
||||||
#sdpipsdtsppe
|
#sdpipsdtsppe
|
||||||
#theprtertoer
|
#theprtertoer
|
||||||
|
@ -78,8 +75,6 @@ class Session(lt.session):
|
||||||
mask = 0b000001000001
|
mask = 0b000001000001
|
||||||
self.set_alert_mask(mask)
|
self.set_alert_mask(mask)
|
||||||
|
|
||||||
conf = self.conf = self.setup_conf()
|
|
||||||
|
|
||||||
self.listen_on(
|
self.listen_on(
|
||||||
conf.getint("Settings", "listen_low"),
|
conf.getint("Settings", "listen_low"),
|
||||||
conf.getint("Settings", "listen_high")
|
conf.getint("Settings", "listen_high")
|
||||||
|
@ -87,66 +82,44 @@ class Session(lt.session):
|
||||||
|
|
||||||
self.alert_manager = AlertManager(self)
|
self.alert_manager = AlertManager(self)
|
||||||
self.alert_manager.callback_add(
|
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
|
h = a.handle
|
||||||
ihash = str(h.info_hash())
|
ihash = str(h.info_hash())
|
||||||
self.log.debug("Metadata received.")
|
self.log.debug("Metadata received.")
|
||||||
t_path = self.write_torrent(h)
|
self.write_torrent(h)
|
||||||
self.torrents[ihash] = t_path
|
self.torrents[ihash]["ti"] = h.get_torrent_info()
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def load_state(self):
|
def load_state(self):
|
||||||
try:
|
try:
|
||||||
with open(os.path.join(data_dir, "session"), 'rb') as f:
|
with open(os.path.join(data_dir, "session"), 'rb') as f:
|
||||||
state = lt.bdecode(f.read())
|
state = lt.bdecode(f.read())
|
||||||
lt.session.load_state(self, state)
|
lt.session.load_state(self, state)
|
||||||
except:
|
except Exception as e:
|
||||||
self.log.debug("Could not load previous session state.")
|
self.log.debug("Could not load previous session state.")
|
||||||
|
self.log.debug(e)
|
||||||
|
else:
|
||||||
|
self.log.info("Session restored from disk.")
|
||||||
|
|
||||||
settings = self.settings()
|
settings = self.settings()
|
||||||
from Globals import version
|
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)
|
self.set_settings(settings)
|
||||||
|
|
||||||
def save_state(self):
|
def save_state(self):
|
||||||
|
@ -169,29 +142,58 @@ class Session(lt.session):
|
||||||
self.log.warning("Could not open the list of torrents.")
|
self.log.warning("Could not open the list of torrents.")
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
paths = cPickle.load(pkl_file)
|
torrents = cPickle.load(pkl_file)
|
||||||
except EOFError:
|
except EOFError:
|
||||||
self.log.exception("Opening the list of torrents failed.")
|
self.log.exception("Opening the list of torrents failed.")
|
||||||
else:
|
else:
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
"List of torrents opened, "
|
"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:
|
try:
|
||||||
self.log.debug("Adding {}".format(v))
|
if isinstance(t, dict):
|
||||||
self.add_torrent(v)
|
for k, v in t.items():
|
||||||
except:
|
if k == "ti":
|
||||||
self.log.exception(
|
with open(v, "rb") as f:
|
||||||
"Restoring torrent {0} failed".format(v)
|
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:
|
finally:
|
||||||
pkl_file.close()
|
pkl_file.close()
|
||||||
|
|
||||||
def save_torrents(self):
|
def save_torrents(self):
|
||||||
self.log.debug("Saving {} torrents.".format(len(self.torrents)))
|
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:
|
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.")
|
self.log.debug("List of torrents saved.")
|
||||||
|
|
||||||
|
@ -201,92 +203,30 @@ class Session(lt.session):
|
||||||
continue
|
continue
|
||||||
data = lt.bencode(h.write_resume_data())
|
data = lt.bencode(h.write_resume_data())
|
||||||
with open(os.path.join(
|
with open(os.path.join(
|
||||||
data_dir, '{}.fastresume'.format(h.info_hash())
|
data_dir, str(h.info_hash()) + ".fastresume"
|
||||||
), 'wb'
|
), 'wb') as f:
|
||||||
) as f:
|
|
||||||
f.write(data)
|
f.write(data)
|
||||||
|
|
||||||
self.log.debug("Fast resume data saved.")
|
self.log.debug("Fast resume data saved.")
|
||||||
|
|
||||||
def add_torrent(self, t_uri):
|
def write_torrent(self, h):
|
||||||
if not t_uri:
|
if h is None:
|
||||||
|
self.log.debug("Tried to write torrent while handle was empty.")
|
||||||
return
|
return
|
||||||
|
|
||||||
storage_path = self.conf.get("Settings", "storage_path")
|
t_info = h.get_torrent_info()
|
||||||
|
|
||||||
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())
|
ihash = str(h.info_hash())
|
||||||
|
|
||||||
new_uri = os.path.join(
|
self.log.debug("Writing torrent file {}".format(ihash))
|
||||||
data_dir, "{}.torrent".format(ihash)
|
|
||||||
)
|
|
||||||
|
|
||||||
if t_uri == new_uri:
|
md = lt.bdecode(t_info.metadata())
|
||||||
pass
|
t = {}
|
||||||
else:
|
t["info"] = md
|
||||||
shutil.copy(t_uri, new_uri)
|
t_path = torrent_path_get(ihash)
|
||||||
|
with open(t_path, "wb") as f:
|
||||||
|
f.write(lt.bencode(t))
|
||||||
|
|
||||||
if self.conf.getboolean("Settings", "delete_original"):
|
return t_path
|
||||||
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")
|
|
||||||
return
|
|
||||||
|
|
||||||
ihash = str(h.info_hash())
|
|
||||||
|
|
||||||
self.torrents[ihash] = t_uri
|
|
||||||
|
|
||||||
if not hasattr(lt, "torrent_added_alert"):
|
|
||||||
class torrent_added_alert(object):
|
|
||||||
def __init__(self, h):
|
|
||||||
self.handle = h
|
|
||||||
|
|
||||||
a = torrent_added_alert(h)
|
|
||||||
|
|
||||||
self.alert_manager.signal(a)
|
|
||||||
|
|
||||||
def remove_torrent(self, h, with_data=False):
|
def remove_torrent(self, h, with_data=False):
|
||||||
ihash = str(h.info_hash())
|
ihash = str(h.info_hash())
|
||||||
|
@ -294,24 +234,26 @@ class Session(lt.session):
|
||||||
fr_path = os.path.join(
|
fr_path = os.path.join(
|
||||||
data_dir, "{}.fastresume".format(ihash)
|
data_dir, "{}.fastresume".format(ihash)
|
||||||
)
|
)
|
||||||
t_path = self.torrents[ihash]
|
#t_dict = self.torrents[ihash]
|
||||||
|
|
||||||
del self.torrents[ihash]
|
del self.torrents[ihash]
|
||||||
lt.session.remove_torrent(self, h, option=with_data)
|
lt.session.remove_torrent(self, h, option=with_data)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(fr_path): pass
|
with open(fr_path):
|
||||||
|
pass
|
||||||
except IOError:
|
except IOError:
|
||||||
self.log.debug("Could not remove fast resume data.")
|
self.log.debug("Could not remove fast resume data.")
|
||||||
else:
|
else:
|
||||||
os.remove(fr_path)
|
os.remove(fr_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(t_path): pass
|
with open(torrent_path_get(ihash)):
|
||||||
|
pass
|
||||||
except IOError:
|
except IOError:
|
||||||
self.log.debug("Could not remove torrent file.")
|
self.log.debug("Could not remove torrent file.")
|
||||||
else:
|
else:
|
||||||
os.remove(t_path)
|
os.remove(torrent_path_get(ihash))
|
||||||
|
|
||||||
if not hasattr(lt, "torrent_removed_alert"):
|
if not hasattr(lt, "torrent_removed_alert"):
|
||||||
class torrent_removed_alert(object):
|
class torrent_removed_alert(object):
|
||||||
|
@ -325,29 +267,64 @@ class Session(lt.session):
|
||||||
|
|
||||||
return ihash
|
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
|
info = lt.torrent_info(t_raw)
|
||||||
# string tracker_url
|
add_dict["ti"] = info
|
||||||
# string info_hash
|
|
||||||
# string name
|
rd = None
|
||||||
# string save_path
|
try:
|
||||||
# string resume_data
|
with open(os.path.join(
|
||||||
# storage_mode_t storage_mode
|
data_dir, "{}.fastresume".format(info.info_hash())
|
||||||
# bool paused
|
), "rb"
|
||||||
# bool auto_managed
|
) as f:
|
||||||
# bool duplicate_is_error
|
rd = f.read()
|
||||||
# storage?
|
except Exception:
|
||||||
# object userdata?
|
self.log.debug("Invalid resume data")
|
||||||
# bool seed_mode
|
else:
|
||||||
# bool override_resume_data
|
add_dict["resume_data"] = rd
|
||||||
# bool upload_mode
|
|
||||||
|
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):
|
class AlertManager(object):
|
||||||
|
|
||||||
|
@ -361,7 +338,7 @@ class AlertManager(object):
|
||||||
self.timer = Timer(self.update_interval, self.update)
|
self.timer = Timer(self.update_interval, self.update)
|
||||||
|
|
||||||
def callback_add(self, alert_type, cb, *args, **kwargs):
|
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] = []
|
||||||
self.alerts[alert_type].append((cb, args, kwargs))
|
self.alerts[alert_type].append((cb, args, kwargs))
|
||||||
|
|
||||||
|
@ -373,7 +350,7 @@ class AlertManager(object):
|
||||||
def signal(self, a):
|
def signal(self, a):
|
||||||
a_name = type(a).__name__
|
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))
|
self.log.debug("No handler: {} | {}".format(a_name, a))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -386,9 +363,9 @@ class AlertManager(object):
|
||||||
def update(self):
|
def update(self):
|
||||||
while 1:
|
while 1:
|
||||||
a = self.session.pop_alert()
|
a = self.session.pop_alert()
|
||||||
if not a: break
|
if not a:
|
||||||
|
break
|
||||||
|
|
||||||
self.signal(a)
|
self.signal(a)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
14
setup.py
14
setup.py
|
@ -4,24 +4,24 @@ from DistUtilsExtra.auto import setup
|
||||||
|
|
||||||
from epour.Globals import version
|
from epour.Globals import version
|
||||||
|
|
||||||
setup(name='epour',
|
setup(
|
||||||
|
name='epour',
|
||||||
version=version,
|
version=version,
|
||||||
author='Kai Huuhko',
|
author='Kai Huuhko',
|
||||||
author_email='kai.huuhko@gmail.com',
|
author_email='kai.huuhko@gmail.com',
|
||||||
maintainer='Kai Huuhko',
|
maintainer='Kai Huuhko',
|
||||||
maintainer_email='kai.huuhko@gmail.com',
|
maintainer_email='kai.huuhko@gmail.com',
|
||||||
description='Simple torrent client',
|
description='A torrent client',
|
||||||
long_description='Epour is a simple torrent client using EFL and libtorrent.',
|
long_description=(
|
||||||
|
'Epour is a torrent client based on EFL and rb-libtorrent.'
|
||||||
|
),
|
||||||
#url='',
|
#url='',
|
||||||
#download_url='',
|
#download_url='',
|
||||||
license='GNU GPL',
|
license='GNU GPL',
|
||||||
platforms='linux',
|
platforms='linux',
|
||||||
requires=[
|
requires=[
|
||||||
'libtorrent',
|
'libtorrent',
|
||||||
'evas',
|
'efl',
|
||||||
'ecore',
|
|
||||||
'elementary',
|
|
||||||
'e_dbus',
|
|
||||||
],
|
],
|
||||||
provides=[
|
provides=[
|
||||||
'epour',
|
'epour',
|
||||||
|
|
Loading…
Reference in New Issue