diff --git a/README.md b/README.md index 9423add..c500d91 100644 --- a/README.md +++ b/README.md @@ -8,21 +8,36 @@ was added so it works perfectly fine with infohashes. 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][2] directory - be careful to install the py2.7 version of Deluge, http://download.deluge-torrent.org/windows/py2.7/ if you're using Windows) +(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]: egg/ +[2]: https://github.com/stefantalpalaru/deluge-default-trackers/raw/master/egg diff --git a/defaulttrackers/__init__.py b/defaulttrackers/__init__.py index 81d6af9..913d90f 100644 --- a/defaulttrackers/__init__.py +++ b/defaulttrackers/__init__.py @@ -1,7 +1,7 @@ # # __init__.py # -# Copyright (C) 2013-2016 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 e7e72fe..d0775fc 100644 --- a/defaulttrackers/common.py +++ b/defaulttrackers/common.py @@ -1,7 +1,7 @@ -# +# -*- coding: utf-8 -*- # common.py # -# Copyright (C) 2013-2016 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 f99b9b9..7de57ec 100644 --- a/defaulttrackers/core.py +++ b/defaulttrackers/core.py @@ -1,7 +1,7 @@ -# +# -*- coding: utf-8 -*- # core.py # -# Copyright (C) 2013-2016 Stefan Talpalaru +# Copyright (C) 2013-2024 Ștefan Talpalaru # # Basic plugin template created by: # Copyright (C) 2008 Martijn Voncken @@ -38,16 +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 + 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__) @@ -67,12 +80,54 @@ def disable(self): def update(self): pass + @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] 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 +151,12 @@ def set_config(self, config): 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 34a2467..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 @@ -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 b0bcc7f..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-2016 + (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.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 b8bab6f..9e8bc49 100644 --- a/defaulttrackers/gtkui.py +++ b/defaulttrackers/gtkui.py @@ -1,7 +1,7 @@ -# +# -*- coding: utf-8 -*- # gtkui.py # -# Copyright (C) 2013-2016 Stefan Talpalaru +# Copyright (C) 2013-2019 Ștefan Talpalaru # # Basic plugin template created by: # Copyright (C) 2008 Martijn Voncken @@ -38,6 +38,7 @@ # statement from all source files in the program, then also delete it here. # +from __future__ import absolute_import, unicode_literals import gtk import logging @@ -47,7 +48,7 @@ from deluge.ui.gtkui import dialogs #from pprint import pprint -from common import get_resource +from .common import get_resource log = logging.getLogger(__name__) @@ -109,7 +110,7 @@ def on_add(self, widget): if not self.in_store(url): self.gtkui.store.append([url]) self.gtkui.trackers.append({"url": url}) - except Exception, err: + except Exception as err: dialogs.ErrorDialog("Error", str(err), self.dialog).run() def generate_opts(self): @@ -127,7 +128,7 @@ def on_apply(self, widget): options = self.generate_opts() self.gtkui.store[self.item_id][0] = options["urls"][0] self.gtkui.trackers[self.item_index]["url"] = options["urls"][0] - except Exception, err: + except Exception as err: dialogs.ErrorDialog("Error", str(err), self.dialog).run() def on_cancel(self, widget): @@ -142,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")) @@ -172,16 +174,31 @@ def disable(self): 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.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: @@ -258,3 +275,10 @@ def on_down_button_clicked(self, widget): 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 7f6dc84..f0f4a71 100644 --- a/defaulttrackers/webui.py +++ b/defaulttrackers/webui.py @@ -1,7 +1,7 @@ -# +# -*- coding: utf-8 -*- # webui.py # -# Copyright (C) 2013-2016 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.1-py2.7.egg b/egg/DefaultTrackers-0.1-py2.7.egg deleted file mode 100644 index 015d33d..0000000 Binary files a/egg/DefaultTrackers-0.1-py2.7.egg and /dev/null differ 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 025b55e..1c41832 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ # # setup.py # -# Copyright (C) 2013-2016 Stefan Talpalaru +# Copyright (C) 2013-2024 Ștefan Talpalaru # # Basic plugin template created by: # Copyright (C) 2008 Martijn Voncken @@ -39,15 +39,16 @@ # statement from all source files in the program, then also delete it here. # +from __future__ import absolute_import from setuptools import setup __plugin_name__ = "DefaultTrackers" __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 public torrents" +__description__ = "Add default trackers to all public torrents" __long_description__ = """ 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 @@ -70,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())