From d04fdf23f539e8e2a0861980a3d25900a81aeabd Mon Sep 17 00:00:00 2001 From: Kai Huuhko Date: Mon, 30 Jun 2014 17:02:27 +0300 Subject: [PATCH] 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 --- .gitignore | 3 +- TODO | 36 +- bin/epour | 9 +- debian/control | 2 +- epour/Epour.py | 146 ----- epour/Globals.py | 2 +- epour/__init__.py | 223 ++++++++ epour/gui/LICENSE.txt | 21 - epour/gui/Main.py | 530 ------------------ epour/gui/Notify.py | 63 --- epour/gui/Preferences.py | 414 +++++++------- epour/gui/{TorrentInfo.py => TorrentProps.py} | 230 ++++---- epour/gui/TorrentSelector.py | 264 +++++++++ epour/gui/Widgets.py | 114 ++++ epour/gui/__init__.py | 524 ++++++++++++++++- epour/gui/pyperclip.py | 181 ------ epour/gui/which.py | 342 ----------- epour/session.py | 339 ++++++----- setup.py | 32 +- 19 files changed, 1660 insertions(+), 1815 deletions(-) delete mode 100755 epour/Epour.py delete mode 100644 epour/gui/LICENSE.txt delete mode 100644 epour/gui/Main.py delete mode 100644 epour/gui/Notify.py rename epour/gui/{TorrentInfo.py => TorrentProps.py} (54%) create mode 100644 epour/gui/TorrentSelector.py create mode 100644 epour/gui/Widgets.py delete mode 100644 epour/gui/pyperclip.py delete mode 100644 epour/gui/which.py diff --git a/.gitignore b/.gitignore index 647c442..0baa65f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ *.py[co] -epour.sublime-project -epour.sublime-workspace +build/ diff --git a/TODO b/TODO index 780f111..a45afe7 100644 --- a/TODO +++ b/TODO @@ -1,22 +1,26 @@ I18N: This should wait until most of the features are in and the strings are stable. - ☐ Make strings localizable - ☐ Create pot file - ☐ Generate po files + ☐ Make strings localizable + ☐ Create pot file + ☐ Generate po files Add Torrent-dialog: - ☐ Create a new class to hold a torrent and store its instances between sessions. - Migrate the old dict(ihash->torrent_file) to the new list of torrents - ☐ Dialog window - ☐ Initial paused-state - ☐ File selector - ☐ Storage path - ☐ Add preferences for initial/automatic values for the above + ✔ Use dicts to hold torrent info between sessions. @done (15:57 30.06.2014) + ✔ Migrate the old ihash->torrent_file dict to the new list of torrents @done (15:57 30.06.2014) + ✔ Dialog window @done (15:57 30.06.2014) + ✔ Options @done (15:57 30.06.2014) + ✔ File selector @done (15:57 30.06.2014) + ✔ Storage path @done (15:57 30.06.2014) + ☐ Add preferences for initial/automatic values for the above Misc: ☐ Torrent tooltips - handle.status() + Using handle.status() ☐ More tooltips in general - ☐ More D-Bus controls #epour/Epour.py@EpourDBus - ✔ Auto-paste magnet links from clipboard #epour/gui/Main.py@select_torrent @done (13-08-11 21:45) - Note: The pasted text is not filtered. - ☐ Construct and populate #epour/gui/Preferences.py@SessionSettings with an Idler - ✔ Move proxy settings to its own naviframe page and don't autocollapse the frames @done (13-08-18 18:02) + ☐ More D-Bus controls ./epour/Epour.py>EpourDBus + ☐ Auto-paste magnet links from clipboard ./epour/gui/Main.py>TorrentSelector + Remake this feature + ✘ Construct and populate ./epour/gui/Preferences.py>PreferencesSession with an Idler @cancelled (21:20 01.07.2014) + ☐ Moving finished torrents to a different folder + +___________________ +Archive: + ✔ Move proxy settings to its own naviframe page and don't autocollapse the frames @done (13-08-18 18:02) @project(Misc) diff --git a/bin/epour b/bin/epour index b367476..bbe40f2 100755 --- a/bin/epour +++ b/bin/epour @@ -1,9 +1,10 @@ -#!/usr/bin/env python +#!/usr/bin/python -import sys import logging -import epour.Epour as Epour +from epour import Epour -epour = Epour.Epour(sys.argv[1:]) +epour = Epour() +epour.gui.run() +epour.quit() logging.shutdown() diff --git a/debian/control b/debian/control index edb4e69..8081823 100644 --- a/debian/control +++ b/debian/control @@ -7,6 +7,6 @@ Standards-Version: 3.9.1 Package: epour Architecture: all -Depends: ${misc:Depends}, ${python:Depends}, python-libtorrent, python-evas, python-elementary, python-edbus, python-ecore +Depends: ${misc:Depends}, ${python:Depends}, python-libtorrent, python-efl Description: Simple torrent client Epour is a simple torrent client using EFL and libtorrent. diff --git a/epour/Epour.py b/epour/Epour.py deleted file mode 100755 index 699ac8f..0000000 --- a/epour/Epour.py +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env python2 -# -# Epour - A bittorrent client using EFL and libtorrent -# -# Copyright 2012-2013 Kai Huuhko -# -# 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() diff --git a/epour/Globals.py b/epour/Globals.py index 3dae9d5..b60a621 100644 --- a/epour/Globals.py +++ b/epour/Globals.py @@ -1,6 +1,6 @@ import os -version = "0.5.2.0" +version = "0.6.0" conf_dir = os.path.expanduser(os.path.join("~", ".config", "epour")) conf_path = os.path.join(conf_dir, "epour.conf") data_dir = os.path.expanduser(os.path.join("~", ".local", "share", "epour")) diff --git a/epour/__init__.py b/epour/__init__.py index e69de29..e88b910 100644 --- a/epour/__init__.py +++ b/epour/__init__.py @@ -0,0 +1,223 @@ +#!/usr/bin/python +# +# Epour - A bittorrent client using EFL and libtorrent +# +# Copyright 2012-2014 Kai Huuhko +# +# 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() diff --git a/epour/gui/LICENSE.txt b/epour/gui/LICENSE.txt deleted file mode 100644 index de85cd5..0000000 --- a/epour/gui/LICENSE.txt +++ /dev/null @@ -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. - diff --git a/epour/gui/Main.py b/epour/gui/Main.py deleted file mode 100644 index 17276d8..0000000 --- a/epour/gui/Main.py +++ /dev/null @@ -1,530 +0,0 @@ -# -# Epour - A bittorrent client using EFL and libtorrent -# -# Copyright 2012-2013 Kai Huuhko -# -# 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 = "Session" - 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 diff --git a/epour/gui/Notify.py b/epour/gui/Notify.py deleted file mode 100644 index 4fae70c..0000000 --- a/epour/gui/Notify.py +++ /dev/null @@ -1,63 +0,0 @@ -# -# Epour - A bittorrent client using EFL and libtorrent -# -# Copyright 2012-2013 Kai Huuhko -# -# 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() diff --git a/epour/gui/Preferences.py b/epour/gui/Preferences.py index fc4f76b..b763105 100644 --- a/epour/gui/Preferences.py +++ b/epour/gui/Preferences.py @@ -1,11 +1,11 @@ # # Epour - A bittorrent client using EFL and libtorrent # -# Copyright 2012-2013 Kai Huuhko +# Copyright 2012-2014 Kai Huuhko # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or +# the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, @@ -22,11 +22,10 @@ import os import cgi import logging -log = logging.getLogger("epour") +log = logging.getLogger("epour.preferences") import libtorrent as lt -from efl.elementary.icon import Icon from efl.elementary.box import Box from efl.elementary.label import Label from efl.elementary.button import Button @@ -35,39 +34,42 @@ from efl.elementary.entry import Entry from efl.elementary.check import Check from efl.elementary.spinner import Spinner from efl.elementary.hoversel import Hoversel -from efl.elementary.popup import Popup +from efl.elementary.fileselector import Fileselector from efl.elementary.fileselector_button import FileselectorButton -from efl.elementary.scroller import Scroller, ELM_SCROLLER_POLICY_OFF, \ - ELM_SCROLLER_POLICY_AUTO +from efl.elementary.scroller import Scroller, ELM_SCROLLER_POLICY_AUTO from efl.elementary.separator import Separator from efl.elementary.slider import Slider from efl.elementary.actionslider import Actionslider, \ ELM_ACTIONSLIDER_LEFT, ELM_ACTIONSLIDER_CENTER, \ ELM_ACTIONSLIDER_RIGHT, ELM_ACTIONSLIDER_ALL -from efl.elementary.naviframe import Naviframe from efl.elementary.table import Table from efl.elementary.configuration import Configuration -from efl.evas import Rectangle -from efl.ecore import Timer -from efl.elementary.window import Window, ELM_WIN_BASIC +from efl.elementary.window import StandardWindow from efl.elementary.background import Background -import Notify +from efl.evas import Rectangle, EVAS_HINT_EXPAND, EVAS_HINT_FILL -EXPAND_BOTH = 1.0, 1.0 -EXPAND_HORIZ = 1.0, 0.0 -FILL_BOTH = -1.0, -1.0 -FILL_HORIZ = -1.0, 0.5 +from Widgets import UnitSpinner, Error, Information + +EXPAND_BOTH = EVAS_HINT_EXPAND, EVAS_HINT_EXPAND +EXPAND_HORIZ = EVAS_HINT_EXPAND, 0.0 +FILL_BOTH = EVAS_HINT_FILL, EVAS_HINT_FILL +FILL_HORIZ = EVAS_HINT_FILL, 0.5 +ALIGN_LEFT = 0.0, 0.5 SCROLL_BOTH = ELM_SCROLLER_POLICY_AUTO, ELM_SCROLLER_POLICY_AUTO -class PreferencesDialog(Window): + +class PreferencesDialog(StandardWindow): + """ Base class for all preferences dialogs """ - def __init__(self, title): + + def __init__(self, name, title): elm_conf = Configuration() scale = elm_conf.scale - Window.__init__(self, title, ELM_WIN_BASIC, title=title, autodel=True) + StandardWindow.__init__( + self, name, title, autodel=True) self.size = scale * 480, scale * 320 @@ -75,11 +77,10 @@ class PreferencesDialog(Window): self.resize_object_add(bg) bg.show() - # bt = Button(self, text="Close") - # bt.callback_clicked_add(lambda b: self.delete()) - - self.scroller = Scroller(self, policy=SCROLL_BOTH, - size_hint_weight=EXPAND_BOTH, size_hint_align=FILL_BOTH) + self.scroller = Scroller( + self, policy=SCROLL_BOTH, + size_hint_weight=EXPAND_BOTH, size_hint_align=FILL_BOTH + ) self.resize_object_add(self.scroller) self.scroller.show() @@ -87,18 +88,16 @@ class PreferencesDialog(Window): self.box.size_hint_weight = EXPAND_BOTH self.scroller.content = self.box - self.show() - - # def parent_resize_cb(self, parent): - # (pw, ph) = parent.size - # self.table.size_hint_min = pw * 0.7, ph * 0.7 class PreferencesGeneral(PreferencesDialog): + """ General preference dialog """ + def __init__(self, parent, session): self.session = session conf = session.conf - PreferencesDialog.__init__(self, "General") + PreferencesDialog.__init__( + self, "epour_prefs_general", "Epour General Preferences") limits = Limits(self, session) ports = ListenPorts(self, session) @@ -113,32 +112,44 @@ class PreferencesGeneral(PreferencesDialog): sep1.horizontal = True chk1 = Check(self) - chk1.size_hint_align = 0.0, 0.0 + chk1.size_hint_align = ALIGN_LEFT chk1.text = "Delete original .torrent file when added" chk1.state = conf.getboolean("Settings", "delete_original") - chk1.callback_changed_add(lambda x: conf.set("Settings", - "delete_original", str(bool(chk1.state)))) + chk1.callback_changed_add(lambda x: conf.set( + "Settings", "delete_original", str(bool(chk1.state)) + )) chk2 = Check(self) - chk2.size_hint_align = 0.0, 0.0 + chk2.size_hint_align = ALIGN_LEFT chk2.text = "Ask for confirmation on exit" - chk2.state = conf.getboolean("Settings", "confirmations") - chk2.callback_changed_add(lambda x: conf.set("Settings", - "confirmations", str(bool(chk2.state)))) + chk2.state = conf.getboolean("Settings", "confirm_exit") + chk2.callback_changed_add(lambda x: conf.set( + "Settings", "confirm_exit", str(bool(chk2.state)) + )) + + chk3 = Check(self) + chk3.size_hint_align = ALIGN_LEFT + chk3.text = "Torrents to be added from dbus open a dialog" + chk3.state = conf.getboolean("Settings", "dialog_add_dbus") + chk3.callback_changed_add(lambda x: conf.set( + "Settings", "confirmations", str(bool(chk3.state)) + )) sep2 = Separator(self) sep2.horizontal = True - for w in ports, limits, dlsel, pe, pad, sep1, chk1, chk2, sep2: + for w in ports, limits, dlsel, pe, pad, sep1, chk1, chk2, chk3, sep2: w.show() self.box.pack_end(w) + class DataStorageSelector(Frame): + def __init__(self, parent, conf): Frame.__init__(self, parent) - self.size_hint_align = -1.0, 0.0 - self.size_hint_weight = 1.0, 0.0 + self.size_hint_align = FILL_HORIZ + self.size_hint_weight = EXPAND_HORIZ self.text = "Data storage" self.conf = conf @@ -148,12 +159,10 @@ class DataStorageSelector(Frame): lbl = self.path_lbl = Label(parent) lbl.text = conf.get("Settings", "storage_path") - self.dlsel = dlsel = FileselectorButton(self) - dlsel.size_hint_align = -1.0, 0.0 - dlsel.inwin_mode = False - dlsel.folder_only = True - dlsel.expandable = False - dlsel.text = "Change path" + self.dlsel = dlsel = FsButton( + self, size_hint_align=FILL_HORIZ, inwin_mode=False, + text="Change path", folder_only=True, expandable=False + ) dlsel.path = conf.get("Settings", "storage_path") dlsel.callback_file_chosen_add(self.save_dlpath) @@ -169,14 +178,17 @@ class DataStorageSelector(Frame): return if not os.path.exists(self.dlsel.path): - p = Notify.Error(self, "Invalid storage path", + Error( + self, "Invalid storage path", "You have selected an invalid data storage path for torrents.") return self.path_lbl.text = path self.conf.set("Settings", "storage_path", self.dlsel.path) + class ListenPorts(Frame): + def __init__(self, parent, session): Frame.__init__(self, parent) @@ -185,16 +197,16 @@ class ListenPorts(Frame): self.size_hint_align = FILL_HORIZ self.text = "Listen port (range)" - port = session.listen_port() + #port = session.listen_port() b = Box(parent) b.size_hint_weight = EXPAND_HORIZ lp = self.lp = RangeSpinners( parent, - low = session.conf.getint("Settings", "listen_low"), - high = session.conf.getint("Settings", "listen_high"), - minim = 0, maxim = 65535) + low=session.conf.getint("Settings", "listen_low"), + high=session.conf.getint("Settings", "listen_high"), + minim=0, maxim=65535) lp.show() b.pack_end(lp) @@ -215,10 +227,14 @@ class ListenPorts(Frame): self.session.conf.set("Settings", "listen_low", str(low)) self.session.conf.set("Settings", "listen_high", str(high)) + class PreferencesProxy(PreferencesDialog): + """ Proxy preference dialog """ + def __init__(self, parent, session): - PreferencesDialog.__init__(self, "Proxy") + PreferencesDialog.__init__( + self, "epour_prefs_proxy", "Epour Proxy Preferences") proxies = [ ["Proxy for torrent peer connections", @@ -236,6 +252,7 @@ class PreferencesProxy(PreferencesDialog): pg.show() self.box.pack_end(pg) + class ProxyGroup(Frame): proxy_types = { @@ -248,21 +265,20 @@ class ProxyGroup(Frame): } def __init__(self, parent, title, rfunc, wfunc): - Frame.__init__(self, parent) - self.size_hint_weight = EXPAND_HORIZ - self.size_hint_align = FILL_HORIZ - self.text = title + Frame.__init__( + self, parent, size_hint_weight=EXPAND_HORIZ, + size_hint_align=FILL_HORIZ, text=title + ) - t = Table(self, homogeneous=True, padding=(3,3)) - t.size_hint_weight = EXPAND_HORIZ - t.size_hint_align = FILL_HORIZ + t = Table( + self, homogeneous=True, padding=(3, 3), + size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ + ) t.show() - l = Label(self, text="Proxy type") - l.size_hint_align = 0.0, 0.5 + l = Label(self, text="Proxy type", size_hint_align=ALIGN_LEFT) l.show() - ptype = Hoversel(parent) - ptype.size_hint_align = -1.0, 0.5 + ptype = Hoversel(parent, size_hint_align=FILL_HORIZ) ptype.text = rfunc().type.name for n in self.proxy_types.iterkeys(): ptype.item_add(n, callback=lambda x, y, z=n: ptype.text_set(z)) @@ -270,52 +286,42 @@ class ProxyGroup(Frame): t.pack(l, 0, 0, 1, 1) t.pack(ptype, 1, 0, 1, 1) - l = Label(self, text="Hostname") - l.size_hint_align = 0.0, 0.5 + l = Label(self, text="Hostname", size_hint_align=ALIGN_LEFT) l.show() - phost = Entry(parent) - phost.size_hint_weight = EXPAND_HORIZ - phost.size_hint_align = FILL_HORIZ - phost.single_line = True - phost.scrollable = True + phost = Entry( + parent, size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ, + single_line=True, scrollable=True + ) phost.entry = rfunc().hostname phost.show() t.pack(l, 0, 1, 1, 1) t.pack(phost, 1, 1, 1, 1) - l = Label(self, text="Port") - l.size_hint_align = 0.0, 0.5 + l = Label(self, text="Port", size_hint_align=ALIGN_LEFT) l.show() - pport = Spinner(parent) - pport.size_hint_align = -1.0, 0.5 - pport.min_max = 0, 65535 + pport = Spinner(parent, size_hint_align=FILL_HORIZ, min_max=(0, 65535)) pport.value = rfunc().port pport.show() t.pack(l, 0, 2, 1, 1) t.pack(pport, 1, 2, 1, 1) - l = Label(self, text="Username") - l.size_hint_align = 0.0, 0.5 + l = Label(self, text="Username", size_hint_align=ALIGN_LEFT) l.show() - puser = Entry(parent) - puser.size_hint_weight = EXPAND_HORIZ - puser.size_hint_align = FILL_HORIZ - puser.single_line = True - puser.scrollable = True + puser = Entry( + parent, size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ, + single_line=True, scrollable=True + ) puser.entry = rfunc().username puser.show() t.pack(l, 0, 3, 1, 1) t.pack(puser, 1, 3, 1, 1) - l = Label(self, text="Password") - l.size_hint_align = 0.0, 0.5 + l = Label(self, text="Password", size_hint_align=ALIGN_LEFT) l.show() - ppass = Entry(parent) - ppass.size_hint_weight = EXPAND_HORIZ - ppass.size_hint_align = FILL_HORIZ - ppass.single_line = True - ppass.scrollable = True - ppass.password = True + ppass = Entry( + parent, size_hint_weight=EXPAND_HORIZ, size_hint_align=FILL_HORIZ, + single_line=True, scrollable=True, password=True + ) ppass.entry = rfunc().password ppass.show() t.pack(l, 0, 4, 1, 1) @@ -342,45 +348,52 @@ class ProxyGroup(Frame): wfunc(p) + class EncryptionSettings(Frame): + def __init__(self, parent, session): self.session = session Frame.__init__(self, parent) - self.size_hint_align = -1.0, 0.0 + self.size_hint_align = FILL_HORIZ self.text = "Encryption settings" pes = self.pes = session.get_pe_settings() b = Box(parent) - enc_values = lt.enc_policy.disabled, lt.enc_policy.enabled, lt.enc_policy.forced - enc_levels = lt.enc_level.plaintext, lt.enc_level.rc4, lt.enc_level.both + enc_values = lt.enc_policy.disabled, lt.enc_policy.enabled, \ + lt.enc_policy.forced + enc_levels = lt.enc_level.plaintext, lt.enc_level.rc4, \ + lt.enc_level.both - inc = self.inc = ActSWithLabel(parent, - "Incoming encryption", enc_values, pes.in_enc_policy) + inc = self.inc = ActSWithLabel( + parent, "Incoming encryption", enc_values, pes.in_enc_policy + ) b.pack_end(inc) inc.show() - out = self.out = ActSWithLabel(parent, - "Outgoing encryption", enc_values, pes.out_enc_policy) + out = self.out = ActSWithLabel( + parent, "Outgoing encryption", enc_values, pes.out_enc_policy + ) b.pack_end(out) out.show() - lvl = self.lvl = ActSWithLabel(parent, - "Allowed encryption level", enc_levels, pes.allowed_enc_level) + lvl = self.lvl = ActSWithLabel( + parent, "Allowed encryption level", enc_levels, + pes.allowed_enc_level + ) b.pack_end(lvl) lvl.show() - prf = self.prf = Check(parent) - prf.style = "toggle" - prf.text = "Prefer RC4 ecryption" - prf.state = pes.prefer_rc4 + prf = self.prf = Check( + parent, style="toggle", text="Prefer RC4 ecryption", + state=pes.prefer_rc4 + ) b.pack_end(prf) prf.show() - a_btn = Button(parent) - a_btn.text = "Apply" + a_btn = Button(parent, text="Apply") a_btn.callback_clicked_add(self.apply) b.pack_end(a_btn) a_btn.show() @@ -389,17 +402,19 @@ class EncryptionSettings(Frame): self.content = b def apply(self, btn): - #TODO: Use callbacks to set these? + # TODO: Use callbacks to set these? self.pes.in_enc_policy = self.inc.get_value() self.pes.out_enc_policy = self.out.get_value() - #FIXME: Find out why this isn't saved to the session. + # FIXME: Find out why this isn't saved to the session. self.pes.allowed_enc_level = self.lvl.get_value() self.pes.prefer_rc4 = self.prf.state self.session.set_pe_settings(self.pes) + class ActSWithLabel(Box): + def __init__(self, parent, label_text, values, initial_value): Box.__init__(self, parent) @@ -410,16 +425,16 @@ class ActSWithLabel(Box): } self.horizontal = True - self.size_hint_align = -1.0, 0.0 - self.size_hint_weight = 1.0, 0.0 + self.size_hint_align = FILL_HORIZ + self.size_hint_weight = EXPAND_HORIZ l = Label(parent) l.text = label_text l.show() w = self.w = Actionslider(parent) w.magnet_pos = ELM_ACTIONSLIDER_ALL - w.size_hint_align = -1.0, 0.0 - w.size_hint_weight = 1.0, 0.0 + w.size_hint_align = FILL_HORIZ + w.size_hint_weight = EXPAND_HORIZ w.show() parts = "left", "center", "right" @@ -434,49 +449,92 @@ class ActSWithLabel(Box): def get_value(self): return self.vd[self.w.indicator_pos] -class PreferencesSession(PreferencesDialog): - """ Session preference dialog """ - def __init__(self, parent, session): - PreferencesDialog.__init__(self, "Session") - # TODO: Construct and populate this with an Idler +class PreferencesSession(PreferencesDialog): + + """ Session preference dialog """ + + def __init__(self, parent, session): + PreferencesDialog.__init__( + self, "epour_prefs_session", "Epour Session Preferences") self.session = session widgets = {} - elm_conf = Configuration() + #elm_conf = Configuration() + #scale = elm_conf.scale s = session.settings() - t = Table(self, padding=(5,5), homogeneous=True, - size_hint_align=FILL_BOTH) + t = Table( + self, padding=(5, 5), homogeneous=True, size_hint_align=FILL_BOTH + ) self.box.pack_end(t) t.show() i = 0 INT_MIN = -2147483648 - INT_MAX = 2147483647 + INT_MAX = 2147483647 - scale = elm_conf.scale + import time + t1 = time.time() for k in dir(s): - if k.startswith("__"): continue + if k.startswith("__"): + continue try: + if k == "peer_tos" or k == "outgoing_ports": + # XXX: these don't have a C++ -> Python equivalent. + continue a = getattr(s, k) if isinstance(a, lt.disk_cache_algo_t): w = Spinner(t) w.size_hint_align = FILL_HORIZ - # XXX: lt-rb python bindings don't have all values. - w.min_max = 0, 2 #len(lt.disk_cache_algo_t.values.keys()) + w.min_max = 0, len(lt.disk_cache_algo_t.values) - 1 for name, val in lt.disk_cache_algo_t.names.items(): w.special_value_add(val, name) w.value = a + elif k == "suggest_mode": + w = Spinner(t) + w.size_hint_align = FILL_HORIZ + w.min_max = 0, len(lt.suggest_mode_t.values) - 1 + for name, val in lt.suggest_mode_t.names.items(): + w.special_value_add(val, name) + w.value = a + elif k == "choking_algorithm": + w = Spinner(t) + w.size_hint_align = FILL_HORIZ + w.min_max = 0, len(lt.choking_algorithm_t.values) - 1 + for name, val in lt.choking_algorithm_t.names.items(): + w.special_value_add(val, name) + w.value = a + elif k == "seed_choking_algorithm": + w = Spinner(t) + w.size_hint_align = FILL_HORIZ + w.min_max = 0, len(lt.seed_choking_algorithm_t.values) - 1 + for name, val in lt.seed_choking_algorithm_t.names.items(): + w.special_value_add(val, name) + w.value = a + elif k == "mixed_mode_algorithm": + w = Spinner(t) + w.size_hint_align = FILL_HORIZ + w.min_max = 0, len(lt.bandwidth_mixed_algo_t.values) - 1 + for name, val in lt.bandwidth_mixed_algo_t.names.items(): + w.special_value_add(val, name) + w.value = a + elif k == "disk_io_write_mode" or k == "disk_io_read_mode": + w = Spinner(t) + w.size_hint_align = FILL_HORIZ + w.min_max = 0, len(lt.io_buffer_mode_t.values) - 1 + for name, val in lt.io_buffer_mode_t.names.items(): + w.special_value_add(val, name) + w.value = a elif isinstance(a, bool): w = Check(t) - w.size_hint_align = 1.0, 0.0 - w.style = "toggle" + #w.style = "toggle" + w.size_hint_align = FILL_HORIZ w.state = a elif isinstance(a, int): w = Spinner(t) @@ -493,10 +551,6 @@ class PreferencesSession(PreferencesDialog): else: w.min_max = 0.0, 20.0 w.value = a - elif k == "peer_tos": - # XXX: This is an int pair in libtorrent, - # which doesn't have a python equivalent. - continue elif k == "user_agent": w = Entry(t) w.size_hint_align = 1.0, 0.0 @@ -505,15 +559,16 @@ class PreferencesSession(PreferencesDialog): w.editable = False w.entry = cgi.escape(a) else: + log.debug("Using string for %s type %s", k, type(a)) w = Entry(t) - w.part_text_set("guide", "Enter here") w.size_hint_align = FILL_HORIZ w.size_hint_weight = EXPAND_HORIZ w.single_line = True w.entry = cgi.escape(a) + w.part_text_set("guide", "Enter here") l = Label(t) l.text = k.replace("_", " ").capitalize() - l.size_hint_align = 0.0, 0.0 + l.size_hint_align = ALIGN_LEFT l.size_hint_weight = EXPAND_HORIZ l.show() t.pack(l, 0, i, 1, 1) @@ -522,8 +577,11 @@ class PreferencesSession(PreferencesDialog): w.show() widgets[k] = w i += 1 - except TypeError: - pass #print("Error {}".format(k)) + except TypeError as e: + log.debug("Error in {}: {}".format(k, e)) + + t2 = time.time() + log.debug("Session settings time: %f", t2-t1) save_btn = Button(self) save_btn.text = "Apply session settings" @@ -552,69 +610,11 @@ class PreferencesSession(PreferencesDialog): setattr(s, k, v) session.set_settings(s) - Notify.Information(self, "Session settings saved.") + Information(self, "Session settings saved.") -class UnitSpinner(Box): - def __init__(self, parent, base, units): - self.base = base # the divisor/multiplier for units - self.units = units # a list of strings with the base unit description at index 0 - - super(UnitSpinner, self).__init__(parent) - self.horizontal = True - - self.save_timer = None - - s = self.spinner = Spinner(parent) - s.size_hint_weight = EXPAND_HORIZ - s.size_hint_align = FILL_HORIZ - s.min_max = 0, base - s.show() - self.pack_end(s) - - hs = self.hoversel = Hoversel(parent) - for u in units: - hs.item_add(u, None, 0, lambda x=hs, y=None, u=u: x.text_set(u)) - hs.show() - self.pack_end(hs) - - def callback_changed_add(self, func, delay=None): - self.spinner.callback_changed_add(self.changed_cb, func, delay) - self.hoversel.callback_selected_add(self.changed_cb, func, delay) - - def changed_cb(self, widget, *args): - func, delay = args[-2:] - - if delay: - if self.save_timer is not None: - self.save_timer.delete() - - self.save_timer = Timer(2.0, self.save_cb, func) - else: - self.save_cb(func) - - def save_cb(self, func): - v = int(self.get_value()) - log.debug("Saving value {}.".format(v)) - func(v) - return False - - def get_value(self): - return self.spinner.value * ( self.base ** self.units.index(self.hoversel.text) ) - - def set_value(self, v): - i = 0 - - while v // self.base > 0: - i += 1 - v = float(v) / float(self.base) - - if i > len(self.units): - i = len(self.units) - 1 - - self.spinner.value = v - self.hoversel.text = self.units[i] class RangeSpinners(Box): + def __init__(self, parent, low, high, minim, maxim): Box.__init__(self, parent) @@ -638,7 +638,9 @@ class RangeSpinners(Box): self.pack_end(h) h.show() + class Limits(Frame): + def __init__(self, parent, session): Frame.__init__(self, parent) @@ -646,14 +648,22 @@ class Limits(Frame): self.size_hint_align = FILL_HORIZ base = 1024 - units = ( "bytes/s", "KiB/s", "MiB/s", "GiB/s", "TiB/s" ) + units = "bytes/s", "KiB/s", "MiB/s", "GiB/s", "TiB/s" t = Table(parent) for r, values in enumerate(( - ("Upload limit", session.upload_rate_limit, session.set_upload_rate_limit), - ("Download limit", session.download_rate_limit, session.set_download_rate_limit), - ("Upload limit for local connections", session.local_upload_rate_limit, session.set_local_upload_rate_limit), - ("Download limit for local connections", session.local_download_rate_limit, session.set_local_download_rate_limit), + ("Upload limit", + session.upload_rate_limit, + session.set_upload_rate_limit), + ("Download limit", + session.download_rate_limit, + session.set_download_rate_limit), + ("Upload limit for local connections", + session.local_upload_rate_limit, + session.set_local_upload_rate_limit), + ("Download limit for local connections", + session.local_download_rate_limit, + session.set_local_download_rate_limit), )): title, rfunc, wfunc = values @@ -673,6 +683,12 @@ class Limits(Frame): self.content = t + +class FsButton(Fileselector, FileselectorButton): + + def __init__(self, parent, *args, **kwargs): + FileselectorButton.__init__(self, parent, *args, **kwargs) + # TODO: # max uploads?, max conns?, max half open conns? diff --git a/epour/gui/TorrentInfo.py b/epour/gui/TorrentProps.py similarity index 54% rename from epour/gui/TorrentInfo.py rename to epour/gui/TorrentProps.py index eaa6f19..d9499c4 100644 --- a/epour/gui/TorrentInfo.py +++ b/epour/gui/TorrentProps.py @@ -1,7 +1,7 @@ # # Epour - A bittorrent client using EFL and libtorrent # -# Copyright 2012-2013 Kai Huuhko +# Copyright 2012-2014 Kai Huuhko # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -27,108 +27,78 @@ log = logging.getLogger("epour") import libtorrent as lt -try: - from efl import ecore - from efl.elementary.genlist import Genlist, GenlistItemClass - from efl.elementary.innerwindow import InnerWindow - from efl.elementary.button import Button - from efl.elementary.box import Box - from efl.elementary.hoversel import Hoversel - from efl.elementary.check import Check - from efl.elementary.label import Label, ELM_WRAP_CHAR, ELM_WRAP_WORD - from efl.elementary.entry import Entry - from efl.elementary.naviframe import Naviframe - from efl.elementary.frame import Frame - from efl.elementary.object import ELM_SEL_FORMAT_TEXT, ELM_SEL_TYPE_CLIPBOARD -except ImportError: - import ecore - from elementary import Genlist, GenlistItemClass, InnerWindow, Button, \ - Box, Hoversel, Check, Label, ELM_WRAP_CHAR, ELM_WRAP_WORD, Entry, \ - Naviframe, Frame +from efl import ecore +from efl.evas import EVAS_HINT_EXPAND, EVAS_HINT_FILL +from efl.elementary.genlist import Genlist, GenlistItemClass +from efl.elementary.window import StandardWindow +from efl.elementary.innerwindow import InnerWindow +from efl.elementary.button import Button +from efl.elementary.box import Box +from efl.elementary.check import Check +from efl.elementary.label import Label, ELM_WRAP_CHAR +from efl.elementary.entry import Entry +from efl.elementary.frame import Frame +from efl.elementary.object import ELM_SEL_FORMAT_TEXT, \ + ELM_SEL_TYPE_CLIPBOARD +from efl.elementary.table import Table from intrepr import intrepr -from Notify import Information +from Widgets import Information -class TorrentInfo(InnerWindow): +EXPAND_BOTH = EVAS_HINT_EXPAND, EVAS_HINT_EXPAND +EXPAND_HORIZ = EVAS_HINT_EXPAND, 0.0 +FILL_BOTH = EVAS_HINT_FILL, EVAS_HINT_FILL +FILL_HORIZ = EVAS_HINT_FILL, 0.5 +ALIGN_LEFT = 0.0, 0.5 + + +class TorrentProps(InnerWindow): def __init__(self, parent, h): if not h.is_valid(): Information(parent.win, "Invalid torrent handle.") return - if not h.has_metadata(): - Information(parent.win, "Torrent contains no metadata.") - return - - i = h.get_torrent_info() - - InnerWindow.__init__(self, parent.win) + InnerWindow.__init__(self, parent) box = Box(self) - box.size_hint_align = -1.0, -1.0 - box.size_hint_weight = 1.0, 1.0 + box.size_hint_align = FILL_BOTH + box.size_hint_weight = EXPAND_BOTH tname = Label(self) - tname.size_hint_align = -1.0, 0.5 + tname.size_hint_align = FILL_HORIZ tname.line_wrap = ELM_WRAP_CHAR tname.ellipsis = True - tname.text = "{}".format(cgi.escape(i.name())) + tname.text = "{}".format(cgi.escape(h.name())) + tname.scale = 2.0 tname.show() box.pack_end(tname) - for func in i.comment, i.creation_date, i.creator: - try: - w = func() - except Exception as e: - log.debug(e) - else: - if w: - f = Frame(self) - f.size_hint_align = -1.0, 0.0 - f.text = func.__name__.replace("_", " ").capitalize() - l = Label(self) - l.ellipsis = True - l.text = cgi.escape(str(w)) - l.show() - f.content = l - f.show() - box.pack_end(f) - - tpriv = Check(self) - tpriv.size_hint_align = 0.0, 0.0 - tpriv.text = "Private" - tpriv.tooltip_text_set( - "Whether this torrent is private.
\ - i.e., it should not be distributed on the trackerless network
\ - (the kademlia DHT)." - ) - tpriv.disabled = True - tpriv.state = i.priv() + if h.has_metadata(): + ti = TorrentInfo(self, h, size_hint_align=FILL_HORIZ) + box.pack_end(ti) + ti.show() magnet_uri = lt.make_magnet_uri(h) f = Frame(self) - f.size_hint_align = -1.0, 0.0 + f.size_hint_align = FILL_HORIZ f.text = "Magnet URI" - me_box = Box(self) + me_box = Box(f) me_box.horizontal = True - me = Entry(self) - me.size_hint_align = -1.0, 0.0 - me.size_hint_weight = 1.0, 0.0 - #me.editable = False + me = Entry(me_box) + me.size_hint_align = FILL_HORIZ + me.size_hint_weight = EXPAND_HORIZ + me.editable = False me.entry = magnet_uri me_box.pack_end(me) me.show() - me_btn = Button(self) + me_btn = Button(me_box) me_btn.text = "Copy" - if hasattr(me, "cnp_selection_set"): - me_btn.callback_clicked_add( - lambda x: me.top_widget.cnp_selection_set( - ELM_SEL_TYPE_CLIPBOARD, ELM_SEL_FORMAT_TEXT, me.text - ) + me_btn.callback_clicked_add( + lambda x: me.top_widget.cnp_selection_set( + ELM_SEL_TYPE_CLIPBOARD, ELM_SEL_FORMAT_TEXT, me.text ) - else: - import pyperclip - me_btn.callback_clicked_add(lambda x: pyperclip.copy(magnet_uri)) + ) me_btn.show() me_box.pack_end(me_btn) me_box.show() @@ -136,38 +106,29 @@ class TorrentInfo(InnerWindow): f.show() box.pack_end(f) - - fl_btn = Button(self) - fl_btn.text = "Files ->" - fl_btn.callback_clicked_add(self.file_list_cb, h) - - xbtn = Button(self) + xbtn = Button(box) xbtn.text_set("Close") xbtn.callback_clicked_add(lambda x: self.delete()) - - - for w in tpriv, fl_btn, xbtn: - w.show() - box.pack_end(w) + box.pack_end(xbtn) + xbtn.show() box.show() - nf = self.nf = Naviframe(self) - nf.item_simple_push(box) - - self.content_set(nf) + self.content = box self.activate() - def file_list_cb(self, btn, h): - self.nf.item_simple_push(TorrentFiles(self.nf, h)) -class TorrentFiles(Box): - def __init__(self, parent, h): - Box.__init__(self, parent) +class TorrentFiles(StandardWindow): + def __init__(self, h): + StandardWindow.__init__(self, "epour.files", "Files", size=(600, 400)) + self.callback_delete_request_add(lambda x: self.delete()) - filelist = Genlist(self) - filelist.size_hint_align = -1.0, -1.0 - filelist.size_hint_weight = 1.0, 1.0 + box = Box(self, size_hint_weight=EXPAND_BOTH) + self.resize_object_add(box) + + filelist = Genlist(box) + filelist.size_hint_align = FILL_BOTH + filelist.size_hint_weight = EXPAND_BOTH self.populate(filelist, h) @@ -180,24 +141,25 @@ class TorrentFiles(Box): sel_all.show() sel_none = Button(self) - sel_none.text ="Select none" + sel_none.text = "Select none" sel_none.callback_clicked_add(self.select_all_cb, filelist, h, False) sel_none.show() - xbtn = Button(self) - xbtn.text = "Close" - xbtn.callback_clicked_add(lambda x: parent.item_pop()) - xbtn.show() + # xbtn = Button(self) + # xbtn.text = "Close" + # xbtn.callback_clicked_add(lambda x: parent.item_pop()) + # xbtn.show() btn_box = Box(self) btn_box.horizontal = True btn_box.pack_end(sel_all) btn_box.pack_end(sel_none) - btn_box.pack_end(xbtn) + #btn_box.pack_end(xbtn) btn_box.show() - self.pack_end(filelist) - self.pack_end(btn_box) + box.pack_end(filelist) + box.pack_end(btn_box) + box.show() self.show() def select_all_cb(self, btn, filelist, h, all_selected=True): @@ -232,6 +194,7 @@ class TorrentFiles(Box): else: os.startfile(path) + class FileSelectionClass(GenlistItemClass): def text_get(self, obj, part, data): file_entry, progress, n, h = data @@ -256,3 +219,58 @@ class FileSelectionClass(GenlistItemClass): priorities[n] = int(ck.state) h.prioritize_files(priorities) + +class TorrentInfo(Frame): + def __init__(self, parent, h, *args, **kwargs): + Frame.__init__(self, parent, *args, **kwargs) + + self.text = "Torrent info" + + info = h.get_torrent_info() + + box = Box(self, size_hint_weight=EXPAND_HORIZ) + self.content = box + box.show() + + table = Table(box, homogeneous=True, size_hint_align=FILL_HORIZ) + + i = 0 + for func in info.comment, info.creation_date, info.creator: + try: + w = func() + except Exception as e: + log.debug(e) + else: + if w: + l = Label(table, size_hint_align=ALIGN_LEFT) + l.text = func.__name__.replace("_", " ").capitalize() + table.pack(l, 0, i, 1, 1) + l.show() + v = Label(table, ellipsis=True, size_hint_align=ALIGN_LEFT) + v.text = cgi.escape(str(w)) + table.pack(v, 1, i, 1, 1) + v.show() + i += 1 + + tpriv = Check(self) + tpriv.size_hint_align = 0.0, 0.0 + tpriv.text = "Private" + tpriv.tooltip_text_set( + "Whether this torrent is private,
" + "i.e. it should not be distributed on the
" + "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() diff --git a/epour/gui/TorrentSelector.py b/epour/gui/TorrentSelector.py new file mode 100644 index 0000000..e989f32 --- /dev/null +++ b/epour/gui/TorrentSelector.py @@ -0,0 +1,264 @@ +# +# Epour - A bittorrent client using EFL and libtorrent +# +# Copyright 2012-2014 Kai Huuhko +# +# 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) diff --git a/epour/gui/Widgets.py b/epour/gui/Widgets.py new file mode 100644 index 0000000..e12cf89 --- /dev/null +++ b/epour/gui/Widgets.py @@ -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() diff --git a/epour/gui/__init__.py b/epour/gui/__init__.py index 527462d..496f8bc 100644 --- a/epour/gui/__init__.py +++ b/epour/gui/__init__.py @@ -1,8 +1,520 @@ -from Main import MainInterface +# +# Epour - A bittorrent client using EFL and libtorrent +# +# Copyright 2012-2014 Kai Huuhko +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# -try: - from efl import elementary -except ImportError: - import elementary +import cgi +import logging +from datetime import timedelta -elementary.init() +from efl.evas import EVAS_HINT_EXPAND, EVAS_HINT_FILL, \ + EVAS_ASPECT_CONTROL_VERTICAL, Rectangle +from efl.ecore import Timer +from efl import elementary as elm +elm.init() +from efl.elementary.genlist import Genlist, GenlistItemClass, \ + ELM_GENLIST_ITEM_FIELD_TEXT, ELM_GENLIST_ITEM_FIELD_CONTENT, \ + ELM_OBJECT_SELECT_MODE_NONE, ELM_LIST_COMPRESS +from efl.elementary.window import StandardWindow +from efl.elementary.icon import Icon +from efl.elementary.box import Box +from efl.elementary.label import Label +from efl.elementary.panel import Panel, ELM_PANEL_ORIENT_BOTTOM +from efl.elementary.table import Table +from efl.elementary.menu import Menu +from efl.elementary.configuration import Configuration +from efl.elementary.toolbar import Toolbar, ELM_TOOLBAR_SHRINK_NONE + +from Widgets import ConfirmExit, Error, Information + +from intrepr import intrepr + +EXPAND_BOTH = EVAS_HINT_EXPAND, EVAS_HINT_EXPAND +EXPAND_HORIZ = EVAS_HINT_EXPAND, 0.0 +FILL_BOTH = EVAS_HINT_FILL, EVAS_HINT_FILL +FILL_HORIZ = EVAS_HINT_FILL, 0.5 + +elm_conf = Configuration() +scale = elm_conf.scale + +log = logging.getLogger("epour.gui") + + +class MainInterface(object): + def __init__(self, parent, session): + self.parent = parent + self.session = session + + self.torrentitems = {} + + win = self.win = StandardWindow( + "epour", "Epour", size=(480 * scale, 400 * scale), + screen_constrain=True + ) + win.callback_delete_request_add(lambda x: self.quit()) + + mbox = Box(win, size_hint_weight=EXPAND_BOTH) + win.resize_object_add(mbox) + mbox.show() + + # --- TOOLBAR --- + tb = Toolbar( + win, size_hint_align=FILL_HORIZ, homogeneous=False, + shrink_mode=ELM_TOOLBAR_SHRINK_NONE, + select_mode=ELM_OBJECT_SELECT_MODE_NONE + ) + tb.menu_parent = win + + item = tb.item_append( + "document-new", "Add torrent", + lambda x, y: self.add_torrent() + ) + + def pause_session(it): + self.session.pause() + it.state_set(it.state_next()) + + def resume_session(it): + session.resume() + del it.state + + item = tb.item_append( + "media-playback-pause", "Pause Session", + lambda tb, it: pause_session(it) + ) + item.state_add( + "media-playback-start", "Resume Session", + lambda tb, it: resume_session(it) + ) + + def prefs_general_cb(): + from Preferences import PreferencesGeneral + PreferencesGeneral(self, self.session).show() + + def prefs_proxy_cb(): + from Preferences import PreferencesProxy + PreferencesProxy(self, self.session).show() + + def prefs_session_cb(): + from Preferences import PreferencesSession + PreferencesSession(self, self.session).show() + + item = tb.item_append("preferences-system", "Preferences") + item.menu = True + item.menu.item_add( + None, "General", "preferences-system", + lambda o, i: prefs_general_cb() + ) + item.menu.item_add( + None, "Proxy", "preferences-system", + lambda o, i: prefs_proxy_cb() + ) + item.menu.item_add( + None, "Session", "preferences-system", + lambda o, i: prefs_session_cb() + ) + + item = tb.item_append("application-exit", "Exit", + lambda tb, it: elm.exit()) + + mbox.pack_start(tb) + tb.show() + + # --- TORRENT LIST --- + self.tlist = tlist = Genlist( + mbox, select_mode=ELM_OBJECT_SELECT_MODE_NONE, + mode=ELM_LIST_COMPRESS, size_hint_weight=EXPAND_BOTH, + size_hint_align=FILL_BOTH, homogeneous=True + ) + + def item_activated_cb(gl, item): + h = item.data + ItemMenu(self.win, item, self.session, h) + + tlist.callback_activated_add(item_activated_cb) + tlist.show() + + mbox.pack_end(tlist) + + topbox = Box(win, size_hint_weight=EXPAND_BOTH, horizontal=False) + + pad1 = Rectangle(win.evas, size_hint_weight=EXPAND_BOTH) + + p = Panel( + topbox, size_hint_weight=EXPAND_BOTH, size_hint_align=FILL_BOTH, + color=(200, 200, 200, 200), orient=ELM_PANEL_ORIENT_BOTTOM + ) + p.content = SessionStatus(win, session) + p.hidden = True + p.show() + + win.resize_object_add(topbox) + + topbox.pack_end(pad1) + topbox.pack_end(p) + topbox.stack_above(mbox) + topbox.show() + + def torrent_added_cb(a): + h = a.handle + self.add_torrent_item(h) + + def torrent_removed_cb(a): + self.remove_torrent_item(str(a.info_hash)) + + def torrent_update_cb(a): + self.remove_torrent_item(str(a.old_ih)) + new_h = self.session.find_torrent(str(a.new_ih)) + self.add_torrent_item(new_h) + + session.alert_manager.callback_add( + "torrent_added_alert", torrent_added_cb) + session.alert_manager.callback_add( + "torrent_removed_alert", torrent_removed_cb) + session.alert_manager.callback_add( + "torrent_update_alert", torrent_update_cb) + + def torrent_paused_cb(a): + self.update_icon(a.handle) + + for a_name in "torrent_paused_alert", "torrent_resumed_alert": + session.alert_manager.callback_add(a_name, torrent_paused_cb) + + def state_changed_cb(a): + h = a.handle + if not h.is_valid(): + log.debug("State changed for invalid handle.") + return + + ihash = str(h.info_hash()) + if not ihash in self.torrentitems: + log.debug("%s not in list", str(ihash)) + return + + self.update_icon(h) + + session.alert_manager.callback_add( + "state_changed_alert", state_changed_cb) + + def torrent_finished_cb(a): + msg = "Torrent {} has finished downloading.".format( + cgi.escape(a.handle.name()) + ) + log.info(msg) + + Information(self.win, msg) + + Timer(15.0, lambda: session.alert_manager.callback_add( + "torrent_finished_alert", torrent_finished_cb)) + + def add_torrent(self, t_uri=None): + from TorrentSelector import TorrentSelector + TorrentSelector(self.win, self.session, t_uri) + + def run(self): + self.win.show() + + self.timer = Timer(1.0, self.update) + elm.run() + elm.shutdown() + + def update(self): + for v in self.tlist.realized_items_get(): + v.fields_update("*", ELM_GENLIST_ITEM_FIELD_TEXT) + return True + + def update_icon(self, h): + if not h.is_valid(): + return + ihash = str(h.info_hash()) + if not ihash in self.torrentitems: + return + self.torrentitems[ihash].fields_update( + "elm.swallow.icon", ELM_GENLIST_ITEM_FIELD_CONTENT + ) + + def add_torrent_item(self, h): + ihash = str(h.info_hash()) + + itc = TorrentClass(self.session, "double_label") + item = self.tlist.item_append(itc, h) + self.torrentitems[ihash] = item + + def remove_torrent_item(self, info_hash): + it = self.torrentitems.pop(info_hash, None) + if it is not None: + it.delete() + + def show_error(self, title, text): + Error(self.win, title, text) + + def quit(self, *args): + if self.session.conf.getboolean("Settings", "confirm_exit"): + ConfirmExit(self.win, elm.exit) + else: + elm.exit() + + +class SessionStatus(Table): + + log = logging.getLogger("epour.gui.session") + + def __init__(self, parent, session): + Table.__init__(self, parent) + self.session = session + + s = session.status() + + self.homogeneous = True + self.padding = 5, 2 + + ses_pause_ic = self.ses_pause_ic = Icon(parent) + ses_pause_ic.size_hint_align = -1.0, -1.0 + try: + if session.is_paused(): + ses_pause_ic.standard = "player_pause" + else: + ses_pause_ic.standard = "player_play" + except RuntimeError: + log.debug("Setting session ic failed") + self.pack(ses_pause_ic, 1, 0, 1, 1) + ses_pause_ic.show() + + title_l = Label(parent, text="Session") + 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) diff --git a/epour/gui/pyperclip.py b/epour/gui/pyperclip.py deleted file mode 100644 index 7fc2bc5..0000000 --- a/epour/gui/pyperclip.py +++ /dev/null @@ -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) diff --git a/epour/gui/which.py b/epour/gui/which.py deleted file mode 100644 index 99f5e3f..0000000 --- a/epour/gui/which.py +++ /dev/null @@ -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 - (, ) -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: - -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 , --path= - An alternative path (list of directories) may - be specified for searching. - -e , --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 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 (, ). - """ - 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) ) - - diff --git a/epour/session.py b/epour/session.py index 28158a4..8004085 100644 --- a/epour/session.py +++ b/epour/session.py @@ -1,8 +1,7 @@ -#!/usr/bin/env python2 # # Epour - A bittorrent client using EFL and libtorrent # -# Copyright 2012-2013 Kai Huuhko +# Copyright 2012-2014 Kai Huuhko # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -22,51 +21,49 @@ import os import mimetypes -from ConfigParser import SafeConfigParser -import cPickle import urlparse import urllib -import HTMLParser - +import logging import shutil -import cgi +import cPickle +from collections import OrderedDict import libtorrent as lt -try: - from efl.ecore import Timer -except: - from ecore import Timer +from efl.ecore import Timer -import logging +from Globals import data_dir + + +def torrent_path_get(ihash): + return os.path.join(data_dir, "{}.torrent".format(ihash)) -from Globals import conf_dir, conf_path, data_dir class Session(lt.session): - def __init__(self, parent): - self.parent = parent - + def __init__(self, conf): + self.conf = conf self.log = logging.getLogger("epour.session") from Globals import version ver_ints = [] for s in version.split("."): ver_ints.append(int(s)) + ver_ints.append(0) fp = lt.fingerprint("EP", *ver_ints) self.log.debug("peer-id: {}".format(fp)) lt.session.__init__( self, - fingerprint = fp, - # flags = \ - #lt.session_flags_t.add_default_plugins | \ + fingerprint=fp, + # flags= + #lt.session_flags_t.add_default_plugins| #lt.session_flags_t.start_default_features ) self.log.info("Session started") - self.torrents = {} + self.torrents = OrderedDict() #sdpipsdtsppe #theprtertoer @@ -78,8 +75,6 @@ class Session(lt.session): mask = 0b000001000001 self.set_alert_mask(mask) - conf = self.conf = self.setup_conf() - self.listen_on( conf.getint("Settings", "listen_low"), conf.getint("Settings", "listen_high") @@ -87,66 +82,44 @@ class Session(lt.session): self.alert_manager = AlertManager(self) self.alert_manager.callback_add( - "metadata_received_alert", self.metadata_received) + "add_torrent_alert", self._add_torrent_cb) + self.alert_manager.callback_add( + "metadata_received_alert", self._metadata_received_cb) - def metadata_received(self, a): + def _add_torrent_cb(self, a): + e = a.error + if e.value() > 0: + self.log.error("Adding torrent failed: %r" % (e.message())) + return + h = a.handle + ihash = str(h.info_hash()) + self.torrents[ihash] = a.params + self.log.debug("Torrent added.") + + def _metadata_received_cb(self, a): h = a.handle ihash = str(h.info_hash()) self.log.debug("Metadata received.") - t_path = self.write_torrent(h) - self.torrents[ihash] = t_path - - def write_torrent(self, h): - t_info = h.get_torrent_info() - ihash = str(h.info_hash()) - - self.log.debug("Writing torrent file {}".format(ihash)) - - md = lt.bdecode(t_info.metadata()) - t = {} - t["info"] = md - t_path = os.path.join( - data_dir, "{}.torrent".format(ihash) - ) - with open(t_path, "wb") as f: - f.write(lt.bencode(t)) - - return t_path - - - def setup_conf(self): - conf = SafeConfigParser({ - "storage_path": os.path.expanduser( - os.path.join("~", "Downloads") - ), - "confirmations": str(False), - "delete_original": str(False), - "listen_low": str(0), - "listen_high": str(0), - }) - - conf.read(conf_path) - - if not conf.has_section("Settings"): - conf.add_section("Settings") - - return conf - - def save_conf(self): - with open(conf_path, 'wb') as configfile: - self.conf.write(configfile) + self.write_torrent(h) + self.torrents[ihash]["ti"] = h.get_torrent_info() def load_state(self): try: with open(os.path.join(data_dir, "session"), 'rb') as f: state = lt.bdecode(f.read()) lt.session.load_state(self, state) - except: + except Exception as e: self.log.debug("Could not load previous session state.") + self.log.debug(e) + else: + self.log.info("Session restored from disk.") settings = self.settings() from Globals import version - settings.user_agent = "Epour/{} libtorrent/{}".format(version, lt.version) + version += ".0" + ver_s = "Epour/{} libtorrent/{}".format(version, lt.version) + settings.user_agent = ver_s + self.log.debug("User agent: {}".format(ver_s)) self.set_settings(settings) def save_state(self): @@ -169,29 +142,58 @@ class Session(lt.session): self.log.warning("Could not open the list of torrents.") else: try: - paths = cPickle.load(pkl_file) + torrents = cPickle.load(pkl_file) except EOFError: self.log.exception("Opening the list of torrents failed.") else: self.log.debug( "List of torrents opened, " - "restoring {} torrents.".format(len(paths)) + "restoring {} torrents.".format(len(torrents)) ) - for k, v in paths.iteritems(): + for i, t in torrents.items(): try: - self.log.debug("Adding {}".format(v)) - self.add_torrent(v) - except: - self.log.exception( - "Restoring torrent {0} failed".format(v) - ) + if isinstance(t, dict): + for k, v in t.items(): + if k == "ti": + with open(v, "rb") as f: + torrents[i][k] = \ + lt.torrent_info(lt.bdecode(f.read())) + elif k == "info_hash": + torrents[i][k] = lt.big_number(v) + else: + # Upgrade from older versions where t is file path + p = t + t = {} + with open(p, "rb") as f: + t["ti"] = \ + lt.torrent_info(lt.bdecode(f.read())) + rp = os.path.join(data_dir, i + ".fastresume") + if os.path.exists(rp): + with open(rp, "rb") as f: + t["resume_data"] = f.read() + except Exception: + self.log.exception("Opening torrent file %s failed", v) + continue + + self.async_add_torrent(t) finally: pkl_file.close() def save_torrents(self): self.log.debug("Saving {} torrents.".format(len(self.torrents))) + + for i, t in self.torrents.items(): + for k, v in t.items(): + if k == "ti": + self.torrents[i][k] = torrent_path_get(i) + elif k == "info_hash": + if v.is_all_zeros(): + del self.torrents[i][k] + else: + self.torrents[i][k] = v.to_string() + with open(os.path.join(data_dir, "torrents"), 'wb') as f: - cPickle.dump(self.torrents, f) + cPickle.dump(self.torrents, f, protocol=2) self.log.debug("List of torrents saved.") @@ -201,92 +203,30 @@ class Session(lt.session): continue data = lt.bencode(h.write_resume_data()) with open(os.path.join( - data_dir, '{}.fastresume'.format(h.info_hash()) - ), 'wb' - ) as f: + data_dir, str(h.info_hash()) + ".fastresume" + ), 'wb') as f: f.write(data) self.log.debug("Fast resume data saved.") - def add_torrent(self, t_uri): - if not t_uri: - return - - storage_path = self.conf.get("Settings", "storage_path") - - if not t_uri.startswith("magnet"): - mimetype = mimetypes.guess_type(t_uri)[0] - if not mimetype == "application/x-bittorrent": - self.log.error("Invalid file") - return - - if t_uri.startswith("file://"): - t_uri = urllib.unquote(urlparse.urlsplit(t_uri).path) - - with open(t_uri, 'rb') as t: - t_raw = lt.bdecode(t.read()) - - info = lt.torrent_info(t_raw) - rd = None - try: - with open(os.path.join( - data_dir, "{}.fastresume".format(info.info_hash()) - ), "rb" - ) as f: - rd = lt.bdecode(f.read()) - except: - try: - with open(os.path.join( - data_dir, "{}.fastresume".format(info.name()) - ), "rb" - ) as f: - rd = lt.bdecode(f.read()) - except: - self.log.debug("Invalid resume data") - - h = lt.session.add_torrent( - self, info, storage_path, resume_data=rd) - - ihash = str(h.info_hash()) - - new_uri = os.path.join( - data_dir, "{}.torrent".format(ihash) - ) - - if t_uri == new_uri: - pass - else: - shutil.copy(t_uri, new_uri) - - if self.conf.getboolean("Settings", "delete_original"): - self.log.debug("Deleting original torrent file {}".format(t_uri)) - os.remove(t_uri) - - t_uri = new_uri - else: - t_uri = urllib.unquote(t_uri) - t_uri = str(HTMLParser.HTMLParser().unescape(t_uri)) - h = lt.add_magnet_uri( - self, t_uri, - { "save_path": str(storage_path) } - ) - - if not h.is_valid(): - self.log.error("Invalid torrent handle") + def write_torrent(self, h): + if h is None: + self.log.debug("Tried to write torrent while handle was empty.") return + t_info = h.get_torrent_info() ihash = str(h.info_hash()) - self.torrents[ihash] = t_uri + self.log.debug("Writing torrent file {}".format(ihash)) - if not hasattr(lt, "torrent_added_alert"): - class torrent_added_alert(object): - def __init__(self, h): - self.handle = h + md = lt.bdecode(t_info.metadata()) + t = {} + t["info"] = md + t_path = torrent_path_get(ihash) + with open(t_path, "wb") as f: + f.write(lt.bencode(t)) - a = torrent_added_alert(h) - - self.alert_manager.signal(a) + return t_path def remove_torrent(self, h, with_data=False): ihash = str(h.info_hash()) @@ -294,24 +234,26 @@ class Session(lt.session): fr_path = os.path.join( data_dir, "{}.fastresume".format(ihash) ) - t_path = self.torrents[ihash] + #t_dict = self.torrents[ihash] del self.torrents[ihash] lt.session.remove_torrent(self, h, option=with_data) try: - with open(fr_path): pass + with open(fr_path): + pass except IOError: self.log.debug("Could not remove fast resume data.") else: os.remove(fr_path) try: - with open(t_path): pass + with open(torrent_path_get(ihash)): + pass except IOError: self.log.debug("Could not remove torrent file.") else: - os.remove(t_path) + os.remove(torrent_path_get(ihash)) if not hasattr(lt, "torrent_removed_alert"): class torrent_removed_alert(object): @@ -325,29 +267,64 @@ class Session(lt.session): return ihash + def add_torrent_from_file(self, add_dict, t_uri): + mimetype = mimetypes.guess_type(t_uri)[0] + if not mimetype == "application/x-bittorrent": + self.log.error("Invalid file") + return -class TorrentDict(dict): + if t_uri.startswith("file://"): + t_uri = urllib.unquote(urlparse.urlsplit(t_uri).path) - # Required keys are save_path and either ti or info_hash + with open(t_uri, 'rb') as t: + t_raw = lt.bdecode(t.read()) - # torrent_info ti - # string tracker_url - # string info_hash - # string name - # string save_path - # string resume_data - # storage_mode_t storage_mode - # bool paused - # bool auto_managed - # bool duplicate_is_error - # storage? - # object userdata? - # bool seed_mode - # bool override_resume_data - # bool upload_mode + info = lt.torrent_info(t_raw) + add_dict["ti"] = info + + rd = None + try: + with open(os.path.join( + data_dir, "{}.fastresume".format(info.info_hash()) + ), "rb" + ) as f: + rd = f.read() + except Exception: + self.log.debug("Invalid resume data") + else: + add_dict["resume_data"] = rd + + ihash = str(info.info_hash()) + + new_uri = os.path.join( + data_dir, "{}.torrent".format(ihash) + ) + + if t_uri == new_uri: + pass + else: + shutil.copy(t_uri, new_uri) + + if self.conf.getboolean("Settings", "delete_original"): + self.log.debug( + "Deleting original torrent file {}".format(t_uri)) + os.remove(t_uri) + + t_uri = new_uri + + self.async_add_torrent(add_dict) + + def add_torrent_from_magnet(self, add_dict, t_uri): + self.log.debug("Adding %r", t_uri) + tmp_dict = lt.parse_magnet_uri(bytes(t_uri)) + tmp_dict.update(add_dict) + self.async_add_torrent(tmp_dict) + + def add_torrent_from_hash(self, add_dict, t_uri): + add_dict["info_hash"] = lt.info_hash(bytes(t_uri)) + self.log.debug("Adding %s", t_uri) + self.async_add_torrent(add_dict) - def __init__(self, **kwargs): - dict.__init__(self, **kwargs) class AlertManager(object): @@ -361,7 +338,7 @@ class AlertManager(object): self.timer = Timer(self.update_interval, self.update) def callback_add(self, alert_type, cb, *args, **kwargs): - if not self.alerts.has_key(alert_type): + if not alert_type in self.alerts: self.alerts[alert_type] = [] self.alerts[alert_type].append((cb, args, kwargs)) @@ -373,7 +350,7 @@ class AlertManager(object): def signal(self, a): a_name = type(a).__name__ - if not self.alerts.has_key(a_name): + if not a_name in self.alerts: self.log.debug("No handler: {} | {}".format(a_name, a)) return @@ -386,9 +363,9 @@ class AlertManager(object): def update(self): while 1: a = self.session.pop_alert() - if not a: break + if not a: + break self.signal(a) return True - diff --git a/setup.py b/setup.py index 23027b8..6dfa4f2 100755 --- a/setup.py +++ b/setup.py @@ -4,24 +4,24 @@ from DistUtilsExtra.auto import setup from epour.Globals import version -setup(name='epour', - version=version, - author='Kai Huuhko', - author_email='kai.huuhko@gmail.com', - maintainer='Kai Huuhko', - maintainer_email='kai.huuhko@gmail.com', - description='Simple torrent client', - long_description='Epour is a simple torrent client using EFL and libtorrent.', - #url='', - #download_url='', - license='GNU GPL', - platforms='linux', +setup( + name='epour', + version=version, + author='Kai Huuhko', + author_email='kai.huuhko@gmail.com', + maintainer='Kai Huuhko', + maintainer_email='kai.huuhko@gmail.com', + description='A torrent client', + long_description=( + 'Epour is a torrent client based on EFL and rb-libtorrent.' + ), + #url='', + #download_url='', + license='GNU GPL', + platforms='linux', requires=[ 'libtorrent', - 'evas', - 'ecore', - 'elementary', - 'e_dbus', + 'efl', ], provides=[ 'epour',