deluge-api 0.1.0 → 0.1.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7e9faac059b0177c915d0ca94ef0de556498441e
4
- data.tar.gz: 59d61e6ae269205d9c7db3349c8749d4ecaab409
3
+ metadata.gz: 1bfb48ae99cefbc2e9af6c2633ab9ad0a00130b4
4
+ data.tar.gz: 75e047cba2ecf8382b834c36822f85188af9a21a
5
5
  SHA512:
6
- metadata.gz: a3f922e08b48c0ebe7fdef0b0c0420c0546ad315382e0cf6f979f315058003610bb643ed56f1a4423e71fd8789ee77786e30f0bf0e45e61fb1dd66a2840b81ba
7
- data.tar.gz: 1b18c1573281c8ac0e4052cb989d273ac190676fc7d3fab0114391d73b01842b6c82772246aa8c7c8caf8bd54b1287b29cb3149273dd927538dc6da5a0205de0
6
+ metadata.gz: 64ce08683db0d39a0a4c38526b932713527d57a6f93f8e17c742a04f1d2c2ebb4d8ad6b6174b041a960c4b1d307d44a9ae527516cfb3070402537b0c3fadb423
7
+ data.tar.gz: 3a91b8b7a1325568ac82b9798d43ea2deb972a2fae178a7bf6c25db10735943ed4485791d35d9cf963192a6fc9faae3cd6f49c502eaacf808884537a50358bd7
data/README.md CHANGED
@@ -16,10 +16,13 @@ http://deluge.readthedocs.org/en/develop/core/rpc.html#remote-api
16
16
  require 'deluge'
17
17
 
18
18
  # Initialize client
19
- client = Deluge::Api::Client.new(host: 'localhost', port: 58846, login: 'username', password: 'password')
19
+ client = Deluge::Api::Client.new(
20
+ host: 'localhost', port: 58846,
21
+ login: 'username', password: 'password'
22
+ )
20
23
 
21
24
  # Start connection and authenticate
22
- client.start
25
+ client.connect
23
26
 
24
27
  # Get auth level
25
28
  client.auth_level
@@ -53,6 +56,55 @@ core.get_config
53
56
  client.close
