deluge-api 0.1.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7e9faac059b0177c915d0ca94ef0de556498441e
4
+ data.tar.gz: 59d61e6ae269205d9c7db3349c8749d4ecaab409
5
+ SHA512:
6
+ metadata.gz: a3f922e08b48c0ebe7fdef0b0c0420c0546ad315382e0cf6f979f315058003610bb643ed56f1a4423e71fd8789ee77786e30f0bf0e45e61fb1dd66a2840b81ba
7
+ data.tar.gz: 1b18c1573281c8ac0e4052cb989d273ac190676fc7d3fab0114391d73b01842b6c82772246aa8c7c8caf8bd54b1287b29cb3149273dd927538dc6da5a0205de0
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ *.gem
@@ -0,0 +1 @@
1
+ 2.1.4
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in deluge-api.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Igor Yamolov
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,70 @@
1
+ # Deluge::Api
2
+
3
+ Ruby RPC client library for Deluge torrent client.
4
+ Provides dynamic API bindings depending on RPC server.
5
+ Multi-threading friendly, thanks to ``concurrent-ruby`` gem.
6
+
7
+ Official RPC protocol documentation:
8
+ http://deluge.readthedocs.org/en/develop/core/rpc.html
9
+
10
+ Deluge RPC API reference:
11
+ http://deluge.readthedocs.org/en/develop/core/rpc.html#remote-api
12
+
13
+ ## Usage
14
+
15
+ ```ruby
16
+ require 'deluge'
17
+
18
+ # Initialize client
19
+ client = Deluge::Api::Client.new(host: 'localhost', port: 58846, login: 'username', password: 'password')
20
+
21
+ # Start connection and authenticate
22
+ client.start
23
+
24
+ # Get auth level
25
+ client.auth_level
26
+ # => 5
27
+
28
+ # Get available methods
29
+ client.api_methods
30
+ # => ['daemon.add_torrent_file', 'core.shutdown', ...]
31
+
32
+ # Get deluge version
33
+ client.daemon.info
34
+ # => "1.3.10"
35
+
36
+ # Get torrents list
37
+ client.core.get_torrents_status({}, ['name', 'hash'])
38
+ # => [{name: 'Hot Chicks Action', hash: '<torrent_hash>'}, ...]
39
+
40
+ # Get namespace
41
+ core = client.core
42
+ # => <Deluge::Api::Namespace name="core">
43
+
44
+ # Get namespace methods
45
+ core.api_methods
46
+ # => ['core.get_session_status', 'core.get_upload_rate', ....]
47
+
48
+ # Invoke namespace method
49
+ core.get_config
50
+ # => {"info_sent"=>0.0, "lsd"=>true, "send_info"=>false, ... }
51
+
52
+ # Close connection
53
+ client.close
54
+ ```
55
+
56
+ ## Installation
57
+
58
+ Add this line to your application's Gemfile:
59
+
60
+ ```ruby
61
+ gem 'deluge-api'
62
+ ```
63
+
64
+ And then execute:
65
+
66
+ $ bundle
67
+
68
+ Or install it yourself as:
69
+
70
+ $ gem install deluge-api
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'deluge/api/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "deluge-api"
8
+ spec.version = Deluge::Api::VERSION
9
+ spec.authors = ["Igor Yamolov"]
10
+ spec.email = ["clouster@yandex.ru"]
11
+ spec.summary = %q{Deluge RPC protocol wrapper}
12
+ spec.description = %q{Communicate with Deluge torrent client via RPC protocol}
13
+ spec.homepage = "https://github.com/t3hk0d3/deluge-api"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency 'concurrent-ruby', '~> 0.7'
22
+ spec.add_dependency 'rencoder', '~> 0.1'
23
+ spec.add_development_dependency 'bundler', '~> 1.7'
24
+ spec.add_development_dependency 'rspec', '~> 3.1'
25
+ end
@@ -0,0 +1 @@
1
+ require_relative 'deluge/api'
@@ -0,0 +1,10 @@
1
+ require 'deluge/api/version'
2
+
3
+ module Deluge
4
+ module Api
5
+ require_relative 'api/namespace'
6
+ require_relative 'api/connection'
7
+ require_relative 'api/client'
8
+
9
+ end
10
+ end
@@ -0,0 +1,77 @@
1
+ module Deluge
2
+ module Api
3
+ class Client
4
+ attr_reader :namespaces, :api_methods, :auth_level
5
+
6
+ def initialize(options = {})
7
+ @connection = Deluge::Api::Connection.new(options)
8
+ @login = options.fetch(:login)
9
+ @password = options.fetch(:password)
10
+
11
+ @namespaces = {}
12
+ @api_methods = []
13
+ end
14
+
15
+ def connect
16
+ @connection.start
17
+
18
+ @auth_level = @connection.call('daemon.login', @login, @password)
19
+
20
+ register_methods!
21
+
22
+ true
23
+ end
24
+
25
+ def close
26
+ @connection.close
27
+ @auth_level = nil
28
+ @api_methods = []
29
+ @namespaces.each_key do |ns|
30
+ self.singleton_class.send :undef_method, ns
31
+ end
32
+ @namespaces = {}
33
+ end
34
+
35
+ private
36
+
37
+ def register_methods!
38
+ methods = @connection.call('daemon.get_method_list')
39
+
40
+ methods.each do |method|
41
+ *namespaces, method_name = method.split('.')
42
+
43
+ register_method!(namespaces, method_name)
44
+ @api_methods << method
45
+ end
46
+ end
47
+
48
+ def register_method!(namespaces, method)
49
+ namespace = register_namespace(namespaces)
50
+
51
+ namespace.register_method(method)
52
+ end
53
+
54
+ def register_namespace(namespaces)
55
+ ns = namespaces.shift
56
+
57
+ root = @namespaces[ns]
58
+
59
+ unless root
60
+ root = Api::Namespace.new(ns, @connection)
61
+ @namespaces[ns] = root
62
+
63
+ define_singleton_method(ns.to_sym) do
64
+ @namespaces[ns]
65
+ end
66
+ end
67
+
68
+ namespaces.each do |namespace|
69
+ root = root.register_namespace(namespace)
70
+ end
71
+
72
+ root
73
+ end
74
+
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,158 @@
1
+ require 'rencoder'
2
+ require 'socket'
3
+ require 'openssl'
4
+ require 'thread'
5
+ require 'zlib'
6
+ require 'stringio'
7
+
8
+ require 'concurrent'
9
+
10
+ module Deluge
11
+ module Api
12
+ class Connection
13
+ class RPCError < StandardError; end
14
+
15
+ class InvokeTimeoutError < StandardError; end
16
+
17
+ DEFAULT_CALL_TIMEOUT = 5.0 # seconds
18
+
19
+ DEFAULT_PORT = 58846
20
+
21
+ RPC_RESPONSE = 1
22
+ RPC_ERROR = 2
23
+ RPC_EVENT = 3
24
+
25
+ attr_reader :host, :port
26
+
27
+ def initialize(options = {})
28
+ @host = options.delete(:host) || 'localhost'
29
+ @port = (options.delete(:port) || DEFAULT_PORT).to_i
30
+
31
+ @call_timeout = options.delete(:call_timeout) || DEFAULT_CALL_TIMEOUT
32
+
33
+ @request_id = Concurrent::AtomicFixnum.new
34
+ @running = Concurrent::AtomicBoolean.new
35
+
36
+ @messages = {}
37
+
38
+ @write_mutex = Mutex.new
39
+ end
40
+
41
+ def start
42
+ raise 'Connection already opened' if @connection
43
+
44
+ @connection = OpenSSL::SSL::SSLSocket.new(create_socket, ssl_context)
45
+
46
+ @connection.connect
47
+
48
+ @running.make_true
49
+
50
+ @main_thread = Thread.current
51
+ @thread = Thread.new(&self.method(:read_loop))
52
+ end
53
+
54
+ def close
55
+ @running.make_false
56
+ end
57
+
58
+ def call(method, *args)
59
+ kwargs = {}
60
+ kwargs = args.pop if args.size == 1 && args.last.is_a?(Hash)
61
+
62
+ future = Concurrent::IVar.new
63
+
64
+ request_id = @request_id.increment
65
+ message = [[request_id, method, args, kwargs]]
66
+
67
+ raw = Zlib::Deflate.deflate Rencoder.dump(message)
68
+
69
+ @write_mutex.synchronize do
70
+ @messages[request_id] = future
71
+
72
+ if IO.select([], [@connection], nil, nil)
73
+ @connection.write(raw)
74
+ end
75
+ end
76
+
77
+ result = future.value!(@call_timeout)
78
+
79
+ if result.nil? && future.pending?
80
+ raise InvokeTimeoutError.new("Failed to retreive response for '#{method}' in #{@call_timeout} seconds. Probably method not exists.")
81
+ end
82
+
83
+ result
84
+ end
85
+
86
+ private
87
+
88
+ def read_loop
89
+ 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)
97
+
98
+ raw = Zlib::Inflate.inflate(raw)
99
+
100
+ parse_packets(raw).each do |packet|
101
+ type, response_id, value = packet
102
+
103
+ var = @messages[response_id]
104
+
105
+ next unless var # TODO: Handle unknown messages
106
+
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}"
115
+ end
116
+ end
117
+ end
118
+
119
+ @connection.close if @connection
120
+ @connection = nil
121
+ rescue => e
122
+ @main_thread.raise(e)
123
+ end
124
+
125
+ def create_socket
126
+ socket = TCPSocket.new(host, port)
127
+
128
+ if ::Socket.constants.include?('TCP_NODELAY') || ::Socket.constants.include?(:TCP_NODELAY)
129
+ socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, true)
130
+ end
131
+ socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true)
132
+
133
+ socket
134
+ end
135
+
136
+ def ssl_context
137
+ # SSLv3 is not allowed (http://dev.deluge-torrent.org/ticket/2555)
138
+ context = OpenSSL::SSL::SSLContext.new('SSLv23')
139
+ # TODO: Consider allowing server certificate validation
140
+ context.set_params(verify_mode: OpenSSL::SSL::VERIFY_NONE)
141
+
142
+ context
143
+ end
144
+
145
+ def parse_packets(raw)
146
+ io = StringIO.new(raw)
147
+
148
+ packets = []
149
+
150
+ until(io.eof?)
151
+ packets << Rencoder.load(io)
152
+ end
153
+
154
+ packets
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,45 @@
1
+ module Deluge
2
+ module Api
3
+ class Namespace
4
+ attr_reader :name, :connection, :namespaces, :api_methods
5
+
6
+ def initialize(name, connection)
7
+ @name, @connection = name, connection
8
+ @namespaces = {}
9
+ @api_methods = []
10
+ end
11
+
12
+ def register_namespace(namespace)
13
+ namespace = namespace.to_sym
14
+
15
+ return namespaces[namespace] if namespaces.include?(namespace)
16
+
17
+ ns = Namespace.new("#{self.name}.#{namespace}", connection)
18
+
19
+ namespaces[namespace] = ns
20
+
21
+ define_singleton_method(namespace) do
22
+ ns
23
+ end
24
+
25
+ ns
26
+ end
27
+
28
+ def register_method(method)
29
+ method = method.to_sym
30
+
31
+ api_methods << "#{name}.#{method}"
32
+
33
+ define_singleton_method(method) do |*args|
34
+ call(method, *args)
35
+ end
36
+ end
37
+
38
+ def call(method, *args)
39
+ method_name = "#{name}.#{method}"
40
+
41
+ @connection.call(method_name, *args)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,5 @@
1
+ module Deluge
2
+ module Api
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,78 @@
1
+ require 'spec_helper'
2
+
3
+ describe Deluge::Api::Client do
4
+
5
+ let(:connection) do
6
+ double('ApiConnection').tap do |connection|
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'])
10
+ allow(connection).to receive(:call).with('test.api.method').and_return('winning')
11
+ allow(connection).to receive(:close)
12
+ end
13
+ end
14
+
15
+ before do
16
+ allow(Deluge::Api::Connection).to receive(:new).with(kind_of(Hash)).and_return(connection)
17
+ end
18
+
19
+ subject { described_class.new(host: 'localhost', login: 'test', password: 'password') }
20
+
21
+ describe '#connect' do
22
+ before do
23
+ subject.connect
24
+ end
25
+
26
+ it 'starts connection' do
27
+ expect(connection).to have_received(:start)
28
+ end
29
+
30
+ it 'authenticate' do
31
+ expect(connection).to have_received(:call).with('daemon.login', 'test', 'password')
32
+ end
33
+
34
+ it 'set auth_level' do
35
+ expect(subject.auth_level).to eq(5)
36
+ end
37
+
38
+ it 'fetch methods' do
39
+ expect(connection).to have_received(:call).with('daemon.get_method_list')
40
+ end
41
+
42
+ it 'register methods' do
43
+ expect(subject.api_methods).to include('test.api.method')
44
+ end
45
+
46
+ it 'create namespace access methods' do
47
+ expect(subject.test).to be_a(Deluge::Api::Namespace).and have_attributes(name: 'test')
48
+ end
49
+
50
+ it 'create api access methods' do
51
+ expect(subject.test.api.method).to eq('winning')
52
+ end
53
+ end
54
+
55
+ describe '#close' do
56
+ before do
57
+ subject.connect
58
+
59
+ subject.close
60
+ end
61
+
62
+ it 'closes connection' do
63
+ expect(connection).to have_received(:close)
64
+ end
65
+
66
+ it 'clear namespaces' do
67
+ expect(subject.namespaces).to be_empty
68
+ end
69
+
70
+ it 'clear methods' do
71
+ expect(subject.api_methods).to be_empty
72
+ end
73
+
74
+ it 'remove namespace methods' do
75
+ expect(subject).not_to respond_to(:test)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+
3
+ describe Deluge::Api::Namespace do
4
+
5
+ let(:connection) do
6
+ double('ApiConnection').tap do |connection|
7
+ allow(connection).to receive(:call)
8
+ end
9
+ end
10
+
11
+ let(:instance) { described_class.new('root', connection) }
12
+
13
+ describe '#register_namespace' do
14
+ let(:result) { instance.register_namespace('test') }
15
+
16
+ it 'register new namespace' do
17
+ expect(result).to eq(instance.namespaces[:test])
18
+ end
19
+
20
+ it 'returns registered namespace' do
21
+ expect(result).to be_a(described_class).and have_attributes(name: 'root.test')
22
+ end
23
+
24
+ it 'returns existing namespace if its already registered' do
25
+ expect(instance.register_namespace('test')).to eql(result)
26
+ end
27
+
28
+ it 'creates namespace access instance method' do
29
+ expect(result).to eq(instance.test)
30
+ end
31
+ end
32
+
33
+ describe '#register_method' do
34
+ before do
35
+ instance.register_method('test')
36
+ end
37
+
38
+ it 'register new api method' do
39
+ expect(instance.api_methods).to include('root.test')
40
+ end
41
+
42
+ it 'create instance method' do
43
+ expect(instance).to respond_to(:test)
44
+ end
45
+ end
46
+
47
+ describe 'api access instance method' do
48
+ before do
49
+ instance.register_method('test')
50
+
51
+ instance.test('hello', 'world')
52
+ end
53
+
54
+ it 'invoke api call' do
55
+ expect(connection).to have_received(:call).with('root.test', 'hello', 'world')
56
+ end
57
+ end
58
+
59
+ end
@@ -0,0 +1,5 @@
1
+ require 'rspec'
2
+
3
+ $:.unshift File.expand_path('../../lib', __FILE__)
4
+
5
+ require 'deluge/api'
metadata ADDED
@@ -0,0 +1,120 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: deluge-api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Igor Yamolov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-05-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: concurrent-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.7'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rencoder
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.7'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.7'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.1'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.1'
69
+ description: Communicate with Deluge torrent client via RPC protocol
70
+ email:
71
+ - clouster@yandex.ru
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".ruby-version"
78
+ - Gemfile
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - deluge-api.gemspec
83
+ - lib/deluge.rb
84
+ - lib/deluge/api.rb
85
+ - lib/deluge/api/client.rb
86
+ - lib/deluge/api/connection.rb
87
+ - lib/deluge/api/namespace.rb
88
+ - lib/deluge/api/version.rb
89
+ - spec/deluge/api/client_spec.rb
90
+ - spec/deluge/api/namespace_spec.rb
91
+ - spec/spec_helper.rb
92
+ homepage: https://github.com/t3hk0d3/deluge-api
93
+ licenses:
94
+ - MIT
95
+ metadata: {}
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 2.2.2
113
+ signing_key:
114
+ specification_version: 4
115
+ summary: Deluge RPC protocol wrapper
116
+ test_files:
117
+ - spec/deluge/api/client_spec.rb
118
+ - spec/deluge/api/namespace_spec.rb
119
+ - spec/spec_helper.rb
120
+ has_rdoc: