deluge-webapi 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deluge_webapi/__init__.py +28 -0
- deluge_webapi/common.py +17 -0
- deluge_webapi/core.py +35 -0
- deluge_webapi/data/config.ui +146 -0
- deluge_webapi/data/webapi.js +198 -0
- deluge_webapi/gtk3ui.py +99 -0
- deluge_webapi/test.py +48 -0
- deluge_webapi/webui.py +176 -0
- deluge_webapi-0.5.0.dist-info/METADATA +71 -0
- deluge_webapi-0.5.0.dist-info/RECORD +15 -0
- deluge_webapi-0.5.0.dist-info/WHEEL +5 -0
- deluge_webapi-0.5.0.dist-info/entry_points.txt +8 -0
- deluge_webapi-0.5.0.dist-info/licenses/AUTHORS +14 -0
- deluge_webapi-0.5.0.dist-info/licenses/LICENSE +27 -0
- deluge_webapi-0.5.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
VERSION = (0, 5, 0)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from deluge.plugins.init import PluginInitBase
|
|
6
|
+
|
|
7
|
+
class CorePlugin(PluginInitBase):
|
|
8
|
+
def __init__(self, plugin_name):
|
|
9
|
+
from .core import Core as _plugin_cls
|
|
10
|
+
self._plugin_cls = _plugin_cls
|
|
11
|
+
super(CorePlugin, self).__init__(plugin_name)
|
|
12
|
+
|
|
13
|
+
class Gtk3UIPlugin(PluginInitBase):
|
|
14
|
+
def __init__(self, plugin_name):
|
|
15
|
+
from .gtk3ui import Gtk3UI as _plugin_cls
|
|
16
|
+
self._plugin_cls = _plugin_cls
|
|
17
|
+
super(Gtk3UIPlugin, self).__init__(plugin_name)
|
|
18
|
+
|
|
19
|
+
class WebUIPlugin(PluginInitBase):
|
|
20
|
+
def __init__(self, plugin_name):
|
|
21
|
+
from .webui import WebUI as _plugin_cls
|
|
22
|
+
self._plugin_cls = _plugin_cls
|
|
23
|
+
super(WebUIPlugin, self).__init__(plugin_name)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
except ImportError:
|
|
27
|
+
# That's what we do when this import prevents docs from being built.
|
|
28
|
+
pass
|
deluge_webapi/common.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from importlib.resources import files
|
|
2
|
+
|
|
3
|
+
from packaging import version
|
|
4
|
+
|
|
5
|
+
from deluge.common import get_version
|
|
6
|
+
|
|
7
|
+
DEFAULT_PREFS = {
|
|
8
|
+
"enable_cors": False,
|
|
9
|
+
"allowed_origin": [],
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
# twisted uses byte strings in deluge version 2.1.0 onward
|
|
13
|
+
use_bytes = version.parse(get_version()) >= version.parse("2.1.0")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_resource(filename):
|
|
17
|
+
return str(files("deluge_webapi").joinpath("data", filename))
|
deluge_webapi/core.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import deluge.configmanager
|
|
4
|
+
from deluge.core.rpcserver import export
|
|
5
|
+
from deluge.plugins.pluginbase import CorePluginBase
|
|
6
|
+
|
|
7
|
+
from .common import DEFAULT_PREFS
|
|
8
|
+
|
|
9
|
+
LOGGER = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Core(CorePluginBase):
|
|
13
|
+
|
|
14
|
+
def enable(self):
|
|
15
|
+
LOGGER.info('Enabling WebAPI plugin CORE ...')
|
|
16
|
+
|
|
17
|
+
self.config = deluge.configmanager.ConfigManager('webapi.conf', DEFAULT_PREFS)
|
|
18
|
+
|
|
19
|
+
def disable(self):
|
|
20
|
+
LOGGER.info('Disabling WebAPI plugin CORE ...')
|
|
21
|
+
|
|
22
|
+
self.config.save()
|
|
23
|
+
|
|
24
|
+
@export
|
|
25
|
+
def set_config(self, config):
|
|
26
|
+
"""sets the config dictionary"""
|
|
27
|
+
|
|
28
|
+
for key in config.keys():
|
|
29
|
+
self.config[key] = config[key]
|
|
30
|
+
|
|
31
|
+
self.config.save()
|
|
32
|
+
|
|
33
|
+
@export
|
|
34
|
+
def get_config(self):
|
|
35
|
+
return self.config.config
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<?xml version="1.0"?>
|
|
2
|
+
<interface>
|
|
3
|
+
<!-- interface-requires gtk+ 2.24 -->
|
|
4
|
+
<!-- interface-naming-policy toplevel-contextual -->
|
|
5
|
+
<object class="GtkWindow" id="window1">
|
|
6
|
+
<property name="can_focus">False</property>
|
|
7
|
+
<child>
|
|
8
|
+
<object class="GtkHBox" id="prefs_box">
|
|
9
|
+
<property name="visible">True</property>
|
|
10
|
+
<property name="can_focus">False</property>
|
|
11
|
+
<child>
|
|
12
|
+
<object class="GtkLayout" id="layout1">
|
|
13
|
+
<property name="visible">True</property>
|
|
14
|
+
<property name="can_focus">False</property>
|
|
15
|
+
<child>
|
|
16
|
+
<object class="GtkCheckButton" id="enable_cors">
|
|
17
|
+
<property name="label" translatable="yes">enable CORS (cross-origin request)</property>
|
|
18
|
+
<property name="width_request">247</property>
|
|
19
|
+
<property name="height_request">80</property>
|
|
20
|
+
<property name="visible">True</property>
|
|
21
|
+
<property name="can_focus">True</property>
|
|
22
|
+
<property name="receives_default">False</property>
|
|
23
|
+
<property name="use_action_appearance">False</property>
|
|
24
|
+
<property name="draw_indicator">True</property>
|
|
25
|
+
</object>
|
|
26
|
+
<packing>
|
|
27
|
+
<property name="x">18</property>
|
|
28
|
+
</packing>
|
|
29
|
+
</child>
|
|
30
|
+
<child>
|
|
31
|
+
<object class="GtkLabel" id="label1">
|
|
32
|
+
<property name="width_request">152</property>
|
|
33
|
+
<property name="height_request">21</property>
|
|
34
|
+
<property name="visible">True</property>
|
|
35
|
+
<property name="can_focus">False</property>
|
|
36
|
+
<property name="xalign">0</property>
|
|
37
|
+
<property name="yalign">0</property>
|
|
38
|
+
<property name="label" translatable="yes">Allowed domain name :</property>
|
|
39
|
+
</object>
|
|
40
|
+
<packing>
|
|
41
|
+
<property name="x">19</property>
|
|
42
|
+
<property name="y">60</property>
|
|
43
|
+
</packing>
|
|
44
|
+
</child>
|
|
45
|
+
<child>
|
|
46
|
+
<object class="GtkEntry" id="new_domain">
|
|
47
|
+
<property name="width_request">153</property>
|
|
48
|
+
<property name="height_request">27</property>
|
|
49
|
+
<property name="visible">True</property>
|
|
50
|
+
<property name="can_focus">True</property>
|
|
51
|
+
<property name="invisible_char">●</property>
|
|
52
|
+
<property name="primary_icon_activatable">False</property>
|
|
53
|
+
<property name="secondary_icon_activatable">False</property>
|
|
54
|
+
<property name="primary_icon_sensitive">True</property>
|
|
55
|
+
<property name="secondary_icon_sensitive">True</property>
|
|
56
|
+
</object>
|
|
57
|
+
<packing>
|
|
58
|
+
<property name="x">67</property>
|
|
59
|
+
<property name="y">286</property>
|
|
60
|
+
</packing>
|
|
61
|
+
</child>
|
|
62
|
+
<child>
|
|
63
|
+
<object class="GtkButton" id="add_button">
|
|
64
|
+
<property name="label" translatable="yes">Add</property>
|
|
65
|
+
<property name="width_request">49</property>
|
|
66
|
+
<property name="height_request">27</property>
|
|
67
|
+
<property name="visible">True</property>
|
|
68
|
+
<property name="can_focus">True</property>
|
|
69
|
+
<property name="receives_default">True</property>
|
|
70
|
+
<property name="use_action_appearance">False</property>
|
|
71
|
+
</object>
|
|
72
|
+
<packing>
|
|
73
|
+
<property name="x">16</property>
|
|
74
|
+
<property name="y">286</property>
|
|
75
|
+
</packing>
|
|
76
|
+
</child>
|
|
77
|
+
<child>
|
|
78
|
+
<object class="GtkTreeView" id="listview_allowed_domains">
|
|
79
|
+
<property name="width_request">263</property>
|
|
80
|
+
<property name="height_request">194</property>
|
|
81
|
+
<property name="visible">True</property>
|
|
82
|
+
<property name="can_focus">True</property>
|
|
83
|
+
</object>
|
|
84
|
+
<packing>
|
|
85
|
+
<property name="x">16</property>
|
|
86
|
+
<property name="y">84</property>
|
|
87
|
+
</packing>
|
|
88
|
+
</child>
|
|
89
|
+
<child>
|
|
90
|
+
<object class="GtkScrolledWindow" id="scrolledwindow_allowed_domains">
|
|
91
|
+
<property name="width_request">356</property>
|
|
92
|
+
<property name="height_request">197</property>
|
|
93
|
+
<property name="visible">True</property>
|
|
94
|
+
<property name="can_focus">True</property>
|
|
95
|
+
<property name="hscrollbar_policy">automatic</property>
|
|
96
|
+
<property name="vscrollbar_policy">automatic</property>
|
|
97
|
+
<child>
|
|
98
|
+
<placeholder/>
|
|
99
|
+
</child>
|
|
100
|
+
</object>
|
|
101
|
+
<packing>
|
|
102
|
+
<property name="x">16</property>
|
|
103
|
+
<property name="y">81</property>
|
|
104
|
+
</packing>
|
|
105
|
+
</child>
|
|
106
|
+
<child>
|
|
107
|
+
<object class="GtkButton" id="remove_button">
|
|
108
|
+
<property name="label" translatable="yes">Remove</property>
|
|
109
|
+
<property name="width_request">69</property>
|
|
110
|
+
<property name="height_request">27</property>
|
|
111
|
+
<property name="visible">True</property>
|
|
112
|
+
<property name="can_focus">True</property>
|
|
113
|
+
<property name="receives_default">True</property>
|
|
114
|
+
<property name="use_action_appearance">False</property>
|
|
115
|
+
</object>
|
|
116
|
+
<packing>
|
|
117
|
+
<property name="x">221</property>
|
|
118
|
+
<property name="y">286</property>
|
|
119
|
+
</packing>
|
|
120
|
+
</child>
|
|
121
|
+
<child>
|
|
122
|
+
<object class="GtkButton" id="remove_all_button">
|
|
123
|
+
<property name="label" translatable="yes">Remove all</property>
|
|
124
|
+
<property name="width_request">82</property>
|
|
125
|
+
<property name="height_request">27</property>
|
|
126
|
+
<property name="visible">True</property>
|
|
127
|
+
<property name="can_focus">True</property>
|
|
128
|
+
<property name="receives_default">True</property>
|
|
129
|
+
<property name="use_action_appearance">False</property>
|
|
130
|
+
</object>
|
|
131
|
+
<packing>
|
|
132
|
+
<property name="x">290</property>
|
|
133
|
+
<property name="y">286</property>
|
|
134
|
+
</packing>
|
|
135
|
+
</child>
|
|
136
|
+
</object>
|
|
137
|
+
<packing>
|
|
138
|
+
<property name="expand">True</property>
|
|
139
|
+
<property name="fill">True</property>
|
|
140
|
+
<property name="position">0</property>
|
|
141
|
+
</packing>
|
|
142
|
+
</child>
|
|
143
|
+
</object>
|
|
144
|
+
</child>
|
|
145
|
+
</object>
|
|
146
|
+
</interface>
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
|
|
2
|
+
WebApiPanel = Ext.extend(Ext.form.FormPanel, {
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
constructor: function(config) {
|
|
6
|
+
|
|
7
|
+
config = Ext.apply({
|
|
8
|
+
title: _('WebAPI'),
|
|
9
|
+
border: false,
|
|
10
|
+
labelWidth: 10,
|
|
11
|
+
autoHeight: true
|
|
12
|
+
}, config);
|
|
13
|
+
|
|
14
|
+
WebApiPanel.superclass.constructor.call(this, config);
|
|
15
|
+
},
|
|
16
|
+
initComponent: function() {
|
|
17
|
+
WebApiPanel.superclass.initComponent.call(this);
|
|
18
|
+
this.opts = new Deluge.OptionsManager();
|
|
19
|
+
|
|
20
|
+
var fieldset = this.add({
|
|
21
|
+
xtype: 'fieldset',
|
|
22
|
+
title: _('WebAPI Settings'),
|
|
23
|
+
defaultType: 'checkbox',
|
|
24
|
+
autoHeight: true
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
this.opts.bind('enable_cors', fieldset.add({
|
|
28
|
+
name: 'enable_cors',
|
|
29
|
+
id: 'enable_cors',
|
|
30
|
+
height: 22,
|
|
31
|
+
fieldLabel: '',
|
|
32
|
+
labelSeparator: '',
|
|
33
|
+
boxLabel: _('Enable CORS (cross-origin request)')
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
this.list = this.add({
|
|
37
|
+
xtype: 'listview',
|
|
38
|
+
height: 150,
|
|
39
|
+
store: new Ext.data.ArrayStore({
|
|
40
|
+
fields: [
|
|
41
|
+
{ name: 'domain', mapping: 0 }
|
|
42
|
+
]
|
|
43
|
+
}),
|
|
44
|
+
columns: [{
|
|
45
|
+
id: 'domain',
|
|
46
|
+
header: _('Allowed domain'),
|
|
47
|
+
width: 1,
|
|
48
|
+
sortable: true,
|
|
49
|
+
dataIndex: 'domain'
|
|
50
|
+
}],
|
|
51
|
+
singleSelect: true,
|
|
52
|
+
autoExpandColumn: 'domain',
|
|
53
|
+
listeners: {
|
|
54
|
+
selectionchange: { fn: this.onDomainSelect, scope: this }
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
this.panel = this.add({
|
|
59
|
+
region: 'center',
|
|
60
|
+
autoScroll: true,
|
|
61
|
+
items: [this.list]
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
this.domain_name = this.opts.bind('domain_name', fieldset.add({
|
|
65
|
+
fieldLabel: '',
|
|
66
|
+
xtype: 'textfield',
|
|
67
|
+
anchor: '50%',
|
|
68
|
+
name: 'domain_name',
|
|
69
|
+
id: 'domain_name',
|
|
70
|
+
autoWidth: true,
|
|
71
|
+
value: ''
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
this.toolbar = this.add({
|
|
75
|
+
fieldLabel: _(''),
|
|
76
|
+
name: 'list_toolbar',
|
|
77
|
+
xtype: 'container',
|
|
78
|
+
layout: 'hbox',
|
|
79
|
+
items: [
|
|
80
|
+
{
|
|
81
|
+
xtype: 'button',
|
|
82
|
+
text: 'Add'
|
|
83
|
+
},
|
|
84
|
+
this.domain_name,
|
|
85
|
+
{
|
|
86
|
+
xtype: 'button',
|
|
87
|
+
text: 'Remove'
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
xtype: 'button',
|
|
91
|
+
text: 'Remove all'
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
this.toolbar.getComponent(0).setHandler(this.addDomain, this);
|
|
97
|
+
this.toolbar.getComponent(2).setHandler(this.removeDomain, this);
|
|
98
|
+
this.toolbar.getComponent(3).setHandler(this.removeAllDomain, this);
|
|
99
|
+
|
|
100
|
+
deluge.preferences.on('show', this.onPreferencesShow, this);
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
removeDomain: function() {
|
|
104
|
+
if (!this.selectedDomain) return;
|
|
105
|
+
var index = this.list.getStore().find('domain', this.selectedDomain);
|
|
106
|
+
if (index == -1) return;
|
|
107
|
+
this.list.getStore().removeAt(index);
|
|
108
|
+
index = this.allowedDomains.indexOf(this.selectedDomain);
|
|
109
|
+
if (index !== -1) this.allowedDomains.splice(index, 1);
|
|
110
|
+
this.selectedDomain = false;
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
removeAllDomain: function() {
|
|
114
|
+
this.list.getStore().removeAll();
|
|
115
|
+
this.allowedDomains = [];
|
|
116
|
+
this.selectedDomain = false;
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
onDomainSelect: function (dv, selections) {
|
|
120
|
+
if (selections.length === 0) return;
|
|
121
|
+
var r = dv.getRecords(selections)[0];
|
|
122
|
+
this.selectedDomain = r.get('domain');
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
addDomain: function() {
|
|
126
|
+
var changed = this.opts.getDirty();
|
|
127
|
+
if (!Ext.isObjectEmpty(changed)) {
|
|
128
|
+
if (!Ext.isEmpty(changed['domain_name'])) {
|
|
129
|
+
changed['domain_name'] = changed['domain_name'];
|
|
130
|
+
this.allowedDomains.push(changed['domain_name']);
|
|
131
|
+
this.updateDomainsGrid();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
updateDomainsGrid: function () {
|
|
137
|
+
var Domains = [];
|
|
138
|
+
Ext.each(this.allowedDomains, function (domain) {
|
|
139
|
+
Domains.push([domain]);
|
|
140
|
+
}, this);
|
|
141
|
+
this.list.getStore().loadData(Domains);
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
onPreferencesShow: function () {
|
|
145
|
+
deluge.client.webapi.get_config({
|
|
146
|
+
success: function (config) {
|
|
147
|
+
if (!Ext.isEmpty(config['enable_cors'])) {
|
|
148
|
+
config['enable_cors'] = config['enable_cors'];
|
|
149
|
+
}
|
|
150
|
+
if (!Ext.isEmpty(config['allowed_origin'])) {
|
|
151
|
+
config['allowed_origin'] = config['allowed_origin'];
|
|
152
|
+
}
|
|
153
|
+
this.allowedDomains = config['allowed_origin'];
|
|
154
|
+
this.updateDomainsGrid();
|
|
155
|
+
this.opts.set(config);
|
|
156
|
+
},
|
|
157
|
+
scope: this
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
onApply: function(e) {
|
|
162
|
+
var changed = this.opts.getDirty();
|
|
163
|
+
if (!Ext.isObjectEmpty(changed)) {
|
|
164
|
+
if (!Ext.isEmpty(changed['enable_cors'])) {
|
|
165
|
+
changed['enable_cors'] = changed['enable_cors'];
|
|
166
|
+
}
|
|
167
|
+
deluge.client.webapi.set_config(changed, {
|
|
168
|
+
success: this.onSetConfig,
|
|
169
|
+
scope: this
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
var config = {};
|
|
173
|
+
config['allowed_origin'] = this.allowedDomains;
|
|
174
|
+
console.log(config);
|
|
175
|
+
deluge.client.webapi.set_config(config);
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
onSetConfig: function() {
|
|
179
|
+
this.opts.commit();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
WebApiPlugin = Ext.extend(Deluge.Plugin, {
|
|
186
|
+
|
|
187
|
+
name: 'WebAPI',
|
|
188
|
+
|
|
189
|
+
onDisable: function() {
|
|
190
|
+
deluge.preferences.removePage(this.prefsPage);
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
onEnable: function() {
|
|
194
|
+
this.prefsPage = deluge.preferences.addPage(new WebApiPanel());
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
Deluge.registerPlugin('WebAPI', WebApiPlugin);
|
deluge_webapi/gtk3ui.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from gi.repository import Gtk
|
|
4
|
+
from deluge import component
|
|
5
|
+
from deluge.plugins.pluginbase import Gtk3PluginBase
|
|
6
|
+
from deluge.ui.client import client
|
|
7
|
+
|
|
8
|
+
from .common import get_resource
|
|
9
|
+
|
|
10
|
+
LOGGER = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Gtk3UI(Gtk3PluginBase):
|
|
14
|
+
|
|
15
|
+
plugin_id = 'WebAPI'
|
|
16
|
+
|
|
17
|
+
def enable(self):
|
|
18
|
+
"""Triggers when plugin is enabled."""
|
|
19
|
+
LOGGER.info('Enabling WebAPI plugin GTK ...')
|
|
20
|
+
|
|
21
|
+
self.builder = Gtk.Builder.new_from_file(get_resource("config.ui"))
|
|
22
|
+
|
|
23
|
+
component.get('Preferences').add_page('WebAPI', self.builder.get_object('prefs_box'))
|
|
24
|
+
component.get('PluginManager').register_hook('on_apply_prefs', self.on_apply_prefs)
|
|
25
|
+
component.get('PluginManager').register_hook('on_show_prefs', self.on_show_prefs)
|
|
26
|
+
|
|
27
|
+
self.builder.get_object('add_button').connect('clicked', self.add_domain)
|
|
28
|
+
self.builder.get_object('remove_button').connect('clicked', self.remove_domain)
|
|
29
|
+
self.builder.get_object('remove_all_button').connect('clicked', self.remove_all_domain)
|
|
30
|
+
self.create_listview()
|
|
31
|
+
|
|
32
|
+
def disable(self):
|
|
33
|
+
"""Triggers when plugin is disabled."""
|
|
34
|
+
LOGGER.info('Disabling WebAPI plugin GTK ...')
|
|
35
|
+
|
|
36
|
+
component.get('Preferences').remove_page('WebAPI')
|
|
37
|
+
component.get('PluginManager').deregister_hook('on_apply_prefs', self.on_apply_prefs)
|
|
38
|
+
component.get('PluginManager').deregister_hook('on_show_prefs', self.on_show_prefs)
|
|
39
|
+
|
|
40
|
+
def on_apply_prefs(self):
|
|
41
|
+
"""Triggers when plugin prefs are apply."""
|
|
42
|
+
config = {}
|
|
43
|
+
if self.builder.get_object('enable_cors').get_active():
|
|
44
|
+
config['enable_cors'] = True
|
|
45
|
+
else:
|
|
46
|
+
config['enable_cors'] = False
|
|
47
|
+
|
|
48
|
+
config['allowed_origin'] = []
|
|
49
|
+
for i, value in enumerate(self.model):
|
|
50
|
+
item = self.model.get_iter(i)
|
|
51
|
+
config['allowed_origin'].append(self.model[item][0])
|
|
52
|
+
|
|
53
|
+
client.webapi.set_config(config)
|
|
54
|
+
|
|
55
|
+
def on_show_prefs(self):
|
|
56
|
+
"""Triggers when plugin prefs are show."""
|
|
57
|
+
client.webapi.get_config().addCallback(self.cb_get_config)
|
|
58
|
+
|
|
59
|
+
def cb_get_config(self, config):
|
|
60
|
+
"""callback for on show_prefs"""
|
|
61
|
+
self.builder.get_object('enable_cors').set_active(config['enable_cors'])
|
|
62
|
+
self.remove_all_domain()
|
|
63
|
+
for domain in config['allowed_origin']:
|
|
64
|
+
self.model.append([domain])
|
|
65
|
+
|
|
66
|
+
def create_listview(self):
|
|
67
|
+
self.model = Gtk.ListStore(str)
|
|
68
|
+
self.list = self.builder.get_object('listview_allowed_domains')
|
|
69
|
+
self.list.set_model(self.model)
|
|
70
|
+
|
|
71
|
+
parent = self.list.get_parent()
|
|
72
|
+
parent.remove(self.list)
|
|
73
|
+
self.scrollTree = self.builder.get_object('scrolledwindow_allowed_domains')
|
|
74
|
+
self.scrollTree.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
|
75
|
+
self.scrollTree.add(self.list)
|
|
76
|
+
|
|
77
|
+
column = Gtk.TreeViewColumn('domain', Gtk.CellRendererText(), text=0)
|
|
78
|
+
column.set_clickable(True)
|
|
79
|
+
column.set_resizable(True)
|
|
80
|
+
self.list.append_column(column)
|
|
81
|
+
|
|
82
|
+
self.selection = self.list.get_selection()
|
|
83
|
+
|
|
84
|
+
def add_domain(self, button):
|
|
85
|
+
domain = self.builder.get_object('new_domain').get_text()
|
|
86
|
+
self.model.append([domain])
|
|
87
|
+
|
|
88
|
+
def remove_domain(self, button):
|
|
89
|
+
# if there is still an entry in the model
|
|
90
|
+
if len(self.model) != 0:
|
|
91
|
+
# get the selection
|
|
92
|
+
(model, item) = self.selection.get_selected()
|
|
93
|
+
if item is not None:
|
|
94
|
+
self.model.remove(item)
|
|
95
|
+
|
|
96
|
+
def remove_all_domain(self, button=None):
|
|
97
|
+
for i, value in enumerate(self.model):
|
|
98
|
+
item = self.model.get_iter(0)
|
|
99
|
+
self.model.remove(item)
|
deluge_webapi/test.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from os import environ
|
|
3
|
+
|
|
4
|
+
PASSWORD = environ.get('WEBAPI_PASSWORD', '*****')
|
|
5
|
+
COOKIES = None
|
|
6
|
+
REQUEST_ID = 0
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def send_request(method, params=None):
|
|
10
|
+
global REQUEST_ID
|
|
11
|
+
global COOKIES
|
|
12
|
+
REQUEST_ID += 1
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
|
|
16
|
+
response = requests.post(
|
|
17
|
+
'http://localhost:8112/json',
|
|
18
|
+
json={'id': REQUEST_ID, 'method': method, 'params': params or []},
|
|
19
|
+
cookies=COOKIES)
|
|
20
|
+
|
|
21
|
+
except requests.exceptions.ConnectionError:
|
|
22
|
+
raise Exception('WebUI seems to be unavailable. Run deluge-web or enable WebUI plugin using other thin client.')
|
|
23
|
+
|
|
24
|
+
data = response.json()
|
|
25
|
+
|
|
26
|
+
error = data.get('error')
|
|
27
|
+
|
|
28
|
+
if error:
|
|
29
|
+
msg = error['message']
|
|
30
|
+
|
|
31
|
+
if msg == 'Unknown method':
|
|
32
|
+
msg += '. Check WebAPI is enabled.'
|
|
33
|
+
|
|
34
|
+
raise Exception('API response: %s' % msg)
|
|
35
|
+
|
|
36
|
+
COOKIES = response.cookies
|
|
37
|
+
|
|
38
|
+
return data['result']
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
assert send_request('auth.login', [PASSWORD]), 'Unable to log in. Check password.'
|
|
42
|
+
|
|
43
|
+
version_number = send_request('webapi.get_api_version')
|
|
44
|
+
assert version_number
|
|
45
|
+
|
|
46
|
+
print('WebAPI version: %s' % version_number)
|
|
47
|
+
|
|
48
|
+
print('Success')
|
deluge_webapi/webui.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import deluge.configmanager
|
|
4
|
+
from deluge import component, common
|
|
5
|
+
from deluge.plugins.pluginbase import WebPluginBase
|
|
6
|
+
from deluge.ui.client import client
|
|
7
|
+
from deluge.ui.web.json_api import export as export_api
|
|
8
|
+
from twisted.internet.defer import Deferred
|
|
9
|
+
from twisted.web import http, server
|
|
10
|
+
|
|
11
|
+
from .common import DEFAULT_PREFS, get_resource, use_bytes
|
|
12
|
+
|
|
13
|
+
LOGGER = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class WebUI(WebPluginBase):
|
|
17
|
+
|
|
18
|
+
scripts = [get_resource('webapi.js')]
|
|
19
|
+
|
|
20
|
+
def enable(self):
|
|
21
|
+
"""Triggers when plugin is enabled."""
|
|
22
|
+
LOGGER.info('Enabling WebAPI plugin WEB ...')
|
|
23
|
+
|
|
24
|
+
self.patched = False
|
|
25
|
+
self.config = deluge.configmanager.ConfigManager('webapi.conf', DEFAULT_PREFS)
|
|
26
|
+
|
|
27
|
+
if self.config['enable_cors']:
|
|
28
|
+
self.patch_web_ui()
|
|
29
|
+
|
|
30
|
+
def disable(self):
|
|
31
|
+
"""Triggers when plugin is disabled."""
|
|
32
|
+
LOGGER.info('Disabling WebAPI plugin WEB ...')
|
|
33
|
+
|
|
34
|
+
self.unpatch_web_ui()
|
|
35
|
+
|
|
36
|
+
def patch_web_ui(self):
|
|
37
|
+
"""Applies CORS headers patching to the WebUI JSON component."""
|
|
38
|
+
|
|
39
|
+
if self.patched:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
LOGGER.info('Patching webui for CORS ...')
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
cmp_json = component.get('JSON')
|
|
46
|
+
|
|
47
|
+
except KeyError:
|
|
48
|
+
LOGGER.warning('The JSON component is not available in this process. CORS patching is skipped.')
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
self.old_render = cmp_json.render
|
|
52
|
+
self.old_send_request = cmp_json._send_response
|
|
53
|
+
cmp_json.render = self.render_patch
|
|
54
|
+
cmp_json._send_response = self._send_response_patch
|
|
55
|
+
|
|
56
|
+
self.patched = True
|
|
57
|
+
|
|
58
|
+
def unpatch_web_ui(self):
|
|
59
|
+
"""Reverts CORS headers patching of the WebUI JSON component."""
|
|
60
|
+
|
|
61
|
+
if not self.patched:
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
LOGGER.info('Unpatching webui for CORS ...')
|
|
65
|
+
|
|
66
|
+
cmp_json = component.get('JSON')
|
|
67
|
+
cmp_json.render = self.old_render
|
|
68
|
+
cmp_json._send_response = self.old_send_request
|
|
69
|
+
|
|
70
|
+
self.patched = False
|
|
71
|
+
|
|
72
|
+
def render_patch(self, request):
|
|
73
|
+
|
|
74
|
+
if request.method != (b'OPTIONS' if use_bytes else 'OPTIONS'):
|
|
75
|
+
return self.old_render(request)
|
|
76
|
+
|
|
77
|
+
request.setResponseCode(http.OK)
|
|
78
|
+
origin = request.getHeader('Origin')
|
|
79
|
+
|
|
80
|
+
if origin in self.config['allowed_origin']:
|
|
81
|
+
request.setHeader('Access-Control-Allow-Origin', origin)
|
|
82
|
+
request.setHeader('Access-Control-Allow-Headers', 'content-type')
|
|
83
|
+
request.setHeader('Access-Control-Allow-Methods', 'POST')
|
|
84
|
+
request.setHeader('Access-Control-Allow-Credentials', 'true')
|
|
85
|
+
|
|
86
|
+
request.write(b'' if use_bytes else '')
|
|
87
|
+
request.finish()
|
|
88
|
+
return server.NOT_DONE_YET
|
|
89
|
+
|
|
90
|
+
def _send_response_patch(self, request, response):
|
|
91
|
+
|
|
92
|
+
if request._disconnected:
|
|
93
|
+
return ''
|
|
94
|
+
|
|
95
|
+
origin = request.getHeader('Origin')
|
|
96
|
+
|
|
97
|
+
if origin in self.config['allowed_origin']:
|
|
98
|
+
request.setHeader('Access-Control-Allow-Origin', origin)
|
|
99
|
+
request.setHeader('Access-Control-Allow-Credentials', 'true')
|
|
100
|
+
|
|
101
|
+
return self.old_send_request(request, response)
|
|
102
|
+
|
|
103
|
+
@export_api
|
|
104
|
+
def get_torrents(self, ids=None, params=None):
|
|
105
|
+
"""Returns information about all or a definite torrent.
|
|
106
|
+
Returned information can be filtered by supplying wanted parameter names.
|
|
107
|
+
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
if params is None:
|
|
111
|
+
filter_fields = ['name', 'comment', 'hash', 'save_path']
|
|
112
|
+
else:
|
|
113
|
+
if isinstance(params, list):
|
|
114
|
+
filter_fields = params
|
|
115
|
+
else:
|
|
116
|
+
filter_fields = params.replace(" ","").split(",")
|
|
117
|
+
|
|
118
|
+
proxy = component.get('SessionProxy')
|
|
119
|
+
|
|
120
|
+
filter_dict = {}
|
|
121
|
+
if ids is not None:
|
|
122
|
+
filter_dict['id'] = set(ids).intersection(set(proxy.torrents.keys()))
|
|
123
|
+
|
|
124
|
+
document = {
|
|
125
|
+
'torrents': []
|
|
126
|
+
}
|
|
127
|
+
response = Deferred()
|
|
128
|
+
|
|
129
|
+
deffered_torrents = proxy.get_torrents_status(filter_dict, filter_fields)
|
|
130
|
+
|
|
131
|
+
def on_complete(torrents_dict):
|
|
132
|
+
document['torrents'] = list(torrents_dict.values())
|
|
133
|
+
response.callback(document)
|
|
134
|
+
deffered_torrents.addCallback(on_complete)
|
|
135
|
+
|
|
136
|
+
return response
|
|
137
|
+
|
|
138
|
+
@export_api
|
|
139
|
+
def add_torrent(self, metainfo, options=None):
|
|
140
|
+
"""Adds a torrent with the given options.
|
|
141
|
+
metainfo could either be base64 torrent data or a magnet link.
|
|
142
|
+
|
|
143
|
+
Returns `torrent_id` string or None.
|
|
144
|
+
|
|
145
|
+
Available options are listed in deluge.core.torrent.TorrentOptions.
|
|
146
|
+
|
|
147
|
+
:rtype: None|str
|
|
148
|
+
"""
|
|
149
|
+
if options is None:
|
|
150
|
+
options = {}
|
|
151
|
+
|
|
152
|
+
coreconfig = component.get('CoreConfig')
|
|
153
|
+
|
|
154
|
+
if 'download_location' not in options.keys():
|
|
155
|
+
options.update({'download_location': coreconfig.get("download_location")})
|
|
156
|
+
|
|
157
|
+
if common.is_magnet(metainfo):
|
|
158
|
+
LOGGER.info('Adding torrent from magnet URI `%s` using options `%s` ...', metainfo, options)
|
|
159
|
+
result = client.core.add_torrent_magnet(metainfo, options)
|
|
160
|
+
|
|
161
|
+
else:
|
|
162
|
+
LOGGER.info('Adding torrent from base64 string using options `%s` ...', options)
|
|
163
|
+
result = client.core.add_torrent_file(None, metainfo, options)
|
|
164
|
+
|
|
165
|
+
return result
|
|
166
|
+
|
|
167
|
+
@export_api
|
|
168
|
+
def remove_torrent(self, torrent_id, remove_data=False):
|
|
169
|
+
"""Removes a given torrent. Optionally can remove data."""
|
|
170
|
+
return client.core.remove_torrent(torrent_id, remove_data)
|
|
171
|
+
|
|
172
|
+
@export_api
|
|
173
|
+
def get_api_version(self):
|
|
174
|
+
"""Returns WebAPI plugin version."""
|
|
175
|
+
from . import VERSION
|
|
176
|
+
return '.'.join(map(str, VERSION))
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deluge-webapi
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Plugin for Deluge WebUI providing sane JSON API.
|
|
5
|
+
Author-email: Igor `idle sign` Starikov <idlesign@yandex.ru>
|
|
6
|
+
License: BSD 3-Clause License
|
|
7
|
+
Project-URL: Homepage, https://github.com/idlesign/deluge-webapi
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
13
|
+
Classifier: Environment :: Plugins
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/x-rst
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
License-File: AUTHORS
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
deluge-webapi
|
|
22
|
+
=============
|
|
23
|
+
https://github.com/idlesign/deluge-webapi
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
.. image:: https://img.shields.io/pypi/v/deluge-webapi.svg
|
|
27
|
+
:target: https://pypi.python.org/pypi/deluge-webapi
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
Description
|
|
31
|
+
-----------
|
|
32
|
+
|
|
33
|
+
*Plugin for Deluge WebUI providing sane JSON API.*
|
|
34
|
+
|
|
35
|
+
Exposes some sane JSON API through Deluge WebUI (missing out of the box or restyled) so that you can manipulate
|
|
36
|
+
Deluge from your programs using HTTP requests.
|
|
37
|
+
|
|
38
|
+
Supported methods:
|
|
39
|
+
|
|
40
|
+
* ``get_torrents``
|
|
41
|
+
* ``add_torrent`` (magnet or file)
|
|
42
|
+
* ``remove_torrent``
|
|
43
|
+
* ``get_api_version``
|
|
44
|
+
|
|
45
|
+
Deluge is a lightweight, Free Software, cross-platform BitTorrent client. Download it at http://deluge-torrent.org/
|
|
46
|
+
|
|
47
|
+
.. note:: Use egg version 0.4.0 or higher for Deluge 2
|
|
48
|
+
.. note:: Use egg version 0.3.2 or lower for Deluge 1.x
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
Installation
|
|
52
|
+
------------
|
|
53
|
+
|
|
54
|
+
1. Get plugin egg file, from ``dist/`` (https://github.com/idlesign/deluge-webapi/tree/master/dist) directory.
|
|
55
|
+
|
|
56
|
+
2. Open Deluge Web UI, go to "Preferences -> Plugins -> Install plugin" and choose egg file.
|
|
57
|
+
|
|
58
|
+
3. Activate ``WebAPI`` plugin.
|
|
59
|
+
|
|
60
|
+
4. In case you use Client-Server setup, you'll need to install the plugin both on client and server, see - https://dev.deluge-torrent.org/wiki/Plugins#Client-ServerSetups
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
.. note::
|
|
64
|
+
|
|
65
|
+
To build .egg file from source code yourself use ``python setup.py bdist_egg`` command in source code directory.
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
Documentation
|
|
69
|
+
-------------
|
|
70
|
+
|
|
71
|
+
http://deluge-webapi.readthedocs.org/
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
deluge_webapi/__init__.py,sha256=6Cvdwf2wHE8jmKeamI2MV25JtD6uaQebpAqxpGZIND4,887
|
|
2
|
+
deluge_webapi/common.py,sha256=oCh7t6hw6mHDOxnOiVreRhAkxCF4JPV-OvTDf7O0Idk,404
|
|
3
|
+
deluge_webapi/core.py,sha256=b2M_2usudeYbVB5mCJEvklvKCT9HpNyjHw3gskcZ-bk,782
|
|
4
|
+
deluge_webapi/gtk3ui.py,sha256=8rw9X0VyN0VDLyIlpwX-t9-BSrqDEE0huNvEJfwA2a0,3669
|
|
5
|
+
deluge_webapi/test.py,sha256=wRGstZUF9xZMlVUzrAFwVZSHFaU7-1N5ChJY0RdpKHQ,1116
|
|
6
|
+
deluge_webapi/webui.py,sha256=UKafC3J5D0jNSEUYG9IaU8BiR8boX-qpj7NAiwMLujE,5548
|
|
7
|
+
deluge_webapi/data/config.ui,sha256=E9iEGGm5ro6hAu5Mux_7_jKaNe3Xel7gMbc65CDUNqo,6660
|
|
8
|
+
deluge_webapi/data/webapi.js,sha256=dC6N9pYLo95CWsnXrSg7D9OvKzTmB7P6PA_FWzfPvog,5803
|
|
9
|
+
deluge_webapi-0.5.0.dist-info/licenses/AUTHORS,sha256=No-8N3pBL_yFSDBESx1LD4PK31d1pgNmF0vScpQaU2c,340
|
|
10
|
+
deluge_webapi-0.5.0.dist-info/licenses/LICENSE,sha256=LevsUhQhcqhuI9hWa6WD6IUknGIGVpyaXb_HdpHCNEE,1504
|
|
11
|
+
deluge_webapi-0.5.0.dist-info/METADATA,sha256=2hNOPIq2dUtpxaVsQ3VM_zm4ro4qkaJRYzTR_fyGICw,2126
|
|
12
|
+
deluge_webapi-0.5.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
13
|
+
deluge_webapi-0.5.0.dist-info/entry_points.txt,sha256=BMX6LLx1C2hQm9Sb10Url1p5zJuOBdoBkcTIhwIFSf0,171
|
|
14
|
+
deluge_webapi-0.5.0.dist-info/top_level.txt,sha256=ci4wChOllyVj1-BYYxvrdExM0wQjWtM9s8u16neoekg,14
|
|
15
|
+
deluge_webapi-0.5.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
deluge-webapi authors
|
|
2
|
+
=====================
|
|
3
|
+
|
|
4
|
+
Created by Igor `idle sign` Starikov.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Contributors
|
|
8
|
+
------------
|
|
9
|
+
|
|
10
|
+
theonedemon <https://github.com/theonedemon>
|
|
11
|
+
Daniel Marcos <https://github.com/dmarcosl>
|
|
12
|
+
Brandon Gillis <https://github.com/BrandonGillis>
|
|
13
|
+
Ivan Filippov <https://github.com/ivanfilippov>
|
|
14
|
+
griest024 <https://github.com/griest024>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Copyright (c) 2014-2019 Igor `idle sign` Starikov
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
8
|
+
list of conditions and the following disclaimer.
|
|
9
|
+
|
|
10
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
|
12
|
+
and/or other materials provided with the distribution.
|
|
13
|
+
|
|
14
|
+
* Neither the name of the deluge-webapi nor the names of its
|
|
15
|
+
contributors may be used to endorse or promote products derived from
|
|
16
|
+
this software without specific prior written permission.
|
|
17
|
+
|
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
19
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
20
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
23
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
24
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
25
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
26
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
27
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
deluge_webapi
|