deluge-api 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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