kuuko pushed a commit to branch master. http://git.enlightenment.org/apps/epour.git/commit/?id=0b1b72fcacd532d47eb0b47b1bb66e6bcad7c20a
commit 0b1b72fcacd532d47eb0b47b1bb66e6bcad7c20a Author: Kai Huuhko <[email protected]> Date: Thu Aug 4 18:23:02 2016 +0300 Change internals to work with newer libtorrent versions This will once again change the save format of the torrent list, hopefully for the last time. --- README | 23 +- TODO | 4 +- bin/epour | 12 +- epour/Epour.py | 86 ++---- epour/gui/TorrentSelector.py | 63 ++-- epour/gui/Widgets.py | 41 ++- epour/gui/__init__.py | 351 +++++++++++----------- epour/session.py | 683 +++++++++++++++++++++++++++++++------------ po/epour.pot | 264 ++++++++--------- 9 files changed, 890 insertions(+), 637 deletions(-) diff --git a/README b/README index 007012d..dd4a30a 100644 --- a/README +++ b/README @@ -2,22 +2,18 @@ REQUIREMENTS ============ -* libtorrent-rasterbar 1.0 or later, currently tested and developed with - version 1.0.5 +* libtorrent-rasterbar 1.2.0 or later, currently tested and developed with + version 1.2.0 - http://www.libtorrent.org/ + https://libtorrent.org/ -* Enlightenment Foundation Libraries 1.8 or later +* Python-EFL 1.15 or later https://www.enlightenment.org/download -* Python-EFL 1.8 or later +* Python 3.2 or later - https://www.enlightenment.org/download - -* Python 2.6 or later, 3.2 or later - - http://www.python.org/ + https://www.python.org/ * python-distutils-extra @@ -25,11 +21,11 @@ REQUIREMENTS * dbus-python - http://www.freedesktop.org/wiki/Software/DBusBindings/#dbus-python + https://www.freedesktop.org/wiki/Software/DBusBindings/#dbus-python * python-xdg / pyxdg - http://www.freedesktop.org/wiki/Software/pyxdg/ + https://www.freedesktop.org/wiki/Software/pyxdg/ ======= INSTALL @@ -43,5 +39,4 @@ CONTACT/BUGS Email: [email protected] Mailing list: [email protected] -Forums: http://forums.bodhilinux.com/index.php?/topic/7722-epour/ -Launchpad: https://launchpad.net/epour +Bugs: https://phab.enlightenment.org/maniphest/ (set Tags-field to Epour) diff --git a/TODO b/TODO index 7179ed0..463bb03 100644 --- a/TODO +++ b/TODO @@ -39,7 +39,7 @@ Misc: ☐ Moving finished torrents to a different folder ☐ Individual torrent move when finished Can this setting be saved to torrent dicts user data? - ☐ Save torrent & resume data periodically + ✔ Save torrent & resume data periodically @done (16-07-30 08:05) Will this accomplish anything? http://libtorrent.org/reference-Core.html#save_resume_data() http://libtorrent.org/reference-Core.html#need_save_resume_data() @@ -47,7 +47,7 @@ Misc: ☐ Total count of torrents in session status ☐ Local search function for torrents, files? ☐ Web search - ☐ Handle switching between py2 <--> py3, messes up with pickled list of torrents + ✘ Handle switching between py2 <--> py3, messes up with pickled list of torrents @cancelled (16-07-30 08:15) File "/usr/bin/epour", line 7, in <module> app = Epour() File "/usr/lib/python2.7/dist-packages/epour/Epour.py", line 89, in __init__ diff --git a/bin/epour b/bin/epour index 72519f8..1890371 100755 --- a/bin/epour +++ b/bin/epour @@ -1,10 +1,14 @@ -#!/usr/bin/python2 +#!/usr/bin/python import logging from epour.Epour import Epour -app = Epour() -app.gui.run() -app.quit() +epour = Epour() +epour.gui.run_mainloop() +try: + epour.save_conf() +except Exception: + epour.log.exception("Saving conf failed") + logging.shutdown() diff --git a/epour/Epour.py b/epour/Epour.py index 17cb8a4..9d28418 100644 --- a/epour/Epour.py +++ b/epour/Epour.py @@ -2,7 +2,7 @@ # # Epour - A bittorrent client using EFL and libtorrent # -# Copyright 2012-2014 Kai Huuhko <[email protected]> +# Copyright 2012-2016 Kai Huuhko <[email protected]> # # 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,8 +27,7 @@ parser.add_argument( '-v', '--verbose', action="count", help="max is -vvv") parser.add_argument( '--disable-add-dialog', action="store_true", - help="Torrents to be added from arguments don't open a dialog" - ) + help="Torrents to be added from arguments don't open a dialog") parser.add_argument( 'torrents', nargs="*", help="file path, magnet uri, or info hash", metavar="TORRENT") @@ -72,11 +71,13 @@ from .gui import MainInterface class Epour(object): + def __init__(self): self.log = self.setup_log() + self.log.debug("Logging started") self.conf = self.setup_conf() - session = self.session = Session(self.conf) + session = self.session = Session(self.conf, self._quit_cb) session.load_state() self.gui = MainInterface(self, session) @@ -93,16 +94,7 @@ class Epour(object): self.conf.getboolean("Settings", "add_dialog_enabled"): 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) + self.session.add_torrent_with_uri(t) def setup_log(self): log_level = logging.ERROR @@ -115,18 +107,15 @@ class Epour(object): ch = logging.StreamHandler() ch_formatter = logging.Formatter( - '%(name)s: [%(levelname)s] %(message)s' - ) + '%(name)s: [%(levelname)s] %(message)s (%(filename)s: %(lineno)d)') ch.setFormatter(ch_formatter) ch.setLevel(log_level) log.addHandler(ch) fh = logging.FileHandler( - os.path.join(save_data_path("epour"), "epour.log") - ) + os.path.join(save_data_path("epour"), "epour.log")) fh_formatter = logging.Formatter( - '%(asctime)s - %(name)s - %(levelname)s - %(message)s' - ) + '%(asctime)s - %(name)s - %(levelname)s - %(message)s') fh.setFormatter(fh_formatter) fh.setLevel(logging.ERROR) log.addHandler(fh) @@ -167,25 +156,8 @@ class Epour(object): 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") + def _quit_cb(self): + self.gui.stop_mainloop() class EpourDBus(dbus.service.Object): @@ -198,8 +170,7 @@ class EpourDBus(dbus.service.Object): self.conf = conf dbus.service.Object.__init__( self, dbus.SessionBus(), - "/net/launchpad/epour", "net.launchpad.epour" - ) + "/net/launchpad/epour", "net.launchpad.epour") self.props = { } @@ -212,16 +183,7 @@ class EpourDBus(dbus.service.Object): if self.conf.getboolean("Settings", "add_dialog_enabled"): self.gui.add_torrent(t) else: - add_dict = { - "save_path": self.conf.get("Settings", "storage_path"), - "flags": 592 - } - if t.startswith("magnet:"): - self.session.add_torrent_from_magnet(add_dict, t) - elif os.path.isfile(t): - self.session.add_torrent_from_file(add_dict, t) - else: - self.session.add_torrent_from_hash(add_dict, t) + self.session.add_torrent_with_uri(t) except Exception: self.log.exception("Error while adding torrent from dbus") @@ -230,16 +192,7 @@ class EpourDBus(dbus.service.Object): def AddTorrentWithoutDialog(self, t): self.log.info("Adding %s from dbus" % t) try: - add_dict = { - "save_path": self.conf.get("Settings", "storage_path"), - "flags": 592 - } - if t.startswith("magnet:"): - self.session.add_torrent_from_magnet(add_dict, t) - elif os.path.isfile(t): - self.session.add_torrent_from_file(add_dict, t) - else: - self.session.add_torrent_from_hash(add_dict, t) + self.session.add_torrent_with_uri(t) except Exception: self.log.exception("Error while adding torrent from dbus") @@ -247,13 +200,16 @@ if __name__ == "__main__": efllog = logging.getLogger("efl") efllog.setLevel(logging.INFO) efllog_formatter = logging.Formatter( - '%(name)s: [%(levelname)s] %(message)s' - ) + '%(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() + epour.gui.run_mainloop() + try: + epour.save_conf() + except Exception: + epour.log.exception("Saving conf failed") + logging.shutdown() diff --git a/epour/gui/TorrentSelector.py b/epour/gui/TorrentSelector.py index 01142d7..ab19d48 100644 --- a/epour/gui/TorrentSelector.py +++ b/epour/gui/TorrentSelector.py @@ -1,7 +1,7 @@ # # Epour - A bittorrent client using EFL and libtorrent # -# Copyright 2012-2014 Kai Huuhko <[email protected]> +# Copyright 2012-2016 Kai Huuhko <[email protected]> # # 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 @@ -26,18 +26,18 @@ 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, Window, ELM_WIN_DIALOG_BASIC -from efl.elementary.background import Background -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, \ +from efl.elementary import StandardWindow, Window, ELM_WIN_DIALOG_BASIC +from efl.elementary import Background +from efl.elementary import Button +from efl.elementary import Box +from efl.elementary import Frame +from efl.elementary import Fileselector +from efl.elementary import FileselectorEntry +from efl.elementary import Entry, markup_to_utf8, utf8_to_markup +from efl.elementary import Check +from efl.elementary import Scroller +from efl.elementary import Spinner +# from efl.elementary import ELM_SEL_TYPE_CLIPBOARD, \ # ELM_SEL_FORMAT_TEXT from .Widgets import UnitSpinner @@ -47,7 +47,7 @@ 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 +from efl.elementary import Configuration elm_conf = Configuration() scale = elm_conf.scale @@ -287,8 +287,8 @@ always used.''' opt_box.pack_end(e) e.show() - def fs_cb(fs, key): - self.add_dict[key] = fs.path + def fs_cb(fs): + self.add_dict["save_path"] = fs.path # Downloaded data is saved in this path title = _("Data Save Path") @@ -296,8 +296,9 @@ always used.''' opt_box, size_hint_align=FILL_HORIZ, text=title, folder_only=True, expandable=False ) - save_path.path = session.conf.get("Settings", "storage_path") - save_path.callback_changed_add(fs_cb, "save_path") + save_path.callback_changed_add(fs_cb) + path = session.conf.get("Settings", "storage_path").strip() + save_path.path = path opt_box.pack_end(save_path) save_path.show() @@ -376,8 +377,7 @@ always used.''' bbox = Box( box, size_hint_weight=EXPAND_HORIZ, - size_hint_align=FILL_HORIZ, horizontal=True - ) + 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) @@ -401,28 +401,13 @@ always used.''' uri = markup_to_utf8(uri) - if uri.startswith("magnet:"): - log.debug("Adding torrent from magnet link: %s", uri) - session.add_torrent_from_magnet(add_dict, uri) - self.delete() - return - elif uri.startswith("file://"): - uri = uri[7:] + session.fill_add_dict_based_on_uri(add_dict, uri) + session.add_torrent_with_dict(add_dict) - if os.path.isfile(uri): - log.debug("Adding file: %s", uri) - session.add_torrent_from_file(add_dict, uri) - self.delete() - return - else: - log.debug("Adding torrent from info hash: %s", uri) - session.add_torrent_from_hash(add_dict, uri) - self.delete() - return + self.delete() ok_btn.callback_clicked_add( - add_torrent_cb, uri_entry, session, self.add_dict - ) + add_torrent_cb, uri_entry, session, self.add_dict) cancel_btn.callback_clicked_add(lambda x: self.delete()) self.show() diff --git a/epour/gui/Widgets.py b/epour/gui/Widgets.py index 7aa0f06..9e1dab9 100644 --- a/epour/gui/Widgets.py +++ b/epour/gui/Widgets.py @@ -1,15 +1,36 @@ +# +# Epour - A bittorrent client using EFL and libtorrent +# +# Copyright 2012-2016 Kai Huuhko <[email protected]> +# +# 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. +# + from efl.evas import EVAS_HINT_EXPAND, EVAS_HINT_FILL, Rectangle 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 -from efl.elementary.grid import Grid -from efl.elementary.fileselector import Fileselector -from efl.elementary.fileselector_button import FileselectorButton +from efl.elementary import Box +from efl.elementary import Spinner +from efl.elementary import Hoversel +from efl.elementary import Label +from efl.elementary import Notify +from efl.elementary import Popup +from efl.elementary import Button +from efl.elementary import Grid +from efl.elementary import Fileselector +from efl.elementary import FileselectorButton EXPAND_BOTH = EVAS_HINT_EXPAND, EVAS_HINT_EXPAND EXPAND_HORIZ = EVAS_HINT_EXPAND, 0.0 diff --git a/epour/gui/__init__.py b/epour/gui/__init__.py index e16c77c..cf31d1b 100644 --- a/epour/gui/__init__.py +++ b/epour/gui/__init__.py @@ -1,7 +1,7 @@ # # Epour - A bittorrent client using EFL and libtorrent # -# Copyright 2012-2014 Kai Huuhko <[email protected]> +# Copyright 2012-2016 Kai Huuhko <[email protected]> # # 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 @@ -34,20 +34,19 @@ 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, \ +from efl.elementary import Genlist, GenlistItemClass, \ ELM_GENLIST_ITEM_FIELD_TEXT, ELM_GENLIST_ITEM_FIELD_CONTENT, \ ELM_OBJECT_SELECT_MODE_NONE, ELM_OBJECT_SELECT_MODE_DEFAULT, \ 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 efl.elementary import StandardWindow +from efl.elementary import Icon +from efl.elementary import Box +from efl.elementary import Label +from efl.elementary import Panel, ELM_PANEL_ORIENT_BOTTOM +from efl.elementary import Table +from efl.elementary import Menu +from efl.elementary import Configuration +from efl.elementary import Toolbar, ELM_TOOLBAR_SHRINK_NONE from .Widgets import ConfirmExit, Error, Information, BlockGraph @@ -65,16 +64,16 @@ log = logging.getLogger("epour.gui") class MainInterface(object): + def __init__(self, parent, session): - self.parent = parent - self.session = session + self._session = session + self.itc = TorrentClass(self._session, "double_label") self.torrentitems = {} win = self.win = StandardWindow( "epour", "Epour", size=(480 * scale, 400 * scale), - screen_constrain=True - ) + screen_constrain=True) win.callback_delete_request_add(lambda x: self.quit()) mbox = Box(win, size_hint_weight=EXPAND_BOTH) @@ -85,17 +84,15 @@ class MainInterface(object): tb = Toolbar( win, size_hint_align=FILL_HORIZ, homogeneous=False, shrink_mode=ELM_TOOLBAR_SHRINK_NONE, - select_mode=ELM_OBJECT_SELECT_MODE_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() - ) + lambda x, y: self.add_torrent()) def pause_session(it): - self.session.pause() + self._session.pause() it.state_set(it.state_next()) def resume_session(it): @@ -104,42 +101,37 @@ class MainInterface(object): item = tb.item_append( "media-playback-pause", _("Pause Session"), - lambda tb, it: pause_session(it) - ) + lambda tb, it: pause_session(it)) item.state_add( "media-playback-start", _("Resume Session"), - lambda tb, it: resume_session(it) - ) + lambda tb, it: resume_session(it)) def prefs_general_cb(): from .Preferences import PreferencesGeneral - PreferencesGeneral(self, self.session).show() + PreferencesGeneral(self, self._session).show() def prefs_proxy_cb(): from .Preferences import PreferencesProxy - PreferencesProxy(self, self.session).show() + PreferencesProxy(self, self._session).show() def prefs_session_cb(): from .Preferences import PreferencesSession - PreferencesSession(self, self.session).show() + 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() - ) + lambda o, i: prefs_general_cb()) item.menu.item_add( None, _("Proxy"), "preferences-system", - lambda o, i: prefs_proxy_cb() - ) + lambda o, i: prefs_proxy_cb()) item.menu.item_add( None, _("Session"), "preferences-system", - lambda o, i: prefs_session_cb() - ) + lambda o, i: prefs_session_cb()) item = tb.item_append("application-exit", _("Exit"), - lambda tb, it: elm.exit()) + lambda tb, it: self.quit()) mbox.pack_start(tb) tb.show() @@ -148,12 +140,12 @@ class MainInterface(object): self.tlist = tlist = Genlist( mbox, select_mode=ELM_OBJECT_SELECT_MODE_DEFAULT, mode=ELM_LIST_COMPRESS, size_hint_weight=EXPAND_BOTH, - size_hint_align=FILL_BOTH, homogeneous=True - ) + size_hint_align=FILL_BOTH, homogeneous=True) def item_activated_cb(gl, item): - h = item.data - itm = ItemMenu(tlist, item, self.session, h) + torrent = item.data + handle = torrent.handle + itm = ItemMenu(tlist, item, self._session, handle) itm.focus = True tlist.callback_activated_add(item_activated_cb) @@ -167,8 +159,8 @@ class MainInterface(object): p = Panel( topbox, size_hint_weight=EXPAND_BOTH, size_hint_align=FILL_BOTH, - color=(200, 200, 200, 200), orient=ELM_PANEL_ORIENT_BOTTOM - ) + color=(200, 200, 200, 200), orient=ELM_PANEL_ORIENT_BOTTOM) + p.content = SessionStatus(win, session) p.hidden = True p.show() @@ -182,28 +174,31 @@ class MainInterface(object): def torrent_added_cb(a): h = a.handle - self.add_torrent_item(h) + info_hash = h.info_hash() + log.debug("Torrent added: %s" % str(info_hash)) + torrent = session.torrents[str(info_hash)] + self.add_torrent_item(torrent) 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) + # 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) + # session.alert_manager.callback_add( + # "torrent_update_alert", torrent_update_cb) - def torrent_paused_cb(a): - self.update_icon(a.handle) + # 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) + # 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 @@ -211,16 +206,36 @@ class MainInterface(object): log.debug("State changed for invalid handle.") return - ihash = str(h.info_hash()) - if not ihash in self.torrentitems: - log.debug("%s state changed but not in list", str(ihash)) + info_hash = str(h.info_hash()) + if info_hash not in self.torrentitems: + log.debug("%s state changed but not in list", str(info_hash)) return - self.update_icon(h) + torrent = self._session.torrents[info_hash] + torrent.state = a.state + + self.torrentitems[info_hash].fields_update( + "elm.swallow.icon", ELM_GENLIST_ITEM_FIELD_CONTENT + ) session.alert_manager.callback_add( "state_changed_alert", state_changed_cb) + # def state_update_alert_cb(a): + # statuses = a.status + # for status in statuses: + # info_hash = str(status.info_hash) + + # if info_hash not in self.torrentitems: + # return + + # self.torrentitems[info_hash].fields_update( + # "*", ELM_GENLIST_ITEM_FIELD_TEXT + # ) + + # session.alert_manager.callback_add( + # "state_update_alert", state_update_alert_cb) + def torrent_finished_cb(a): msg = _("Torrent {} has finished downloading.").format( cgi.escape(a.handle.name()) @@ -234,9 +249,9 @@ class MainInterface(object): def add_torrent(self, t_uri=None): from .TorrentSelector import TorrentSelector - TorrentSelector(self.win, self.session, t_uri) + TorrentSelector(self.win, self._session, t_uri) - def run(self): + def run_mainloop(self): self.win.show() self.timer = Timer(1.0, self.update) @@ -245,45 +260,27 @@ class MainInterface(object): self.win.callback_iconified_add(lambda x: self.timer.freeze()) self.win.callback_normal_add(lambda x: self.timer.thaw()) elm.run() - elm.shutdown() + + def stop_mainloop(self): + elm.exit() def update(self): #log.debug("Torrent list TICK") for v in self.tlist.realized_items_get(): v.fields_update("*", ELM_GENLIST_ITEM_FIELD_TEXT) + self._session.post_torrent_updates(64) 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 _torrent_item_tooltip_cb(self, gl, it, tooltip, h): - if not h.is_valid(): - return - - s = h.status(8) - - if not s.has_metadata: - return - - tt = TorrentTooltip(tooltip, h, s) + def _torrent_item_tooltip_cb(self, gl, it, tooltip, session, torrent): + return TorrentTooltip(tooltip, session, torrent) - return tt + def add_torrent_item(self, torrent): + info_hash = torrent.info_hash - def add_torrent_item(self, h): - ihash = str(h.info_hash()) - - itc = TorrentClass(self.session, "double_label") - item = self.tlist.item_append(itc, h) - item.tooltip_content_cb_set(self._torrent_item_tooltip_cb, h) + item = self.tlist.item_append(self.itc, torrent) + item.tooltip_content_cb_set(self._torrent_item_tooltip_cb, self._session, torrent) item.tooltip_window_mode_set(True) - self.torrentitems[ihash] = item + self.torrentitems[str(info_hash)] = item def remove_torrent_item(self, info_hash): it = self.torrentitems.pop(info_hash, None) @@ -294,10 +291,14 @@ class MainInterface(object): Error(self.win, title, text) def quit(self, *args): - if self.session.conf.getboolean("Settings", "confirm_exit"): - ConfirmExit(self.win, elm.exit) + if self._session.conf.getboolean("Settings", "confirm_exit"): + ConfirmExit(self.win, self._quit_cb) else: - elm.exit() + self._quit_cb() + + def _quit_cb(self): + self.win.hide() + self._session.shutdown() class SessionStatus(Table): @@ -306,7 +307,7 @@ class SessionStatus(Table): def __init__(self, parent, session): Table.__init__(self, parent) - self.session = session + self._session = session s = session.status() @@ -398,30 +399,29 @@ class SessionStatus(Table): self.update_timer = Timer(1.0, self.update) self.on_del_add(lambda x: self.update_timer.delete()) self.top_widget.callback_withdrawn_add( - lambda x: self.update_timer.freeze() - ) + lambda x: self.update_timer.freeze()) self.top_widget.callback_iconified_add( - lambda x: self.update_timer.freeze() - ) + lambda x: self.update_timer.freeze()) self.top_widget.callback_normal_add(lambda x: self.update_timer.thaw()) def update(self): #log.debug("Session status TICK") - s = self.session.status() + 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) + t = "%s/%s" % (str(s.num_unchoked), str(s.allowed_upload_slots)) self.uploads_l.text = t - self.listen_l.text = str(self.session.is_listening()) - if self.session.is_paused(): - icon = "media-playback-pause" - else: - icon = "media-playback-play" + + self.listen_l.text = str(self._session.is_listening()) + + icon = "media-playback-pause" if self._session.is_paused() else "media-playback-play" try: self.ses_pause_ic.standard = icon - except Exception as e: - log.debug(e) + except Exception: + pass return True @@ -431,54 +431,53 @@ class TorrentClass(GenlistItemClass): state_str = ( _('Queued'), _('Checking'), _('Downloading metadata'), _('Downloading'), _('Finished'), _('Seeding'), _('Allocating'), - _('Checking resume data') - ) + _('Checking resume data')) log = logging.getLogger("epour.gui.torrent_list") def __init__(self, session, *args, **kwargs): GenlistItemClass.__init__(self, *args, **kwargs) - self.session = session + self._session = session def text_get(self, obj, part, item_data): - h = item_data + torrent = item_data + handle = torrent.handle + + if not handle.is_valid(): + return _("Invalid torrent") if part == "elm.text": - name = h.name() - return '%s' % ( - name - ) + return '%s' % (torrent.status.name) elif part == "elm.text.sub": - if not h.is_valid(): - return _("Invalid torrent") - s = h.status(0) - qp = h.queue_position() + status = torrent.status + qp = handle.queue_position() if qp == -1: qp = "seeding" - return _("{0:.0%} complete, ETA: {1} " + return _( + "{0:.0%} complete, ETA: {1} " "(Down: {2}/s Up: {3}/s Queue pos: {4})").format( - s.progress, - timedelta(seconds=self.get_eta(s)), - intrepr(s.download_payload_rate, precision=0), - intrepr(s.upload_payload_rate, precision=0), - qp, - ) + status.progress, + timedelta(seconds=self.get_eta(status)), + intrepr(status.download_payload_rate, precision=0), + intrepr(status.upload_payload_rate, precision=0), + qp) def content_get(self, obj, part, item_data): if part != "elm.swallow.icon": return - h = item_data + torrent = item_data + handle = torrent.handle - if not h.is_valid(): + if not handle.is_valid(): return - s = h.status(0) + status = torrent.status ic = Icon(obj) try: - if h.is_paused(): + if status.paused: try: ic.standard = "player_pause" except Exception: @@ -486,7 +485,7 @@ class TorrentClass(GenlistItemClass): ic.standard = "media-playback-pause" except Exception: pass - elif h.is_seed(): + elif status.is_seeding: try: ic.standard = "up" except Exception: @@ -504,16 +503,16 @@ class TorrentClass(GenlistItemClass): pass except RuntimeError: log.debug("Setting torrent ic failed") - ic.tooltip_text_set(self.state_str[s.state]) + ic.tooltip_text_set(self.state_str[torrent.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"]: + # if s.is_seeding 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 + # stop_ratio = self._session.settings().share_ratio_limit # return ( # (s.all_time_download * stop_ratio) - # s.all_time_upload @@ -530,6 +529,7 @@ class TorrentClass(GenlistItemClass): class ItemMenu(Menu): + def __init__(self, parent, item, session, h): Menu.__init__(self, parent) @@ -542,29 +542,22 @@ class ItemMenu(Menu): ) q = self.item_add(None, _("Queue"), None, None) self.item_add( - q, _("Up"), None, lambda x, y: h.queue_position_up() - ) + q, _("Up"), None, lambda x, y: h.queue_position_up()) self.item_add( - q, _("Down"), None, lambda x, y: h.queue_position_down() - ) + q, _("Down"), None, lambda x, y: h.queue_position_down()) self.item_add( - q, _("Top"), None, lambda x, y: h.queue_position_top() - ) + q, _("Top"), None, lambda x, y: h.queue_position_top()) self.item_add( - q, _("Bottom"), None, lambda x, y: h.queue_position_bottom() - ) + 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.remove_torrent_cb, item, session, h, False) self.item_add( rem, _("and data files"), None, - self.remove_torrent_cb, item, session, h, True - ) + self.remove_torrent_cb, item, session, h, True) self.item_separator_add(None) it = self.item_add( - None, _("Force reannounce"), None, lambda x, y: h.force_reannounce() - ) + None, _("Force reannounce"), None, lambda x, y: h.force_reannounce()) it.tooltip_text_set( "<b>Force reannounce</b> will force this torrent<br>" "to do another tracker request, to receive new<br>" @@ -573,26 +566,21 @@ class ItemMenu(Menu): "since the last announce, the forced announce<br>" "will be scheduled to happen immediately as<br>" "the min_interval expires. This is to honor<br>" - "trackers minimum re-announce interval settings." - ) + "trackers minimum re-announce interval settings.") self.item_add( None, _("Force DHT reannounce"), None, - lambda x, y: h.force_dht_announce() - ) + lambda x, y: h.force_dht_announce()) it = self.item_add( None, _("Scrape tracker"), None, - lambda x, y: h.scrape_tracker() - ) + lambda x, y: h.scrape_tracker()) it.tooltip_text_set( "<b>Scrape tracker</b> will send a scrape request to the<br>" "tracker. A scrape request queries the tracker for<br>" "statistics such as total number of incomplete peers,<br>" - "complete peers, number of downloads etc." - ) + "complete peers, number of downloads etc.") it = self.item_add( None, _("Force re-check"), None, - lambda x, y: h.force_recheck() - ) + lambda x, y: h.force_recheck()) it.tooltip_text_set( "force_recheck puts the torrent back in a state<br>" "where it assumes to have no resume data.<br>" @@ -602,30 +590,25 @@ class ItemMenu(Menu): "checked (all the files will be read and compared<br>" "to the piece hashes).<br>" "Once the check is complete, the torrent will start<br>" - "connecting to peers again, as normal." - ) + "connecting to peers again, as normal.") self.item_separator_add(None) it = self.item_add( None, _("Torrent properties"), None, - self.torrent_props_cb, h - ) + self.torrent_props_cb, h) def files_cb(menu, it, h): from .TorrentProps import TorrentFiles TorrentFiles(h) self.item_add( it, "Files", None, - files_cb, h - ) + files_cb, h) self.move(*item.track_object.pos) del item.track_object self.show() - def remove_torrent_cb( - self, menu, item, glitem, session, h, with_data=False - ): + def remove_torrent_cb(self, menu, item, glitem, session, h, with_data=False): menu.close() session.remove_torrent(h, with_data) @@ -657,12 +640,22 @@ class TorrentTooltip(Table): (_("Total uploaded this session"), intrepr, "total_payload_upload"), (_("Total failed"), intrepr, "total_failed_bytes"), (_("Number of seeds"), None, "num_seeds"), - (_("Number of peers"), None, "num_peers"), - ) + (_("Number of peers"), None, "num_peers")) bold = "font_weight=Bold" - def __init__(self, parent, h, s): + def __init__(self, parent, session, torrent): + + handle = torrent.handle + + if not handle.is_valid(): + raise ValueError("Invalid handle") + + flags = lt.status_flags_t.query_pieces + status = handle.status(flags) + + # if not s.has_metadata: + # return Table.__init__(self, parent, size_hint_weight=EXPAND_BOTH) @@ -673,7 +666,7 @@ class TorrentTooltip(Table): l1 = Label(self, text=desc) l1.show() self.pack(l1, 0, i, 1, 1) - v = getattr(s, attr_name) + v = getattr(status, attr_name) if conv: if conv == datetime.fromtimestamp and v == 0: v = _("N/A") @@ -693,27 +686,27 @@ class TorrentTooltip(Table): WIDTH = 30 HEIGHT = 4 - g = BlockGraph( - self, s.pieces, s.num_pieces, - size=(WIDTH, HEIGHT), size_hint_align=FILL_BOTH - ) + graph = BlockGraph( + self, status.pieces, status.num_pieces, + size=(WIDTH, HEIGHT), size_hint_align=FILL_BOTH) l.text = "".join(( "<font %s>" % (self.bold), - _("Pieces (scaled 1:%d)") % (g.block_size), - "</>" - )) + _("Pieces (scaled 1:%d)") % (graph.block_size), + "</>")) - self.pack(g, 1, i, 1, 1) - g.show() + self.pack(graph, 1, i, 1, 1) + graph.show() - self.timer = Timer(1.0, self.update, h, self.items, value_labels, g) + self.timer = Timer(1.0, self.update, torrent, self.items, value_labels, graph) self.on_del_add(lambda x: self.timer.delete()) @staticmethod - def update(h, items, value_labels, g): + def update(torrent, items, value_labels, g): #log.debug("Tooltip TICK") - s = h.status(8) + handle = torrent.handle + flags = lt.status_flags_t.query_pieces + s = handle.status(flags) for i, l in enumerate(value_labels): conv, attr_name = items[i][1:] v = getattr(s, attr_name) diff --git a/epour/session.py b/epour/session.py index e13d945..8acfedb 100644 --- a/epour/session.py +++ b/epour/session.py @@ -1,7 +1,7 @@ # # Epour - A bittorrent client using EFL and libtorrent # -# Copyright 2012-2015 Kai Huuhko <[email protected]> +# Copyright 2012-2016 Kai Huuhko <[email protected]> # # 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 @@ -19,14 +19,9 @@ # MA 02110-1301, USA. # -import sys import os import mimetypes -import urllib -try: - import urlparse -except ImportError: - from urllib import parse as urlparse +from urllib.parse import urlparse, urlsplit import logging import shutil try: @@ -42,10 +37,63 @@ from efl.ecore import Timer from xdg.BaseDirectory import save_data_path, load_data_paths +log = logging.getLogger("epour.session") + + +flags_t = lt.add_torrent_params_flags_t +default_flags = ( + flags_t.flag_apply_ip_filter + + flags_t.flag_update_subscribe + + flags_t.flag_duplicate_is_error + + flags_t.flag_auto_managed) + + +def read_torrent_file(info_hash): + if not info_hash: + log.debug("Tried to read torrent with invalid info_hash.") + return + + info_hash = str(info_hash) + + log.debug("Reading torrent file %s.torrent", info_hash) + + paths = load_data_paths("epour") + for p in paths: + t_path = os.path.join(p, "%s.torrent" % info_hash) + if os.path.exists(t_path): + ti = lt.torrent_info(t_path) + return ti + + +def read_resume_data(info_hash): + log.debug("Reading resume data for %s", info_hash) + + data = None + + paths = load_data_paths("epour") + for p in paths: + t_path = os.path.join(p, "%s.fastresume" % info_hash) + if os.path.exists(t_path): + with open(t_path, "rb") as fp: + data = lt.read_resume_data(fp.read()) + break + + if data: + return data + else: + raise ValueError("Fast Resume data not found") + + class Session(lt.session): - def __init__(self, conf): + + def __init__(self, conf, shutdown_cb): self.conf = conf - self.log = logging.getLogger("epour.session") + self._shutdown_cb = shutdown_cb + self._shutdown_timer = None + self._torrents_changed = False + + self.torrents = OrderedDict() + self._outstanding_resume_data = 0 from epour import __version__ as version ver_ints = [] @@ -54,7 +102,7 @@ class Session(lt.session): ver_ints.append(0) fp = lt.fingerprint("EP", *ver_ints) - self.log.debug("peer-id: {}".format(fp)) + log.debug("peer-id: %s", fp) lt.session.__init__( self, @@ -64,9 +112,7 @@ class Session(lt.session): #lt.session_flags_t.start_default_features ) - self.log.info("Session started") - - self.torrents = OrderedDict() + log.info("Session started") #rsdpipsdtsppe #stheprtertoer @@ -80,14 +126,32 @@ class Session(lt.session): self.listen_on( conf.getint("Settings", "listen_low"), - conf.getint("Settings", "listen_high") - ) + conf.getint("Settings", "listen_high")) self.alert_manager = AlertManager(self) - self.alert_manager.callback_add( - "add_torrent_alert", self._add_torrent_cb) - self.alert_manager.callback_add( - "metadata_received_alert", self._metadata_received_cb) + self.alert_manager.callback_add("add_torrent_alert", self._add_torrent_cb) + self.alert_manager.callback_add("metadata_received_alert", self._metadata_received_cb) + + def save_resume_alert_cb(a): + handle = a.handle + if handle is None: + log.error("Tried to write resume data with invalid handle.") + return + + info_hash = str(handle.info_hash()) + data = a.resume_data + + self.torrents[info_hash].write_resume_data(data) + + self._outstanding_resume_data -= 1 + log.debug("Resume data written for %s", info_hash) + + def save_resume_failed_alert_cb(a): + log.error(a.message) + self._outstanding_resume_data -= 1 + + self.alert_manager.callback_add("save_resume_data_alert", save_resume_alert_cb) + self.alert_manager.callback_add("save_resume_data_failed_alert", save_resume_failed_alert_cb) def torrent_finished_move_cb(a): h = a.handle @@ -100,22 +164,51 @@ class Session(lt.session): self.alert_manager.callback_add( "torrent_finished_alert", torrent_finished_move_cb) + def periodic_save_func(): + self.save_resume_data() + if self._torrents_changed: + self.save_torrents() + self._torrents_changed = False + return True + + self.periodic_save_timer = Timer(15.0, periodic_save_func) + + def state_update_alert_cb(a): + statuses = a.status + for status in statuses: + info_hash = status.info_hash + self.torrents[str(info_hash)].status = status + + self.alert_manager.callback_add("state_update_alert", state_update_alert_cb) + def _add_torrent_cb(self, a): e = a.error if e.value() > 0: - self.log.error("Adding torrent failed: %r" % (e.message())) + params = a.params + info_hash = str(params["info_hash"]) + if info_hash in self.torrents: + del self.torrents[info_hash] + log.error("Adding torrent %s failed: %s", info_hash, e.message()) return - h = a.handle - ihash = str(h.info_hash()) - self.torrents[ihash] = a.params - self.log.debug("Torrent added.") + handle = a.handle + info_hash = handle.info_hash() + + log.debug("Torrent %s added", info_hash) + + if str(info_hash) in self.torrents: + log.debug("Torrent already in list, setting session") + self.torrents[str(info_hash)].set_session(self) + else: + torrent = Torrent(self, info_hash) + self.torrents[str(info_hash)] = torrent + self._torrents_changed = True def _metadata_received_cb(self, a): - h = a.handle - ihash = str(h.info_hash()) - self.log.debug("Metadata received.") - t_info = h.get_torrent_info() - self.torrents[ihash]["ti"] = t_info + handle = a.handle + info_hash = handle.info_hash() + log.debug("Metadata received for %s", str(info_hash)) + torrent = self.torrents[str(info_hash)] + torrent.add_metadata() def load_state(self): for p in load_data_paths("epour"): @@ -126,18 +219,20 @@ class Session(lt.session): state = lt.bdecode(f.read()) lt.session.load_state(self, state) except Exception as e: - self.log.debug("Could not load previous session state.") - self.log.debug(e) + log.debug("Could not load previous session state.") + log.debug(e) else: - self.log.info("Session restored from disk.") + log.info("Session restored from disk.") break settings = self.settings() + from epour import __version__ as version version += ".0" ver_s = "Epour/{} libtorrent/{}".format(version, lt.version) settings.user_agent = ver_s - self.log.debug("User agent: {}".format(ver_s)) + log.debug("User agent: %s", ver_s) + self.set_settings(settings) def save_state(self): @@ -147,7 +242,7 @@ class Session(lt.session): with open(path, 'wb') as f: f.write(lt.bencode(state)) - self.log.debug("Session state saved.") + log.debug("Session state saved.") def load_torrents(self): for p in load_data_paths("epour"): @@ -156,204 +251,171 @@ class Session(lt.session): break if not torrents_path: - self.info.debug("No previous list of torrents found.") + log.debug("Previous list of torrents not found.") return try: pkl_file = open(torrents_path, 'rb') except IOError: - self.log.warning("Could not open the list of torrents.") + log.warning("Could not open the list of torrents.") else: try: torrents = cPickle.load(pkl_file) except Exception: - self.log.exception("Opening the list of torrents failed.") + log.exception("Opening the list of torrents failed.") else: - self.log.debug( + log.debug( "List of torrents opened, " - "restoring {} torrents.".format(len(torrents)) + "restoring %d torrents.", len(torrents) ) - for i, t in torrents.items(): + for info_hash, torrent in torrents.items(): + log.debug("Restoring torrent %s", info_hash) + self.torrents[info_hash] = torrent + + params_dict = torrent.get_params() + + params = None + + if "ti" in params_dict and params_dict["ti"]: + try: + ti = read_torrent_file(info_hash) + except Exception: + log.exception("Opening torrent %s failed", info_hash) + else: + params_dict["ti"] = ti + try: + params = read_resume_data(info_hash) + except Exception: + pass + else: + params.trackers = list(set(params.trackers)) + + if params is None: + params = lt.add_torrent_params() + + for k, v in params_dict.items(): + setattr(params, k, v) + try: - for k, v in t.items(): - if v is None: - continue - elif k == "ti": - # Epour <= 0.6 compat - if isinstance(v, dict): - t[k] = lt.torrent_info(lt.bdecode(v)) - else: - t[k] = lt.bdecode(v) - # elif k == "info_hash": - # torrents[i][k] = lt.big_number(v) + self.async_add_torrent(params) except Exception: - self.log.exception("Opening torrent %s failed", i) + log.exception("Opening torrent %s failed", info_hash) 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 == "info_hash": - if v.is_all_zeros(): - del self.torrents[i][k] - else: - self.torrents[i][k] = v.to_bytes() - - handles = self.get_torrents() - for h in handles: - if h.is_valid(): - i = str(h.info_hash()) - t_dict = self.torrents[i] - t_dict["save_path"] = h.save_path() - s = h.status(0) - if s.has_metadata: - resume_data = lt.bencode(h.write_resume_data()) - t_dict["resume_data"] = resume_data - t_info = h.get_torrent_info() - t_dict["ti"] = lt.bencode(t_info) + def save_resume_data(self): + for handle in self.get_torrents(): + if not handle.is_valid(): + log.error("Invalid handle while trying to save resume data") + continue + status = handle.status(0) + if not status.has_metadata: + continue + if not status.need_save_resume: + continue + + handle.save_resume_data() + self._outstanding_resume_data += 1 + + def shutdown(self): + self.pause() + + self.save_resume_data() + + def _check_outstanding(): + if self._outstanding_resume_data == 0: + self.save_torrents() + self.save_state() + self._shutdown_cb() + return False else: - self.log.debug("Handle is invalid, skipping") - - path = os.path.join(save_data_path("epour"), "torrents") - with open(path, 'wb') as f: - cPickle.dump(self.torrents, f, protocol=cPickle.HIGHEST_PROTOCOL) - - self.log.debug("List of torrents saved.") - - # 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.log.debug("Writing torrent file {}".format(ihash)) - - # md = lt.bdecode(t_info.metadata()) - # t = {} - # t["info"] = md - - # p = save_data_path("epour") - # t_path = os.path.join(p, "{0}.torrent".format(ihash)) + return True - # if t_path: - # with open(t_path, "wb") as f: - # f.write(lt.bencode(t)) + self._shutdown_timer = Timer(1.0, _check_outstanding) - # return t_path + def save_torrents(self): + for info_hash, torrent in self.torrents.items(): + info_hash2 = str(torrent.handle.info_hash()) + assert info_hash == info_hash2, "%s is not %s" % (info_hash, info_hash2) + path = os.path.join(save_data_path("epour"), "torrents") + try: + data = cPickle.dumps(self.torrents, protocol=cPickle.HIGHEST_PROTOCOL) + except Exception: + log.exception("Failed to save torrents") + else: + with open(path, 'wb') as fp: + fp.write(data) + log.debug("List of torrents saved.") def remove_torrent(self, h, with_data=False): - ihash = str(h.info_hash()) + info_hash = str(h.info_hash()) + + torrent = self.torrents[info_hash] + torrent.delete_torrent_file() + torrent.delete_resume_data() - del self.torrents[ihash] + del self.torrents[info_hash] lt.session.remove_torrent(self, h, option=with_data) - for p in load_data_paths("epour"): - fr_path = os.path.join( - p, "{0}.fastresume".format(ihash) - ) + self._torrents_changed = True - try: - with open(fr_path): - pass - except IOError: - self.log.debug("Could not remove %s", fr_path) - else: - os.remove(fr_path) + def add_torrent_with_uri(self, uri): + storage_path = self.conf.get("Settings", "storage_path") + #default_flags = self.conf.get("Settings", "default_flags") - t_path = None - for p in load_data_paths("epour"): - t_path = os.path.join(p, "{0}.torrent".format(ihash)) - break + add_dict = { + "save_path": storage_path, + "flags": default_flags, + } - if t_path: - try: - with open(t_path): - pass - except IOError: - self.log.debug("Could not remove torrent file.") - else: - os.remove(t_path) + self.fill_add_dict_based_on_uri(add_dict, uri) - if not hasattr(lt, "torrent_removed_alert"): - class torrent_removed_alert(object): - def __init__(self, h, info_hash): - self.handle = h - self.info_hash = info_hash + self.add_torrent_with_dict(add_dict) - a = torrent_removed_alert(h, ihash) + def fill_add_dict_based_on_uri(self, add_dict, uri): + parsed_uri = urlparse(uri) - self.alert_manager.signal(a) + if parsed_uri.scheme == "magnet": + add_dict["url"] = uri + elif parsed_uri.scheme == "file" or parsed_uri.scheme == "" and os.path.isfile(parsed_uri.path): + path = parsed_uri.path - return ihash + mimetype = mimetypes.guess_type(path)[0] + if not mimetype == "application/x-bittorrent": + log.warning("%s is not of a known torrent file type", path) - 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 + with open(path, 'rb') as t: + t_raw = lt.bdecode(t.read()) - if t_uri.startswith("file://"): - t_uri = urllib.unquote(urlparse.urlsplit(t_uri).path) + info = lt.torrent_info(t_raw) - with open(t_uri, 'rb') as t: - t_raw = lt.bdecode(t.read()) + add_dict["ti"] = info - info = lt.torrent_info(t_raw) - add_dict["ti"] = info + ihash = str(info.info_hash()) + path_dir = save_data_path("epour") + new_path = os.path.join(path_dir, "{0}.torrent".format(ihash)) - rd = None - fr_file_name = "{}.fastresume".format(info.info_hash()) - for p in load_data_paths("epour"): - path = os.path.join(p, fr_file_name) - if os.path.isfile(path): - try: - with open(path, "rb") as f: - rd = f.read() - except Exception: - self.log.debug("Invalid resume data") - else: - add_dict["resume_data"] = rd - break + if path == new_path: + pass + else: + shutil.copy(path, new_path) - ihash = str(info.info_hash()) + if self.conf.getboolean("Settings", "delete_original"): + log.debug( + "Deleting original torrent file %s", path) + os.remove(path) - path = save_data_path("epour") - new_uri = os.path.join(path, "{0}.torrent".format(ihash)) + path = new_path - if t_uri == new_uri: - pass + elif len(uri) == 40: # looks like a sha1 string + add_dict["info_hash"] = uri + # elif uri.scheme == "http" or uri.scheme == "https": + # 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 + raise RuntimeError("Could not parse the torrent string.") - self.async_add_torrent(add_dict) - - def add_torrent_from_magnet(self, add_dict, t_uri): - self.log.debug("Adding %r", t_uri) - t_uri = t_uri.encode("ascii") - tmp_dict = lt.parse_magnet_uri(t_uri) - tmp_dict.update(add_dict) - tmp_dict["info_hash"] = tmp_dict["info_hash"].to_bytes() - self.async_add_torrent(tmp_dict) - - def add_torrent_from_hash(self, add_dict, t_uri): - t_uri = t_uri.encode("ascii") - add_dict["info_hash"] = t_uri - self.log.debug("Adding %s", t_uri) + def add_torrent_with_dict(self, add_dict): self.async_add_torrent(add_dict) @@ -383,18 +445,255 @@ class AlertManager(object): a_name = type(a).__name__ if a_name not in self.alerts: - self.log.debug("No handler: {} | {}".format(a_name, a)) + log.debug("No handler: %s | %s", a_name, a) return for cb, args, kwargs in self.alerts[a_name]: try: cb(a, *args, **kwargs) except: - self.log.exception("Exception while handling alerts") + log.exception("Exception while handling alerts") def update(self): - #self.log.debug("Alerts TICK") for a in self.session.pop_alerts(): self.signal(a) return True + + +def status_to_flags(status): + #flags_t = lt.add_torrent_params_flags_t + flags = 0 + flags += 1 if status.is_seeding else 0 + #flags += 2 deprecated + flags += 4 if status.upload_mode else 0 + flags += 8 if status.share_mode else 0 + flags += 16 if status.ip_filter_applies else 0 + flags += 32 if status.paused else 0 + flags += 64 # auto_managed + flags += 128 # duplicate_is_error + #flags += 256 deprecated + flags += 512 # update_subscribe + flags += 1024 if status.super_seeding else 0 + flags += 2048 if status.sequential_download else 0 + #flags += 4096 if pinned else 0 + #flags += 8192 if stop_when_ready else 0 + #flags += 16384 if override_trackers else 0 + #flags += 32768 if override_web_seeds else 0 + #flags += 65536 deprecated + #flags += 131072 if override_resume_data else 0 + #flags += 262144 if merge_resume_trackers else 0 + #flags += 524288 if use_resume_save_path else 0 + #flags += 1048576 if merge_resume_http_seeds else 0 + + return flags + + +class Torrent(object): + + def __init__(self, session, info_hash, options={}): + assert isinstance(session, lt.session) + assert isinstance(info_hash, lt.sha1_hash) + + self.session = session + self.info_hash = info_hash + self.options = options + + self._status = None + + # @property + # def info_hash(self): + # return self.handle.info_hash() + + @property + def handle(self): + return self.session.find_torrent(self.info_hash) + + @property + def state(self): + if getattr(self, "_state", None): + return self._state + else: + status = self.handle.status(0) + state = status.state + self._state = state + return state + + @state.setter + def state(self, value): + self._state = value + + @property + def status(self): + if getattr(self, "_status", None): + return self._status + else: + status = self.handle.status(64) + self._status = status + return status + + @status.setter + def status(self, value): + self._status = value + + def get_params(self): + return self._params + + def _get_params(self): + handle = self.handle + status = handle.status() + flags = status_to_flags(status) + + # trackers = [] + + # for tracker in handle.trackers(): + # trackers.append(tracker["url"]) + + # trackers = ",".join(trackers) + + params = { + #"trackers": trackers, + "url_seeds": handle.url_seeds(), + #"dht_nodes": + "name": status.name, + "save_path": status.save_path, + "storage_mode": status.storage_mode, + #"storage": + #"userdata": + "file_priorities": handle.file_priorities(), + #"trackerid": + #"url": + #"uuid": + #"source_feed_url": + "flags": flags, + #"info_hash": handle.info_hash().to_bytes(), + "info_hash": self.info_hash, + "max_uploads": handle.max_uploads(), + "max_connections": handle.max_connections(), + "upload_limit": handle.upload_limit(), + "download_limit": handle.download_limit(), + } + if self.has_metadata: + params["ti"] = self.torrent_file_path + return params + + def __getstate__(self): + state = self.__dict__.copy() + + params = self._get_params() + info_hash1 = params["info_hash"].to_bytes() + params["info_hash"] = info_hash1 + state["_params"] = params + + info_hash2 = state["info_hash"].to_bytes() + state["info_hash"] = info_hash2 + + del state["session"] + del state["_status"] + del state["_state"] + return state + + def __setstate__(self, state): + params = state["_params"] + info_hash1 = params["info_hash"] + info_hash1 = lt.sha1_hash(info_hash1) + params["info_hash"] = info_hash1 + + info_hash2 = state["info_hash"] + info_hash2 = lt.sha1_hash(info_hash2) + state["info_hash"] = info_hash1 + + self.__dict__.update(state) + + def set_session(self, session): + self.session = session + + @property + def has_metadata(self): + return self.handle.status(0).has_metadata + + @property + def torrent_file_path(self): + paths = load_data_paths("epour") + for p in paths: + t_path = os.path.join(p, "{0}.torrent".format(self.info_hash)) + if os.path.exists(t_path): + return t_path + + return None + + def write_torrent_file(self): + assert self.handle is not None + + handle = self.handle + info_hash = str(handle.info_hash()) + + log.debug("Writing torrent file {0}.torrent".format(info_hash)) + + p = save_data_path("epour") + t_path = os.path.join(p, "{0}.torrent".format(info_hash)) + + if t_path: + t_info = handle.torrent_file() + metadata = lt.bdecode(t_info.metadata()) + torrent_file = {"info": metadata} + with open(t_path, "wb") as fp: + fp.write(lt.bencode(torrent_file)) + + log.debug("Torrent file was written to %s" % t_path) + + return t_path + + def write_resume_data(self, data): + assert self.handle is not None + + info_hash = self.handle.info_hash() + info_hash = str(info_hash) + + log.debug("Writing resume data for {}".format(info_hash)) + + t_path = os.path.join( + save_data_path("epour"), + info_hash + ".fastresume") + + if t_path: + with open(t_path, "wb") as f: + f.write(lt.bencode(data)) + else: + return + + return t_path + + def delete_torrent_file(self): + assert self.handle is not None + + info_hash = str(self.handle.info_hash()) + + for p in load_data_paths("epour"): + t_path = os.path.join(p, "{0}.torrent".format(info_hash)) + try: + with open(t_path): + pass + except IOError: + continue + else: + os.remove(t_path) + break + + def delete_resume_data(self): + assert self.handle is not None + + info_hash = str(self.handle.info_hash()) + + for p in load_data_paths("epour"): + fr_path = os.path.join(p, "{0}.fastresume".format(info_hash)) + try: + with open(fr_path): + pass + except IOError: + continue + else: + os.remove(fr_path) + + def add_metadata(self): + self.write_torrent_file() diff --git a/po/epour.pot b/po/epour.pot index ac0dff1..35a65c4 100644 --- a/po/epour.pot +++ b/po/epour.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-01-20 22:04+0900\n" +"POT-Creation-Date: 2016-08-04 18:18+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <[email protected]>\n" @@ -17,172 +17,115 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: ../epour/gui/TorrentProps.py:90 -msgid "Enable/disable file download" -msgstr "" - -#: ../epour/gui/TorrentProps.py:124 -msgid "Invalid torrent handle." -msgstr "" - -#: ../epour/gui/TorrentProps.py:149 -msgid "Torrent info" -msgstr "" - -#: ../epour/gui/TorrentProps.py:157 -msgid "Torrent settings" -msgstr "" - -#: ../epour/gui/TorrentProps.py:165 -msgid "Torrent status" -msgstr "" - -#: ../epour/gui/TorrentProps.py:174 -msgid "Magnet URI" -msgstr "" - -#: ../epour/gui/TorrentProps.py:182 -msgid "Copy" -msgstr "" - -#: ../epour/gui/TorrentProps.py:209 -#, python-format -msgid "Epour - Files for torrent: %s" -msgstr "" - -#: ../epour/gui/TorrentProps.py:251 -msgid "Select all" -msgstr "" - -#: ../epour/gui/TorrentProps.py:255 -msgid "Select none" -msgstr "" - -#: ../epour/gui/TorrentProps.py:394 -msgid "Private" -msgstr "" - -#: ../epour/gui/TorrentProps.py:445 -msgid "Storage path" -msgstr "" - -#: ../epour/gui/TorrentProps.py:451 -msgid "Select" -msgstr "" - -#: ../epour/gui/TorrentProps.py:503 -msgid "disabled" -msgstr "" - -#: ../epour/gui/TorrentProps.py:539 ../epour/gui/__init__.py:432 -msgid "Queued" -msgstr "" - -#: ../epour/gui/TorrentProps.py:539 ../epour/gui/__init__.py:432 -msgid "Checking" -msgstr "" - -#: ../epour/gui/TorrentProps.py:539 ../epour/gui/__init__.py:432 -msgid "Downloading metadata" -msgstr "" - -#: ../epour/gui/TorrentProps.py:539 ../epour/gui/__init__.py:433 -msgid "Downloading" -msgstr "" - -#: ../epour/gui/TorrentProps.py:540 ../epour/gui/__init__.py:433 -msgid "Finished" -msgstr "" - -#: ../epour/gui/TorrentProps.py:540 ../epour/gui/__init__.py:433 -msgid "Seeding" -msgstr "" - -#: ../epour/gui/TorrentProps.py:540 ../epour/gui/__init__.py:433 -msgid "Allocating" -msgstr "" - -#: ../epour/gui/TorrentProps.py:540 ../epour/gui/__init__.py:434 -msgid "Checking resume data" -msgstr "" - -#: ../epour/gui/Widgets.py:109 +#: ../epour/gui/Widgets.py:130 msgid "Close" msgstr "" -#: ../epour/gui/Widgets.py:122 +#: ../epour/gui/Widgets.py:143 msgid "OK" msgstr "" -#: ../epour/gui/Widgets.py:131 +#: ../epour/gui/Widgets.py:152 msgid "Confirm exit" msgstr "" -#: ../epour/gui/Widgets.py:132 +#: ../epour/gui/Widgets.py:153 msgid "Are you sure you wish to exit Epour?" msgstr "" -#: ../epour/gui/Widgets.py:134 +#: ../epour/gui/Widgets.py:155 msgid "Yes" msgstr "" -#: ../epour/gui/Widgets.py:138 +#: ../epour/gui/Widgets.py:159 msgid "No" msgstr "" -#: ../epour/gui/__init__.py:93 +#: ../epour/gui/__init__.py:91 msgid "Add torrent" msgstr "" -#: ../epour/gui/__init__.py:106 +#: ../epour/gui/__init__.py:103 msgid "Pause Session" msgstr "" -#: ../epour/gui/__init__.py:110 +#: ../epour/gui/__init__.py:106 msgid "Resume Session" msgstr "" -#: ../epour/gui/__init__.py:126 +#: ../epour/gui/__init__.py:121 msgid "Preferences" msgstr "" -#: ../epour/gui/__init__.py:129 +#: ../epour/gui/__init__.py:124 msgid "General" msgstr "" -#: ../epour/gui/__init__.py:133 +#: ../epour/gui/__init__.py:127 msgid "Proxy" msgstr "" -#: ../epour/gui/__init__.py:137 ../epour/gui/__init__.py:339 +#: ../epour/gui/__init__.py:130 ../epour/gui/__init__.py:340 msgid "Session" msgstr "" -#: ../epour/gui/__init__.py:141 +#: ../epour/gui/__init__.py:133 msgid "Exit" msgstr "" -#: ../epour/gui/__init__.py:225 +#: ../epour/gui/__init__.py:240 msgid "Torrent {} has finished downloading." msgstr "" -#: ../epour/gui/__init__.py:370 +#: ../epour/gui/__init__.py:371 msgid "Peer connections" msgstr "" -#: ../epour/gui/__init__.py:378 +#: ../epour/gui/__init__.py:379 msgid "Upload slots" msgstr "" -#: ../epour/gui/__init__.py:387 +#: ../epour/gui/__init__.py:388 msgid "Listening" msgstr "" -#: ../epour/gui/__init__.py:454 +#: ../epour/gui/__init__.py:432 ../epour/gui/TorrentProps.py:539 +msgid "Queued" +msgstr "" + +#: ../epour/gui/__init__.py:432 ../epour/gui/TorrentProps.py:539 +msgid "Checking" +msgstr "" + +#: ../epour/gui/__init__.py:432 ../epour/gui/TorrentProps.py:539 +msgid "Downloading metadata" +msgstr "" + +#: ../epour/gui/__init__.py:433 ../epour/gui/TorrentProps.py:539 +msgid "Downloading" +msgstr "" + +#: ../epour/gui/__init__.py:433 ../epour/gui/TorrentProps.py:540 +msgid "Finished" +msgstr "" + +#: ../epour/gui/__init__.py:433 ../epour/gui/TorrentProps.py:540 +msgid "Seeding" +msgstr "" + +#: ../epour/gui/__init__.py:433 ../epour/gui/TorrentProps.py:540 +msgid "Allocating" +msgstr "" + +#: ../epour/gui/__init__.py:434 ../epour/gui/TorrentProps.py:540 +msgid "Checking resume data" +msgstr "" + +#: ../epour/gui/__init__.py:448 msgid "Invalid torrent" msgstr "" -#: ../epour/gui/__init__.py:460 +#: ../epour/gui/__init__.py:459 #, python-brace-format msgid "{0:.0%} complete, ETA: {1} (Down: {2}/s Up: {3}/s Queue pos: {4})" msgstr "" @@ -207,95 +150,152 @@ msgstr "" msgid "Down" msgstr "" -#: ../epour/gui/__init__.py:551 +#: ../epour/gui/__init__.py:549 msgid "Top" msgstr "" -#: ../epour/gui/__init__.py:554 +#: ../epour/gui/__init__.py:551 msgid "Bottom" msgstr "" -#: ../epour/gui/__init__.py:557 +#: ../epour/gui/__init__.py:553 msgid "Remove torrent" msgstr "" -#: ../epour/gui/__init__.py:561 +#: ../epour/gui/__init__.py:556 msgid "and data files" msgstr "" -#: ../epour/gui/__init__.py:566 +#: ../epour/gui/__init__.py:560 msgid "Force reannounce" msgstr "" -#: ../epour/gui/__init__.py:579 +#: ../epour/gui/__init__.py:571 msgid "Force DHT reannounce" msgstr "" -#: ../epour/gui/__init__.py:583 +#: ../epour/gui/__init__.py:574 msgid "Scrape tracker" msgstr "" -#: ../epour/gui/__init__.py:593 +#: ../epour/gui/__init__.py:582 msgid "Force re-check" msgstr "" -#: ../epour/gui/__init__.py:609 +#: ../epour/gui/__init__.py:596 msgid "Torrent properties" msgstr "" -#: ../epour/gui/__init__.py:649 +#: ../epour/gui/__init__.py:632 msgid "Time when added" msgstr "" -#: ../epour/gui/__init__.py:650 +#: ../epour/gui/__init__.py:633 msgid "Time when completed" msgstr "" -#: ../epour/gui/__init__.py:651 +#: ../epour/gui/__init__.py:634 msgid "All time downloaded" msgstr "" -#: ../epour/gui/__init__.py:652 +#: ../epour/gui/__init__.py:635 msgid "All time uploaded" msgstr "" -#: ../epour/gui/__init__.py:653 +#: ../epour/gui/__init__.py:636 msgid "Total wanted done" msgstr "" -#: ../epour/gui/__init__.py:654 +#: ../epour/gui/__init__.py:637 msgid "Total wanted" msgstr "" -#: ../epour/gui/__init__.py:655 +#: ../epour/gui/__init__.py:638 msgid "Total downloaded this session" msgstr "" -#: ../epour/gui/__init__.py:657 +#: ../epour/gui/__init__.py:640 msgid "Total uploaded this session" msgstr "" -#: ../epour/gui/__init__.py:658 +#: ../epour/gui/__init__.py:641 msgid "Total failed" msgstr "" -#: ../epour/gui/__init__.py:659 +#: ../epour/gui/__init__.py:642 msgid "Number of seeds" msgstr "" -#: ../epour/gui/__init__.py:660 +#: ../epour/gui/__init__.py:643 msgid "Number of peers" msgstr "" -#: ../epour/gui/__init__.py:679 ../epour/gui/__init__.py:722 +#: ../epour/gui/__init__.py:672 ../epour/gui/__init__.py:715 msgid "N/A" msgstr "" -#: ../epour/gui/__init__.py:703 +#: ../epour/gui/__init__.py:695 #, python-format msgid "Pieces (scaled 1:%d)" msgstr "" +#: ../epour/gui/TorrentProps.py:90 +msgid "Enable/disable file download" +msgstr "" + +#: ../epour/gui/TorrentProps.py:124 +msgid "Invalid torrent handle." +msgstr "" + +#: ../epour/gui/TorrentProps.py:149 +msgid "Torrent info" +msgstr "" + +#: ../epour/gui/TorrentProps.py:157 +msgid "Torrent settings" +msgstr "" + +#: ../epour/gui/TorrentProps.py:165 +msgid "Torrent status" +msgstr "" + +#: ../epour/gui/TorrentProps.py:174 +msgid "Magnet URI" +msgstr "" + +#: ../epour/gui/TorrentProps.py:182 +msgid "Copy" +msgstr "" + +#: ../epour/gui/TorrentProps.py:209 +#, python-format +msgid "Epour - Files for torrent: %s" +msgstr "" + +#: ../epour/gui/TorrentProps.py:251 +msgid "Select all" +msgstr "" + +#: ../epour/gui/TorrentProps.py:255 +msgid "Select none" +msgstr "" + +#: ../epour/gui/TorrentProps.py:394 +msgid "Private" +msgstr "" + +#: ../epour/gui/TorrentProps.py:445 +msgid "Storage path" +msgstr "" + +#: ../epour/gui/TorrentProps.py:451 +msgid "Select" +msgstr "" + +#: ../epour/gui/TorrentProps.py:503 +msgid "disabled" +msgstr "" + #: ../epour/gui/Preferences.py:97 msgid "Epour General Preferences" msgstr "" --