54
57
  ```
55
58
 
59
+ ## Events
60
+
61
+ Receiving events is dead simple:
62
+
63
+ ```ruby
64
+ client.register_event('TorrentAddedEvent') do |torrent_id|
65
+ puts "Torrent #{torrent_id} was added! YAY!"
66
+ end
67
+
68
+ # You can pass symbols as event name,
69
+ # they would be converted to proper camelcase event names aka "EventNameEvent"
70
+ client.register_event(:torrent_removed) do |torrent_id|
71
+ puts "Torrent #{torrent_id} was removed ;_;"
72
+ end
73
+ ```
74
+
75
+ Unfortunately there is no way to listen ALL events, due to Deluge architecture.
76
+ You have to register each event you need.
77
+
78
+ **Keep in mind event blocks would be executed in connection thread, NOT main thread!**
79
+ Avoid time-consuming code!
80
+
81
+ ### Known events
82
+
83
+ Event Name | Arguments | Description
84
+ --------------------------|-------------------------|------------------------------------------------------
85
+ ``TorrentAddedEvent`` | torrent_id | New torrent is successfully added to the session.
86
+ ``TorrentRemovedEvent`` | torrent_id | Torrent has been removed from the session.
87
+ ``PreTorrentRemovedEvent`` | torrent_id | Torrent is about to be removed from the session.
88
+ ``TorrentStateChangedEvent`` | torrent_id, state | Torrent changes state.
89
+ ``TorrentQueueChangedEvent`` |   | The queue order has changed.
90
+ ``TorrentFolderRenamedEvent`` | torrent_id, old, new | Folder within a torrent has been renamed.
91
+ ``TorrentFileRenamedEvent`` | torrent_id, index, name | File within a torrent has been renamed.
92
+ ``TorrentFinishedEvent`` | torrent_id | Torrent finishes downloading.
93
+ ``TorrentResumedEvent`` | torrent_id | Torrent resumes from a paused state.
94
+ ``TorrentFileCompletedEvent`` | torrent_id, index | File completes.
95
+ ``NewVersionAvailableEvent`` | new_release | More recent version of Deluge is available.
96
+ ``SessionStartedEvent`` |   | Session has started. This typically only happens once when the daemon is initially started.
97
+ ``SessionPausedEvent`` |   | Session has been paused.
98
+ ``SessionResumedEvent`` |   | Session has been resumed.
99
+ ``ConfigValueChangedEvent`` | key, value | Config value changes in the Core.
100
+ ``PluginEnabledEvent`` | name | Plugin is enabled in the Core.
101
+ ``PluginDisabledEvent`` | name | Plugin is disabled in the Core.
102
+
103
+ This list was extracted from Deluge 1.3.11 sources. Events for your version can be different.
104
+ There is no official documentation.
105
+
106
+ Current events could be found here: http://git.deluge-torrent.org/deluge/tree/deluge/event.py
107
+
56
108
  ## Installation
57
109
 
58
110
  Add this line to your application's Gemfile:
@@ -15,13 +15,25 @@ module Deluge
15
15
  def connect
16
16
  @connection.start
17
17
 
18
- @auth_level = @connection.call('daemon.login', @login, @password)
18
+ @auth_level = @connection.authenticate(@login, @password)
19
19
 
20
20
  register_methods!
21
21
 
22
22
  true
23
23
  end
24
24
 
25
+ def register_event(event_name, &block)
26
+ raise "Provide block for event" unless block
27
+
28
+ if event_name.is_a?(Symbol)
29
+ event_name = "#{event_name}_event"
30
+ # convert to CamelCase
31
+ event_name.gsub!(/(?:_|^)(.)/) { |match| $1.upcase }
32
+ end
33
+
34
+ @connection.register_event(event_name, &block)
35
+ end
36
+
25
37
  def close
26
38
  @connection.close
27
39
  @auth_level = nil
@@ -35,7 +47,7 @@ module Deluge
35
47
  private
36
48
 
37
49
  def register_methods!
38
- methods = @connection.call('daemon.get_method_list')
50
+ methods = @connection.method_list
39
51
 
40
52
  methods.each do |method|
41
53
  *namespaces, method_name = method.split('.')
@@ -11,8 +11,12 @@ module Deluge
11
11
  module Api
12
12
  class Connection
13
13
  class RPCError < StandardError; end
14
-
15
14
  class InvokeTimeoutError < StandardError; end
15
+ class ConnectionClosedError < StandardError; end
16
+
17
+ DAEMON_LOGIN = 'daemon.login'
18
+ DAEMON_METHOD_LIST = 'daemon.get_method_list'
19
+ DAEMON_REGISTER_EVENT = 'daemon.set_event_interest'
16
20
 
17
21
  DEFAULT_CALL_TIMEOUT = 5.0 # seconds
18
22
 
@@ -34,6 +38,7 @@ module Deluge
34
38
  @running = Concurrent::AtomicBoolean.new
35
39
 
36
40
  @messages = {}
41
+ @events = {}
37
42
 
38
43
  @write_mutex = Mutex.new
39
44
  end
@@ -49,6 +54,30 @@ module Deluge
49
54
 
50
55
  @main_thread = Thread.current
51
56
  @thread = Thread.new(&self.method(:read_loop))
57
+
58
+ # register present events
59
+ recover_events! if @events.size > 0
60
+
61
+ true
62
+ end
63
+
64
+ def authenticate(login, password)
65
+ self.call(DAEMON_LOGIN, login, password)
66
+ end
67
+
68
+ def method_list
69
+ self.call(DAEMON_METHOD_LIST)
70
+ end
71
+
72
+ def register_event(event_name, force = false, &block)
73
+ unless @events[event_name] # Register event only ONCE!
74
+ self.call(DAEMON_REGISTER_EVENT, [event_name]) if @connection # Let events be initialized lazily
75
+ end
76
+
77
+ @events[event_name] ||= []
78
+ @events[event_name] << block
79
+
80
+ true
52
81
  end
53
82
 
54
83
  def close
@@ -56,23 +85,19 @@ module Deluge
56
85
  end
57
86
 
58
87
  def call(method, *args)
88
+ raise "Not connected!" unless @connection
89
+
59
90
  kwargs = {}
60
91
  kwargs = args.pop if args.size == 1 && args.last.is_a?(Hash)
61
92
 
62
93
  future = Concurrent::IVar.new
63
94
 
64
95
  request_id = @request_id.increment
65
- message = [[request_id, method, args, kwargs]]
66
-
67
- raw = Zlib::Deflate.deflate Rencoder.dump(message)
96
+ @messages[request_id] = future
68
97
 
69
- @write_mutex.synchronize do
70
- @messages[request_id] = future
98
+ message = [[request_id, method, args, kwargs]]
71
99
 
72
- if IO.select([], [@connection], nil, nil)
73
- @connection.write(raw)
74
- end
75
- end
100
+ write_packet(message)
76
101
 
77
102
  result = future.value!(@call_timeout)
78
103
 
@@ -87,39 +112,90 @@ module Deluge
87
112
 
88
113
  def read_loop
89
114
  while(@running.true?)
90
- next unless IO.select([@connection], nil, nil, 0.1)
91
-
92
- raw = ""
93
- begin
94
- buffer = @connection.readpartial(1024)
95
- raw += buffer
96
- end until(buffer.size < 1024)
115
+ io_poll = IO.select([@connection], nil, [@connection], 0.1)
97
116
 
98
- raw = Zlib::Inflate.inflate(raw)
117
+ next unless io_poll
99
118
 
100
- parse_packets(raw).each do |packet|
101
- type, response_id, value = packet
119
+ read_sockets, _, error_sockets = io_poll
102
120
 
103
- var = @messages[response_id]
121
+ if @connection.eof?
122
+ # TODO: implement auto-recovery
123
+ raise ConnectionClosedError
124
+ end
104
125
 
105
- next unless var # TODO: Handle unknown messages
126
+ read_sockets.each do |socket|
127
+ packets = read_packets(socket)
106
128
 
107
- case type
108
- when RPC_RESPONSE
109
- var.set(value)
110
- when RPC_ERROR
111
- var.fail(RPCError.new(value))
112
- # TODO: Add events support
113
- else
114
- raise "Unknown response type #{type}"
129
+ packets.each do |packet|
130
+ dispatch_packet(packet)
115
131
  end
116
132
  end
117
133
  end
118
-
119
- @connection.close if @connection
120
- @connection = nil
121
134
  rescue => e
122
135
  @main_thread.raise(e)
136
+ ensure
137
+ @connection.close if @connection
138
+ @connection = nil
139
+ @messages.clear
140
+ end
141
+
142
+ def dispatch_packet(packet)
143
+ type, response_id, value = packet
144
+
145
+ case type
146
+ when RPC_RESPONSE, RPC_ERROR
147
+ future = @messages[response_id]
148
+
149
+ return unless future # TODO: Handle unknown messages
150
+
151
+ if type == RPC_RESPONSE
152
+ future.set(value)
153
+ else
154
+ future.fail(RPCError.new(value))
155
+ end
156
+ when RPC_EVENT
157
+ handlers = @events[response_id]
158
+ return unless handlers # TODO: Handle unknown events
159
+
160
+ handlers.each do |block|
161
+ block.call(*value)
162
+ end
163
+ else
164
+ raise "Unknown packet type #{type.inspect}"
165
+ end
166
+ end
167
+
168
+ def write_packet(packet)
169
+ raw = Zlib::Deflate.deflate Rencoder.dump(packet)
170
+
171
+ @write_mutex.synchronize do
172
+ if IO.select([], [@connection], nil, nil)
173
+ @connection.write(raw)
174
+ end
175
+ end
176
+ end
177
+
178
+ def read_packets(socket)
179
+ raw = ""
180
+ begin
181
+ buffer = socket.readpartial(1024)
182
+ raw += buffer
183
+ end until(buffer.size < 1024)
184
+
185
+ raw = Zlib::Inflate.inflate(raw)
186
+
187
+ parse_packets(raw)
188
+ end
189
+
190
+ def recover_events!
191
+ present_events = @events
192
+ @events = {}
193
+
194
+ present_events.each do |event, handlers|
195
+ handlers.each do |handler|
196
+ self.register_event(event, &handler)
197
+ end
198
+ end
123
199
  end
124
200
 
125
201
  def create_socket
@@ -1,5 +1,5 @@
1
1
  module Deluge
2
2
  module Api
3
- VERSION = "0.1.0"
3
+ VERSION = "0.1.1"
4
4
  end
5
5
  end
@@ -5,8 +5,8 @@ describe Deluge::Api::Client do
5
5
  let(:connection) do
6
6
  double('ApiConnection').tap do |connection|
7
7
  allow(connection).to receive(:start)
8
- allow(connection).to receive(:call).with('daemon.login', 'test', 'password').and_return(5)
9
- allow(connection).to receive(:call).with('daemon.get_method_list').and_return(['test.api.method'])
8
+ allow(connection).to receive(:authenticate).with('test', 'password').and_return(5)
9
+ allow(connection).to receive(:method_list).and_return(['test.api.method'])
10
10
  allow(connection).to receive(:call).with('test.api.method').and_return('winning')
11
11
  allow(connection).to receive(:close)
12
12
  end
@@ -28,17 +28,13 @@ describe Deluge::Api::Client do
28
28
  end
29
29
 
30
30
  it 'authenticate' do
31
- expect(connection).to have_received(:call).with('daemon.login', 'test', 'password')
31
+ expect(connection).to have_received(:authenticate).with('test', 'password')
32
32
  end
33
33
 
34
34
  it 'set auth_level' do
35
35
  expect(subject.auth_level).to eq(5)
36
36
  end
37
37
 
38
- it 'fetch methods' do
39
- expect(connection).to have_received(:call).with('daemon.get_method_list')
40
- end
41
-
42
38
  it 'register methods' do
43
39
  expect(subject.api_methods).to include('test.api.method')
44
40
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deluge-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Yamolov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-05-16 00:00:00.000000000 Z
11
+ date: 2015-05-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby