diff --git a/README.md b/README.md index 5d1d870..c500d91 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,43 @@ ## Description This is a [Deluge][1] plugin that allows you to create a list of default trackers -that will be added to new torrents (and old ones after restarting Deluge). The +that will be added to new public torrents (and old ones after restarting Deluge). The plugin will not duplicate existing trackers and does not care how the torrent was added so it works perfectly fine with infohashes. -Don't use this plugin if you have private torrents where the details are not +Private torrents are excluded on purpose, because their metadata is not supposed to reach public trackers. +Besides manually creating the default tracker list, you can also load it (periodically) from a URL. + +This plugin is compatible with Deluge 2.0 and Python 3.6+. + ## Installation * create the egg with `python setup.py bdist_egg` +(or try to use [the one from the "egg" directory][2] - rename it, if it doesn't match your Python3 version) + +* you need to use the same version of Python3 as the one that Deluge is running under. + * add it to Deluge from Preferences -> Plugins -> Install Plugin +* now you can go to Preferences -> Default Trackers and add individual default trackers, or the URL of a list that should be periodically downloaded + (e.g.: https://newtrackon.com/api/stable +or https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt) + +## Troubleshooting + +To get Deluge's output on Windows, run this in a terminal ("cmd" works): + +`"%ProgramFiles%\Deluge\deluge-debug.exe"` + ## TODO: * log the added trackers so we can remove them from torrents when they are deleted from the default list -* WebUI version [1]: http://deluge-torrent.org/ +[2]: https://github.com/stefantalpalaru/deluge-default-trackers/raw/master/egg + diff --git a/defaulttrackers/__init__.py b/defaulttrackers/__init__.py index d8fde3c..913d90f 100644 --- a/defaulttrackers/__init__.py +++ b/defaulttrackers/__init__.py @@ -1,7 +1,7 @@ # # __init__.py # -# Copyright (C) 2013 Stefan Talpalaru +# Copyright (C) 2013-2019 Stefan Talpalaru # # Basic plugin template created by: # Copyright (C) 2008 Martijn Voncken @@ -38,22 +38,30 @@ # statement from all source files in the program, then also delete it here. # +from __future__ import absolute_import from deluge.plugins.init import PluginInitBase class CorePlugin(PluginInitBase): def __init__(self, plugin_name): - from core import Core as _plugin_cls + from .core import Core as _plugin_cls self._plugin_cls = _plugin_cls super(CorePlugin, self).__init__(plugin_name) class GtkUIPlugin(PluginInitBase): def __init__(self, plugin_name): - from gtkui import GtkUI as _plugin_cls + from .gtkui import GtkUI as _plugin_cls self._plugin_cls = _plugin_cls super(GtkUIPlugin, self).__init__(plugin_name) +class Gtk3UIPlugin(PluginInitBase): + def __init__(self, plugin_name): + from .gtk3ui import Gtk3UI as _plugin_cls + self._plugin_cls = _plugin_cls + super(Gtk3UIPlugin, self).__init__(plugin_name) + class WebUIPlugin(PluginInitBase): def __init__(self, plugin_name): - from webui import WebUI as _plugin_cls + from .webui import WebUI as _plugin_cls self._plugin_cls = _plugin_cls super(WebUIPlugin, self).__init__(plugin_name) + diff --git a/defaulttrackers/common.py b/defaulttrackers/common.py index 1f4c0ab..d0775fc 100644 --- a/defaulttrackers/common.py +++ b/defaulttrackers/common.py @@ -1,7 +1,7 @@ -# +# -*- coding: utf-8 -*- # common.py # -# Copyright (C) 2013 Stefan Talpalaru +# Copyright (C) 2013-2022 Ștefan Talpalaru # # Basic plugin template created by: # Copyright (C) 2008 Martijn Voncken @@ -39,7 +39,8 @@ # +import os +from pkg_resources import resource_filename + def get_resource(filename): - import pkg_resources, os - return pkg_resources.resource_filename("defaulttrackers", - os.path.join("data", filename)) + return resource_filename(__package__, os.path.join("data", filename)) diff --git a/defaulttrackers/core.py b/defaulttrackers/core.py index 4cb5ad3..7de57ec 100644 --- a/defaulttrackers/core.py +++ b/defaulttrackers/core.py @@ -1,7 +1,7 @@ -# +# -*- coding: utf-8 -*- # core.py # -# Copyright (C) 2013 Stefan Talpalaru +# Copyright (C) 2013-2024 Ștefan Talpalaru # # Basic plugin template created by: # Copyright (C) 2008 Martijn Voncken @@ -38,17 +38,29 @@ # statement from all source files in the program, then also delete it here. # +from __future__ import absolute_import, unicode_literals +import datetime import logging +import re +import ssl +import time +import traceback +import urllib + +from deluge.common import is_url, decode_bytes +from deluge.core.rpcserver import export from deluge.plugins.pluginbase import CorePluginBase import deluge.component as component import deluge.configmanager -from deluge.core.rpcserver import export -from pprint import pprint + DEFAULT_PREFS = { "trackers": [ - #{"url": "test"}, + #{"url": "udp://foo.bar:6969/announce"}, ], + "dynamic_trackerlist_url": "", + "last_dynamic_trackers_update": 0, # UTC timestamp + "dynamic_trackers_update_interval": 1, # in days } log = logging.getLogger(__name__) @@ -68,10 +80,54 @@ def disable(self): def update(self): pass - def on_torrent_added(self, torrent_id): + @export + def update_trackerlist_from_url(self): + if self.config["dynamic_trackerlist_url"]: + now = datetime.datetime.utcnow() + last_update = datetime.datetime.utcfromtimestamp(self.config["last_dynamic_trackers_update"]) + if now - last_update > datetime.timedelta(days=self.config["dynamic_trackers_update_interval"]): + new_trackers = [] + seen_trackers = set() + headers = { + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.3', + 'Accept-Encoding': 'none', + 'Accept-Language': 'en-US,en;q=0.8', + } + try: + for url_line in self.config["dynamic_trackerlist_url"].splitlines(): + url_line = url_line.strip() + if not url_line: + continue + req = urllib.request.Request(url_line, headers=headers) + try: + page = urllib.request.urlopen(req, context=ssl._create_unverified_context()).read() + except: + # maybe an older Python version without a "context" argument + page = urllib.request.urlopen(req).read() + # we need to avoid duplicates here + for new_tracker in [decode_bytes(u) for u in re.findall(rb'\w+://[\w\-.:/]+', page) if is_url(decode_bytes(u))]: + if new_tracker not in seen_trackers: + seen_trackers.add(new_tracker) + new_trackers.append(new_tracker) + + if new_trackers: + # replace all existing trackers + self.config["trackers"] = [{"url": t} for t in new_trackers] + + self.config["last_dynamic_trackers_update"] = time.mktime(now.timetuple()) + except: + traceback.print_exc() + return self.config.config + + def on_torrent_added(self, torrent_id, from_state=False): torrent = component.get("TorrentManager")[torrent_id] - trackers = torrent.get_status(["trackers"])["trackers"] + if (torrent.torrent_info and torrent.torrent_info.priv()) or torrent.get_status(["private"])["private"]: + return + trackers = list(torrent.get_status(["trackers"])["trackers"]) existing_urls = [tracker["url"] for tracker in trackers] + self.update_trackerlist_from_url() got_new_trackers = False for new_tracker in self.config["trackers"]: if new_tracker["url"] not in existing_urls: @@ -96,3 +152,11 @@ def get_config(self): """Returns the config dictionary""" return self.config.config + @export + def apply_to_existing(self): + """Apply the tracker list to existings torrents""" + + for torrent_id in component.get("TorrentManager").torrents: + self.on_torrent_added(torrent_id) + return True + diff --git a/defaulttrackers/data/config.glade b/defaulttrackers/data/config.glade index 43fcd69..0629e10 100644 --- a/defaulttrackers/data/config.glade +++ b/defaulttrackers/data/config.glade @@ -8,6 +8,164 @@ True False + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + + + True + False + tracker list URL: + + + False + False + 0 + + + + + True + True + + + True + True + 6 + 1 + + + + + True + True + 0 + + + + + True + False + e.g.: https://newtrackon.com/api/stable +or https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt + True + + + True + True + 1 + + + + + True + False + + + True + False + reload every + + + False + False + 0 + + + + + 34 + True + True + + True + False + False + True + True + + + False + False + 6 + 1 + + + + + True + False + days + + + False + False + 2 + + + + + gtk-refresh + True + True + True + True + reload the tracker list now + True + + + + False + False + 10 + 3 + + + + + False + False + 2 + + + + + + + + + True + False + <b>Dynamic tracker list</b> (optional) + True + + + label_item + + + + + True + True + 0 + + True @@ -54,7 +212,7 @@ True True True - + False @@ -69,7 +227,7 @@ True True True - + False @@ -84,7 +242,7 @@ True True True - + False @@ -99,7 +257,7 @@ True True True - + False @@ -114,7 +272,7 @@ True True True - + False @@ -149,7 +307,7 @@ True True - 0 + 1 diff --git a/defaulttrackers/data/config.ui b/defaulttrackers/data/config.ui new file mode 100644 index 0000000..f2f1eda --- /dev/null +++ b/defaulttrackers/data/config.ui @@ -0,0 +1,310 @@ + + + + + + False + + + True + False + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + False + + + True + False + tracker list URL: + + + False + False + 0 + + + + + True + True + + + True + True + 6 + 1 + + + + + True + True + 0 + + + + + True + False + e.g.: https://newtrackon.com/api/stable +or https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt + True + + + True + True + 1 + + + + + True + False + + + True + False + reload every + + + False + False + 0 + + + + + 34 + True + True + + True + False + False + True + True + + + False + False + 6 + 1 + + + + + True + False + days + + + False + False + 2 + + + + + gtk-refresh + True + True + True + True + reload the tracker list now + True + + + + False + False + 10 + 3 + + + + + False + False + 2 + + + + + + + + + True + False + <b>Dynamic tracker list</b> (optional) + True + + + + + True + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + + + True + False + 2 + + + True + True + automatic + automatic + etched-in + + + + + + True + True + 0 + + + + + True + False + 5 + start + + + gtk-go-up + True + True + True + True + + + + False + False + 0 + + + + + gtk-add + True + True + True + True + + + + False + False + 1 + + + + + gtk-edit + True + True + True + True + + + + False + False + 2 + + + + + gtk-remove + True + True + True + True + + + + False + False + 3 + + + + + gtk-go-down + True + True + True + True + + + + False + False + 4 + + + + + False + True + 3 + 1 + + + + + + + + + True + False + <b>Default trackers</b> + True + + + + + True + True + 1 + + + + + + diff --git a/defaulttrackers/data/defaulttrackers.js b/defaulttrackers/data/defaulttrackers.js index 449f41a..a7c5c00 100644 --- a/defaulttrackers/data/defaulttrackers.js +++ b/defaulttrackers/data/defaulttrackers.js @@ -3,7 +3,7 @@ Script: defaulttrackers.js The client-side javascript code for the DefaultTrackers plugin. Copyright: - (C) Stefan Talpalaru 2013 + (C) Ștefan Talpalaru 2013-2022 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, or (at your option) @@ -31,20 +31,128 @@ Copyright: statement from all source files in the program, then also delete it here. */ -DefaultTrackersPlugin = Ext.extend(Deluge.Plugin, { +DefaultTrackersPanel = Ext.extend(Ext.form.FormPanel, { constructor: function(config) { config = Ext.apply({ - name: "DefaultTrackers" + border: false, + title: ("Default Trackers"), + autoHeight: true, }, config); - DefaultTrackersPlugin.superclass.constructor.call(this, config); + DefaultTrackersPanel.superclass.constructor.call(this, config); }, + initComponent: function() { + DefaultTrackersPanel.superclass.initComponent.call(this); + this.opts = new Deluge.OptionsManager(); - onDisable: function() { + var fieldset = this.add({ + xtype: 'fieldset', + title: ('Dynamic tracker list (optional)'), + autoHeight: true, + autoWidth: true, + }); + this.opts.bind('dynamic_trackerlist_url', fieldset.add({ + xtype: 'textarea', + fieldLabel: ('tracker list URL'), + anchor: '100%', + name: 'dynamic_trackerlist_url', + autoWidth: true, + })); + fieldset.add({ + xtype: 'displayfield', + fieldLabel: 'E.g.', + value: 'https://newtrackon.com/api/stable or
https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt', + }); + this.opts.bind('dynamic_trackers_update_interval', fieldset.add({ + xtype: 'textfield', + fieldLabel: ('reload every X days'), + anchor: '100%', + name: 'dynamic_trackers_update_interval', + autoWidth: true, + })); + + fieldset = this.add({ + xtype: 'fieldset', + title: ('Default trackers'), + autoHeight: true, + autoWidth: true, + }); + this.opts.bind('trackers_ta', fieldset.add({ + xtype: 'textarea', + fieldLabel: (''), + anchor: '100%', + name: 'trackers_ta', + height: '200px', + autoWidth: true, + })); + + var applyToExisting = this.add({ + fieldLabel: _(''), + name: 'trackers_apply', + xtype: 'container', + layout: 'hbox', + items: [{ + xtype: 'button', + text: 'Apply to existing torrents', + }] + }); + + applyToExisting.getComponent(0).setHandler(this.onApplyToExisting, this); + deluge.preferences.on('show', this.onPreferencesShow, this); }, - onEnable: function() { + onPreferencesShow: function() { + deluge.client.defaulttrackers.get_config({ + success: function(config) { + config.trackers_ta = config.trackers + .map(function(tracker){return tracker.url}) + .join('\n'); + + this.opts.set(config); + }, + scope: this, + }); + }, + onApply: function(e) { + var changed = this.opts.getDirty(); + if (!Ext.isObjectEmpty(changed)) { + if (Ext.isDefined(changed['trackers_ta'])) { + changed.trackers = changed.trackers_ta + .split('\n') + .filter(function(line){return line != ''}) + .map(function(url){return {'url': url}}); + } + + deluge.client.defaulttrackers.set_config(changed, { + success: this.onSetConfig, + scope: this, + }); + } + }, + onSetConfig: function() { + this.opts.commit(); + }, + onApplyToExisting: function() { + deluge.client.defaulttrackers.apply_to_existing({ + success: function() { + Ext.Msg.alert('Success', 'Default trackers applied to existing torrents'); + }, + scope: this, + }); + } +}); + +DefaultTrackersPlugin = Ext.extend(Deluge.Plugin, { + name: "Default Trackers", + onDisable: function() { + deluge.preferences.removePage(this.prefsPage); + }, + + onEnable: function() { + this.prefsPage = new DefaultTrackersPanel(); + this.prefsPage = deluge.preferences.addPage(this.prefsPage); } }); -new DefaultTrackersPlugin(); + +Deluge.registerPlugin("Default Trackers", DefaultTrackersPlugin); diff --git a/defaulttrackers/data/options.glade b/defaulttrackers/data/options.glade index 56b82e2..16d3b8d 100644 --- a/defaulttrackers/data/options.glade +++ b/defaulttrackers/data/options.glade @@ -9,7 +9,7 @@ True 250 dialog - + True @@ -27,14 +27,10 @@ True False - + + 134 True True - - False - False - True - True @@ -43,7 +39,7 @@ True False - <b>Tracker</b> + <b>Trackers</b> (one per line) True @@ -71,16 +67,17 @@ <b>Examples:</b> +udp://tracker.opentrackr.org:1337/announce udp://tracker.openbittorrent.com:80 - udp://tracker.publicbt.com:80 - udp://tracker.istole.it:80 - udp://tracker.ccc.de:80 - http://tracker.coppersurfer.tk:6969 - +udp://open.demonii.com:1337 +http://tracker.baravik.org:6970/announce +http://tracker2.wasabii.com.tw:6969/announce +udp://9.rarbg.me:2710/announce +udp://tracker.zer0day.to:1337/announce True True @@ -110,7 +107,7 @@ http://tracker.coppersurfer.tk:6969 True False True - + False @@ -125,7 +122,7 @@ http://tracker.coppersurfer.tk:6969 True False True - + False @@ -140,7 +137,7 @@ http://tracker.coppersurfer.tk:6969 True False True - + False diff --git a/defaulttrackers/data/options.ui b/defaulttrackers/data/options.ui new file mode 100644 index 0000000..1b8a089 --- /dev/null +++ b/defaulttrackers/data/options.ui @@ -0,0 +1,156 @@ + + + + + + False + 10 + Tracker Properties + True + 250 + dialog + + + + True + False + 2 + + + True + False + 0 + 0 + none + + + True + False + + + 134 + True + True + + + + + + + True + False + <b>Trackers</b> (one per line) + True + + + + + True + False + 0 + + + + + True + False + + + True + False + 0 + 5 + 5 + +<b>Examples:</b> + +udp://tracker.opentrackr.org:1337/announce +udp://tracker.openbittorrent.com:80 +udp://tracker.publicbt.com:80 +udp://tracker.istole.it:80 +udp://tracker.ccc.de:80 +http://tracker.coppersurfer.tk:6969 +udp://open.demonii.com:1337 +http://tracker.baravik.org:6970/announce +http://tracker2.wasabii.com.tw:6969/announce +udp://9.rarbg.me:2710/announce +udp://tracker.zer0day.to:1337/announce + + True + True + + + True + True + 0 + + + + + True + True + 1 + + + + + True + False + center + + + gtk-cancel + True + True + False + True + + + + False + False + 0 + + + + + gtk-add + True + True + False + True + + + + False + False + 1 + + + + + gtk-apply + True + True + False + True + + + + False + False + 2 + + + + + False + True + end + 2 + + + + + + diff --git a/defaulttrackers/gtk3ui.py b/defaulttrackers/gtk3ui.py new file mode 100644 index 0000000..b40bb2c --- /dev/null +++ b/defaulttrackers/gtk3ui.py @@ -0,0 +1,287 @@ +# -*- coding: utf-8 -*- +# gtkui.py +# +# Copyright (C) 2013-2022 Ștefan Talpalaru +# +# Basic plugin template created by: +# Copyright (C) 2008 Martijn Voncken +# Copyright (C) 2007-2009 Andrew Resch +# Copyright (C) 2009 Damien Churchill +# Copyright (C) 2010 Pedro Algarvio +# +# Deluge is free software. +# +# You may 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. +# +# deluge 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 deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# + +from __future__ import absolute_import, unicode_literals +from gi.repository import Gtk +import logging +import os + +from deluge.ui.client import client +from deluge.plugins.pluginbase import Gtk3PluginBase +import deluge.component as component +from deluge.ui.gtk3 import dialogs +#from pprint import pprint + +from .common import get_resource + +log = logging.getLogger(__name__) + +def iter_prev(iter, model): + path = model.get_path(iter) + position = path[-1] + if position == 0: + return None + prev_path = list(path)[:-1] + prev_path.append(position - 1) + prev = model.get_iter(tuple(prev_path)) + return prev + +class OptionsDialog(): + def __init__(self, gtkui): + self.gtkui = gtkui + + def show(self, options=None, item_id=None, item_index=None): + self.builder = Gtk.Builder.new_from_file(get_resource("options.ui")) + self.builder.connect_signals({ + "on_opts_add_button_clicked": self.on_add, + "on_opts_apply_button_clicked": self.on_apply, + "on_opts_cancel_button_clicked": self.on_cancel, + "on_options_dialog_close": self.on_cancel, + }) + self.dialog = self.builder.get_object("options_dialog") + self.dialog.set_transient_for(component.get("Preferences").pref_dialog) + + if item_id: + #We have an existing item_id, we are editing + self.builder.get_object("opts_add_button").hide() + self.builder.get_object("opts_apply_button").show() + self.item_id = item_id + else: + #We don't have an id, adding + self.builder.get_object("opts_add_button").show() + self.builder.get_object("opts_apply_button").hide() + self.item_id = None + self.item_index = item_index + + self.load_options(options) + self.dialog.run() + self.dialog.destroy() + + def load_options(self, options): + if options: + self.builder.get_object("tracker_entry").get_buffer().set_text(options.get("url", "")) + + def in_store(self, item): + for row in self.gtkui.store: + if row[0] == item: + return True + return False + + def on_add(self, widget): + try: + options = self.generate_opts() + for url in options["urls"]: + if not self.in_store(url): + self.gtkui.store.append([url]) + self.gtkui.trackers.append({"url": url}) + self.dialog.response(Gtk.ResponseType.DELETE_EVENT) + except Exception as err: + dialogs.ErrorDialog("Error", str(err), self.dialog).run() + + def generate_opts(self): + # generate options dict based on gtk objects + buffer = self.builder.get_object("tracker_entry").get_buffer() + options = { + "urls": buffer.get_text(*buffer.get_bounds(), False).split(), + } + if len(options["urls"]) == 0: + raise Exception("no URLs") + return options + + def on_apply(self, widget): + try: + options = self.generate_opts() + self.gtkui.store[self.item_id][0] = options["urls"][0] + self.gtkui.trackers[self.item_index]["url"] = options["urls"][0] + self.dialog.response(Gtk.ResponseType.DELETE_EVENT) + except Exception as err: + dialogs.ErrorDialog("Error", str(err), self.dialog).run() + + def on_cancel(self, widget): + self.dialog.response(Gtk.ResponseType.DELETE_EVENT) + +class Gtk3UI(Gtk3PluginBase): + def enable(self): + self.builder = Gtk.Builder.new_from_file(get_resource("config.ui")) + self.builder.connect_signals({ + "on_add_button_clicked": self.on_add_button_clicked, + "on_edit_button_clicked": self.on_edit_button_clicked, + "on_remove_button_clicked": self.on_remove_button_clicked, + "on_up_button_clicked": self.on_up_button_clicked, + "on_down_button_clicked": self.on_down_button_clicked, + "on_reload_now_clicked": self.on_reload_now_clicked, + }) + + component.get("Preferences").add_page("Default Trackers", self.builder.get_object("prefs_box")) + component.get("PluginManager").register_hook("on_apply_prefs", self.on_apply_prefs) + component.get("PluginManager").register_hook("on_show_prefs", self.on_show_prefs) + + self.trackers = [] + scrolled_window = self.builder.get_object("scrolledwindow1") + self.store = self.create_model() + self.tree_view = Gtk.TreeView(self.store) + tree_selection = self.tree_view.get_selection() + tree_selection.set_mode(Gtk.SelectionMode.MULTIPLE) + self.tree_view.connect("cursor-changed", self.on_listitem_activated) + self.tree_view.connect("row-activated", self.on_edit_button_clicked) + self.tree_view.set_rules_hint(True) + rendererText = Gtk.CellRendererText() + column = Gtk.TreeViewColumn("URL", rendererText, text=0) + self.tree_view.append_column(column) + scrolled_window.add(self.tree_view) + scrolled_window.show_all() + + self.opts_dialog = OptionsDialog(self) + + def disable(self): + component.get("Preferences").remove_page("Default Trackers") + component.get("PluginManager").deregister_hook("on_apply_prefs", self.on_apply_prefs) + component.get("PluginManager").deregister_hook("on_show_prefs", self.on_show_prefs) + + def on_apply_prefs(self): + log.debug("applying prefs for DefaultTrackers") + try: + update_interval = int(self.builder.get_object("tracker_list_update_interval").get_text() or 1) + except: + update_interval = 1 + tracker_list_buffer = self.builder.get_object("tracker_list_url").get_buffer() + start_iter, end_iter = tracker_list_buffer.get_bounds() + tracker_list_text = tracker_list_buffer.get_text(start_iter, end_iter, False) + self.config.update({ + "trackers": [{"url": str(row[0])} for row in self.store], + "dynamic_trackerlist_url": tracker_list_text, + "dynamic_trackers_update_interval": update_interval, + }) + client.defaulttrackers.set_config(self.config) + + def on_show_prefs(self): + client.defaulttrackers.get_config().addCallback(self.cb_get_config) + + def cb_get_config(self, config): + "callback for on show_prefs" + self.config = config + # dynamic tracker list + if config["dynamic_trackerlist_url"]: + self.builder.get_object("tracker_list_url").get_buffer().set_text(config["dynamic_trackerlist_url"]) + self.builder.get_object("tracker_list_update_interval").set_text(str(config["dynamic_trackers_update_interval"])) + # trackers + self.trackers = list(config["trackers"]) + self.store.clear() + for tracker in self.trackers: + self.store.append([tracker["url"]]) + # Workaround for cached builder signal appearing when re-enabling plugin in same session + if self.builder.get_object("edit_button"): + # Disable the remove and edit buttons, because nothing in the store is selected + self.builder.get_object("remove_button").set_sensitive(False) + self.builder.get_object("edit_button").set_sensitive(False) + self.builder.get_object("up_button").set_sensitive(False) + self.builder.get_object("down_button").set_sensitive(False) + + def create_model(self): + store = Gtk.ListStore(str) + for tracker in self.trackers: + store.append([tracker["url"]]) + return store + + def on_listitem_activated(self, treeview): + tree, tree_paths = self.tree_view.get_selection().get_selected_rows() + if tree_paths: + self.builder.get_object("edit_button").set_sensitive(True) + self.builder.get_object("remove_button").set_sensitive(True) + self.builder.get_object("up_button").set_sensitive(True) + self.builder.get_object("down_button").set_sensitive(True) + else: + self.builder.get_object("edit_button").set_sensitive(False) + self.builder.get_object("remove_button").set_sensitive(False) + self.builder.get_object("up_button").set_sensitive(False) + self.builder.get_object("down_button").set_sensitive(False) + + def on_add_button_clicked(self, widget): + self.opts_dialog.show() + + def on_remove_button_clicked(self, widget): + tree, tree_paths = self.tree_view.get_selection().get_selected_rows() + to_remove = [] + for tree_path in tree_paths: + tree_id = tree.get_iter(tree_path) + index = tree_path[0] + to_remove.append((index, tree_id)) + for index, tree_id in sorted(to_remove, reverse=True): + # we need to delete the indices in reverse order to avoid offsets + del self.trackers[index] + self.store.remove(tree_id) + + def on_edit_button_clicked(self, widget): + tree, tree_paths = self.tree_view.get_selection().get_selected_rows() + if tree_paths: + tree_path = tree_paths[0] + tree_id = tree.get_iter(tree_path) + index = tree_path[0] + url = str(self.store.get_value(tree_id, 0)) + if url: + self.opts_dialog.show({ + "url": url, + }, tree_id, index) + + def on_up_button_clicked(self, widget): + tree, tree_paths = self.tree_view.get_selection().get_selected_rows() + if tree_paths: + tree_path = tree_paths[0] + tree_id = tree.get_iter(tree_path) + prev = iter_prev(tree_id, self.store) + if prev is not None: + self.store.swap(prev, tree_id) + + def on_down_button_clicked(self, widget): + tree, tree_paths = self.tree_view.get_selection().get_selected_rows() + if tree_paths: + tree_path = tree_paths[0] + tree_id = tree.get_iter(tree_path) + nexti = self.store.iter_next(tree_id) + if nexti is not None: + self.store.swap(tree_id, nexti) + + def on_reload_now_clicked(self, widget): + # reset the last update timestamp + self.config["last_dynamic_trackers_update"] = 0 + self.on_apply_prefs() + # we need to reload the tracker list after the update + client.defaulttrackers.update_trackerlist_from_url().addCallback(self.cb_get_config) + diff --git a/defaulttrackers/gtkui.py b/defaulttrackers/gtkui.py index 1071cc8..9e8bc49 100644 --- a/defaulttrackers/gtkui.py +++ b/defaulttrackers/gtkui.py @@ -1,7 +1,7 @@ -# +# -*- coding: utf-8 -*- # gtkui.py # -# Copyright (C) 2013 Stefan Talpalaru +# Copyright (C) 2013-2019 Ștefan Talpalaru # # Basic plugin template created by: # Copyright (C) 2008 Martijn Voncken @@ -38,17 +38,17 @@ # statement from all source files in the program, then also delete it here. # +from __future__ import absolute_import, unicode_literals import gtk import logging from deluge.ui.client import client from deluge.plugins.pluginbase import GtkPluginBase import deluge.component as component -import deluge.common from deluge.ui.gtkui import dialogs -from pprint import pprint +#from pprint import pprint -from common import get_resource +from .common import get_resource log = logging.getLogger(__name__) @@ -95,31 +95,40 @@ def show(self, options=None, item_id=None, item_index=None): def load_options(self, options): if options: - self.glade.get_widget("tracker_entry").set_text(options.get("url", "")) + self.glade.get_widget("tracker_entry").get_buffer().set_text(options.get("url", "")) + + def in_store(self, item): + for row in self.gtkui.store: + if row[0] == item: + return True + return False def on_add(self, widget): try: options = self.generate_opts() - self.gtkui.store.append([options["url"]]) - self.gtkui.trackers.append({"url": options["url"]}) - except Exception, err: + for url in options["urls"]: + if not self.in_store(url): + self.gtkui.store.append([url]) + self.gtkui.trackers.append({"url": url}) + except Exception as err: dialogs.ErrorDialog("Error", str(err), self.dialog).run() def generate_opts(self): # generate options dict based on gtk objects + buffer = self.glade.get_widget("tracker_entry").get_buffer() options = { - "url": self.glade.get_widget("tracker_entry").get_text(), + "urls": buffer.get_text(*buffer.get_bounds()).split(), } - if len(options["url"]) == 0: - raise Exception("empty URL") + if len(options["urls"]) == 0: + raise Exception("no URLs") return options def on_apply(self, widget): try: options = self.generate_opts() - self.gtkui.store[self.item_id][0] = options["url"] - self.gtkui.trackers[self.item_index]["url"] = options["url"] - except Exception, err: + self.gtkui.store[self.item_id][0] = options["urls"][0] + self.gtkui.trackers[self.item_index]["url"] = options["urls"][0] + except Exception as err: dialogs.ErrorDialog("Error", str(err), self.dialog).run() def on_cancel(self, widget): @@ -134,6 +143,7 @@ def enable(self): "on_remove_button_clicked": self.on_remove_button_clicked, "on_up_button_clicked": self.on_up_button_clicked, "on_down_button_clicked": self.on_down_button_clicked, + "on_reload_now_clicked": self.on_reload_now_clicked, }) component.get("Preferences").add_page("Default Trackers", self.glade.get_widget("prefs_box")) @@ -144,6 +154,8 @@ def enable(self): scrolled_window = self.glade.get_widget("scrolledwindow1") self.store = self.create_model() self.tree_view = gtk.TreeView(self.store) + tree_selection = self.tree_view.get_selection() + tree_selection.set_mode(gtk.SELECTION_MULTIPLE) self.tree_view.connect("cursor-changed", self.on_listitem_activated) self.tree_view.connect("row-activated", self.on_edit_button_clicked) self.tree_view.set_rules_hint(True) @@ -156,23 +168,38 @@ def enable(self): self.opts_dialog = OptionsDialog(self) def disable(self): - component.get("Preferences").remove_page("DefaultTrackers") + component.get("Preferences").remove_page("Default Trackers") component.get("PluginManager").deregister_hook("on_apply_prefs", self.on_apply_prefs) component.get("PluginManager").deregister_hook("on_show_prefs", self.on_show_prefs) def on_apply_prefs(self): log.debug("applying prefs for DefaultTrackers") - config = { - "trackers": [{"url": str(row[0])} for row in self.store] - } - client.defaulttrackers.set_config(config) + try: + update_interval = int(self.glade.get_widget("tracker_list_update_interval").get_text() or 1) + except: + update_interval = 1 + tracker_list_buffer = self.glade.get_widget("tracker_list_url").get_buffer() + start_iter, end_iter = tracker_list_buffer.get_bounds() + tracker_list_text = tracker_list_buffer.get_text(start_iter, end_iter, False) + self.config.update({ + "trackers": [{"url": str(row[0])} for row in self.store], + "dynamic_trackerlist_url": tracker_list_text, + "dynamic_trackers_update_interval": update_interval, + }) + client.defaulttrackers.set_config(self.config) def on_show_prefs(self): client.defaulttrackers.get_config().addCallback(self.cb_get_config) def cb_get_config(self, config): "callback for on show_prefs" - self.trackers = config["trackers"] + self.config = config + # dynamic tracker list + if config["dynamic_trackerlist_url"]: + self.glade.get_widget("tracker_list_url").get_buffer().set_text(config["dynamic_trackerlist_url"]) + self.glade.get_widget("tracker_list_update_interval").set_text(str(config["dynamic_trackers_update_interval"])) + # trackers + self.trackers = list(config["trackers"]) self.store.clear() for tracker in self.trackers: self.store.append([tracker["url"]]) @@ -191,8 +218,8 @@ def create_model(self): return store def on_listitem_activated(self, treeview): - tree, tree_id = self.tree_view.get_selection().get_selected() - if tree_id: + tree, tree_paths = self.tree_view.get_selection().get_selected_rows() + if tree_paths: self.glade.get_widget("edit_button").set_sensitive(True) self.glade.get_widget("remove_button").set_sensitive(True) self.glade.get_widget("up_button").set_sensitive(True) @@ -207,31 +234,51 @@ def on_add_button_clicked(self, widget): self.opts_dialog.show() def on_remove_button_clicked(self, widget): - tree, tree_id = self.tree_view.get_selection().get_selected() - index = self.tree_view.get_selection().get_selected_rows()[1][0][0] - self.store.remove(tree_id) - del self.trackers[index] + tree, tree_paths = self.tree_view.get_selection().get_selected_rows() + to_remove = [] + for tree_path in tree_paths: + tree_id = tree.get_iter(tree_path) + index = tree_path[0] + to_remove.append((index, tree_id)) + for index, tree_id in sorted(to_remove, reverse=True): + # we need to delete the indices in reverse order to avoid offsets + del self.trackers[index] + self.store.remove(tree_id) def on_edit_button_clicked(self, widget): - tree, tree_id = self.tree_view.get_selection().get_selected() - index = self.tree_view.get_selection().get_selected_rows()[1][0][0] - url = str(self.store.get_value(tree_id, 0)) - if url: - self.opts_dialog.show({ - "url": url, - }, tree_id, index) + tree, tree_paths = self.tree_view.get_selection().get_selected_rows() + if tree_paths: + tree_path = tree_paths[0] + tree_id = tree.get_iter(tree_path) + index = tree_path[0] + url = str(self.store.get_value(tree_id, 0)) + if url: + self.opts_dialog.show({ + "url": url, + }, tree_id, index) def on_up_button_clicked(self, widget): - tree, tree_id = self.tree_view.get_selection().get_selected() - if tree_id is not None: + tree, tree_paths = self.tree_view.get_selection().get_selected_rows() + if tree_paths: + tree_path = tree_paths[0] + tree_id = tree.get_iter(tree_path) prev = iter_prev(tree_id, self.store) if prev is not None: self.store.swap(prev, tree_id) def on_down_button_clicked(self, widget): - tree, tree_id = self.tree_view.get_selection().get_selected() - if tree_id is not None: + tree, tree_paths = self.tree_view.get_selection().get_selected_rows() + if tree_paths: + tree_path = tree_paths[0] + tree_id = tree.get_iter(tree_path) nexti = self.store.iter_next(tree_id) if nexti is not None: self.store.swap(tree_id, nexti) + def on_reload_now_clicked(self, widget): + # reset the last update timestamp + self.config["last_dynamic_trackers_update"] = 0 + self.on_apply_prefs() + # we need to reload the tracker list after the update + client.defaulttrackers.update_trackerlist_from_url().addCallback(self.cb_get_config) + diff --git a/defaulttrackers/webui.py b/defaulttrackers/webui.py index 224ca37..f0f4a71 100644 --- a/defaulttrackers/webui.py +++ b/defaulttrackers/webui.py @@ -1,7 +1,7 @@ -# +# -*- coding: utf-8 -*- # webui.py # -# Copyright (C) 2013 Stefan Talpalaru +# Copyright (C) 2013-2022 Ștefan Talpalaru # # Basic plugin template created by: # Copyright (C) 2008 Martijn Voncken @@ -38,21 +38,14 @@ # statement from all source files in the program, then also delete it here. # -import logging -from deluge.ui.client import client -from deluge import component +from __future__ import absolute_import from deluge.plugins.pluginbase import WebPluginBase -from common import get_resource - -log = logging.getLogger(__name__) +from .common import get_resource class WebUI(WebPluginBase): scripts = [get_resource("defaulttrackers.js")] - def enable(self): - pass - - def disable(self): - pass + def __init__(self, plugin_name): + super(WebUI, self).__init__(plugin_name) diff --git a/egg/DefaultTrackers-0.6-py3.12.egg b/egg/DefaultTrackers-0.6-py3.12.egg new file mode 100644 index 0000000..43a56c8 Binary files /dev/null and b/egg/DefaultTrackers-0.6-py3.12.egg differ diff --git a/setup.py b/setup.py index 458b1ba..1c41832 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,8 @@ +# -*- coding: utf-8 -*- # # setup.py # -# Copyright (C) 2013 Stefan Talpalaru +# Copyright (C) 2013-2024 Ștefan Talpalaru # # Basic plugin template created by: # Copyright (C) 2008 Martijn Voncken @@ -38,22 +39,23 @@ # statement from all source files in the program, then also delete it here. # -from setuptools import setup, find_packages +from __future__ import absolute_import +from setuptools import setup __plugin_name__ = "DefaultTrackers" -__author__ = "Stefan Talpalaru" +__author__ = u"Ștefan Talpalaru" __author_email__ = "stefantalpalaru@yahoo.com" -__version__ = "0.1" +__version__ = "0.7" __url__ = "https://github.com/stefantalpalaru/deluge-default-trackers" __license__ = "GPLv3" -__description__ = "Add a list of default trackers to all the torrents" +__description__ = "Add default trackers to all public torrents" __long_description__ = """ -Create a list of default trackers that will be added to new torrents (and old -ones after restarting Deluge). The plugin will not duplicate existing trackers -and does not care how the torrent was added so it works perfectly fine with -infohashes. +Create a list of default trackers that will be added to new public torrents +(and old ones after restarting Deluge). The plugin will not duplicate existing +trackers and does not care how the torrent was added so it works perfectly fine +with infohashes. -Don't use this plugin if you have private torrents where the details are not +Private torrents are excluded on purpose, because their metadata is not supposed to reach public trackers. """ __pkg_data__ = {__plugin_name__.lower(): ["data/*"]} @@ -69,13 +71,16 @@ long_description=__long_description__ if __long_description__ else __description__, packages=[__plugin_name__.lower()], - package_data = __pkg_data__, + package_data=__pkg_data__, + install_requires=[], entry_points=""" [deluge.plugin.core] %(plugin_name)s = %(plugin_module)s:CorePlugin [deluge.plugin.gtkui] %(plugin_name)s = %(plugin_module)s:GtkUIPlugin + [deluge.plugin.gtk3ui] + %(plugin_name)s = %(plugin_module)s:Gtk3UIPlugin [deluge.plugin.web] %(plugin_name)s = %(plugin_module)s:WebUIPlugin """ % dict(plugin_name=__plugin_name__, plugin_module=__plugin_name__.lower())