trans-api 0.0.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 +15 -0
- data/.gitignore +19 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +20 -0
- data/LICENSE.txt +22 -0
- data/README.md +334 -0
- data/Rakefile +1 -0
- data/examples/basic_setup.rb +78 -0
- data/examples/torrents/debian-6.0.6-amd64-CD-10.iso.torrent +0 -0
- data/lib/trans-api/client.rb +30 -0
- data/lib/trans-api/connect.rb +331 -0
- data/lib/trans-api/file.rb +56 -0
- data/lib/trans-api/session.rb +83 -0
- data/lib/trans-api/torrent.rb +292 -0
- data/lib/trans-api/version.rb +5 -0
- data/lib/trans-api.rb +12 -0
- data/test/test_helper.rb +8 -0
- data/test/unit/torrents/debian-6.0.6-amd64-CD-1.iso.torrent +0 -0
- data/test/unit/torrents/debian-6.0.6-amd64-CD-2.iso.torrent +0 -0
- data/test/unit/torrents/debian-6.0.6-amd64-CD-3.iso.torrent +0 -0
- data/test/unit/torrents/debian-6.0.6-amd64-CD-4.iso.torrent +0 -0
- data/test/unit/torrents/debian-6.0.6-amd64-CD-5.iso.torrent +0 -0
- data/test/unit/torrents/debian-6.0.6-amd64-CD-6.iso.torrent +0 -0
- data/test/unit/torrents/debian-6.0.6-amd64-CD-7.iso.torrent +0 -0
- data/test/unit/trans_connect.rb +459 -0
- data/test/unit/trans_session_object.rb +82 -0
- data/test/unit/trans_torrent_object.rb +354 -0
- data/trans-api.gemspec +24 -0
- metadata +124 -0
@@ -0,0 +1,331 @@
|
|
1
|
+
module Trans
|
2
|
+
module Api
|
3
|
+
|
4
|
+
|
5
|
+
class Connect
|
6
|
+
|
7
|
+
require 'base64'
|
8
|
+
require 'json'
|
9
|
+
require 'nokogiri'
|
10
|
+
require 'net/http'
|
11
|
+
|
12
|
+
|
13
|
+
METHODS = {
|
14
|
+
session_get: {method: "session-get", tag: 0},
|
15
|
+
session_set: {method: "session-set", tag: 1},
|
16
|
+
session_stats: {method: "session-stats", tag: 2},
|
17
|
+
session_close: {method: "session-close", tag: 3},
|
18
|
+
torrent_get: {method: "torrent-get", tag: 4},
|
19
|
+
torrent_set: {method: "torrent-set", tag: 5},
|
20
|
+
torrent_start: {method: "torrent-start", tag: 6},
|
21
|
+
torrent_start_now: {method: "torrent-start-now", tag: 7},
|
22
|
+
torrent_stop: {method: "torrent-stop", tag: 8},
|
23
|
+
torrent_add: {method: "torrent-add", tag: 9},
|
24
|
+
torrent_remove: {method: "torrent-remove", tag: 10},
|
25
|
+
torrent_verify: {method: "torrent-verify", tag: 11},
|
26
|
+
torrent_reannounce: {method: "torrent-reannounce", tag: 12},
|
27
|
+
torrent_set_location: {method: "torrent-set-location", tag: 13},
|
28
|
+
blocklist_update: {method: "blocklist-update", tag: 14},
|
29
|
+
port_test: {method: "port-test", tag: 15},
|
30
|
+
queue_move_top: {method: "queue-move-top", tag: 16},
|
31
|
+
queue_move_up: {method: "queue-move-up", tag: 17},
|
32
|
+
queue_move_down: {method: "queue-move-down", tag: 18},
|
33
|
+
queue_move_bottom: {method: "queue-move-bottom", tag: 19}
|
34
|
+
}
|
35
|
+
|
36
|
+
|
37
|
+
|
38
|
+
def initialize(options={})
|
39
|
+
if options.empty?
|
40
|
+
@conn = Client::DEFAULT
|
41
|
+
elsif !options.nil?
|
42
|
+
@conn = options
|
43
|
+
end
|
44
|
+
self.reset_conn
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
def reset_conn
|
49
|
+
@conn[:headers] = {}
|
50
|
+
# authentication
|
51
|
+
secret = ::Base64.encode64("#{@conn[:user]}:#{@conn[:pass]}")
|
52
|
+
@conn[:headers]["Authorization"]= "Basic #{secret}" if @conn.include?(:user) && @conn.include?(:pass)
|
53
|
+
# placeholder
|
54
|
+
@conn[:headers]["X-Transmission-Session-Id"] = ""
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
# handles
|
60
|
+
|
61
|
+
#TODO: need to refactor these redundant functions!!
|
62
|
+
|
63
|
+
def session_get
|
64
|
+
data = METHODS[:session_get]
|
65
|
+
ret = self.do(:post, data)
|
66
|
+
# a little dirty, but works great :)
|
67
|
+
session = JSON.parse ret[:response].body.gsub("-","_"), {symbolize_names: true}
|
68
|
+
raise session[:result] unless valid? session, data[:tag]
|
69
|
+
session[:arguments]
|
70
|
+
end
|
71
|
+
|
72
|
+
def session_stats
|
73
|
+
data = METHODS[:session_stats]
|
74
|
+
ret = self.do(:post, data)
|
75
|
+
session = JSON.parse ret[:response].body.gsub("-","_"), {symbolize_names: true}
|
76
|
+
raise session[:result] unless valid? session, data[:tag]
|
77
|
+
session[:arguments]
|
78
|
+
end
|
79
|
+
|
80
|
+
def session_set(arguments={})
|
81
|
+
data = METHODS[:session_set]
|
82
|
+
data[:arguments] = argument_name_to_api arguments
|
83
|
+
ret = self.do :post, data
|
84
|
+
session = JSON.parse ret[:response].body, {symbolize_names: true}
|
85
|
+
raise session[:result] unless valid? session, data[:tag]
|
86
|
+
session[:arguments]
|
87
|
+
end
|
88
|
+
|
89
|
+
def session_close
|
90
|
+
data = METHODS[:session_close]
|
91
|
+
ret = self.do :post, data
|
92
|
+
session = JSON.parse ret[:response].body, {symbolize_names: true}
|
93
|
+
raise session[:result] unless valid? session, data[:tag]
|
94
|
+
session[:arguments]
|
95
|
+
end
|
96
|
+
|
97
|
+
def torrent_get(fields = [:id, :name, :status], ids = [])
|
98
|
+
arguments = { fields: fields }
|
99
|
+
arguments[:ids] = ids unless ids.empty?
|
100
|
+
data = METHODS[:torrent_get]
|
101
|
+
data[:arguments] = argument_name_to_api arguments
|
102
|
+
ret = self.do(:post, data)
|
103
|
+
torrents = JSON.parse ret[:response].body, {symbolize_names: true}
|
104
|
+
raise torrents[:result] unless valid? torrents, data[:tag]
|
105
|
+
torrents[:arguments][:torrents]
|
106
|
+
end
|
107
|
+
|
108
|
+
def torrent_set(arguments={}, ids = [])
|
109
|
+
arguments[:ids] = ids unless ids.empty?
|
110
|
+
data = METHODS[:torrent_set]
|
111
|
+
data[:arguments] = argument_name_to_api arguments
|
112
|
+
ret = self.do(:post, data)
|
113
|
+
torrents = JSON.parse ret[:response].body, {symbolize_names: true}
|
114
|
+
raise torrents[:result] unless valid? torrents, data[:tag]
|
115
|
+
torrents[:arguments][:torrents]
|
116
|
+
end
|
117
|
+
|
118
|
+
def torrent_start(ids = [])
|
119
|
+
arguments = {}
|
120
|
+
arguments[:ids] = ids unless ids.empty?
|
121
|
+
data = METHODS[:torrent_start]
|
122
|
+
data[:arguments] = argument_name_to_api arguments
|
123
|
+
ret = self.do(:post, data)
|
124
|
+
torrents = JSON.parse ret[:response].body, {symbolize_names: true}
|
125
|
+
raise torrents[:result] unless valid? torrents, data[:tag]
|
126
|
+
torrents[:arguments][:torrents]
|
127
|
+
end
|
128
|
+
|
129
|
+
def torrent_start_now(ids = [])
|
130
|
+
arguments = {}
|
131
|
+
arguments[:ids] = ids unless ids.empty?
|
132
|
+
data = METHODS[:torrent_start_now]
|
133
|
+
data[:arguments] = argument_name_to_api arguments
|
134
|
+
ret = self.do(:post, data)
|
135
|
+
torrents = JSON.parse ret[:response].body, {symbolize_names: true}
|
136
|
+
raise torrents[:result] unless valid? torrents, data[:tag]
|
137
|
+
torrents[:arguments][:torrents]
|
138
|
+
end
|
139
|
+
|
140
|
+
def torrent_stop(ids = [])
|
141
|
+
arguments = {}
|
142
|
+
arguments[:ids] = ids unless ids.empty?
|
143
|
+
data = METHODS[:torrent_stop]
|
144
|
+
data[:arguments] = argument_name_to_api arguments
|
145
|
+
ret = self.do(:post, data)
|
146
|
+
torrents = JSON.parse ret[:response].body, {symbolize_names: true}
|
147
|
+
raise torrents[:result] unless valid? torrents, data[:tag]
|
148
|
+
torrents[:arguments][:torrents]
|
149
|
+
end
|
150
|
+
|
151
|
+
def torrent_add(arguments={})
|
152
|
+
data = METHODS[:torrent_add]
|
153
|
+
data[:arguments] = argument_name_to_api arguments
|
154
|
+
ret = self.do(:post, data)
|
155
|
+
torrents = JSON.parse ret[:response].body.gsub("-","_"), {symbolize_names: true}
|
156
|
+
raise torrents[:result] unless valid? torrents, data[:tag]
|
157
|
+
torrents[:arguments][:torrent_added]
|
158
|
+
end
|
159
|
+
|
160
|
+
def torrent_remove(arguments={}, ids=[])
|
161
|
+
arguments[:ids] = ids unless ids.empty?
|
162
|
+
data = METHODS[:torrent_remove]
|
163
|
+
data[:arguments] = argument_name_to_api arguments
|
164
|
+
ret = self.do(:post, data)
|
165
|
+
torrents = JSON.parse ret[:response].body.gsub("-","_"), {symbolize_names: true}
|
166
|
+
raise torrents[:result] unless valid? torrents, data[:tag]
|
167
|
+
torrents[:arguments]
|
168
|
+
end
|
169
|
+
|
170
|
+
def torrent_verify(ids = [])
|
171
|
+
arguments = {}
|
172
|
+
arguments[:ids] = ids unless ids.empty?
|
173
|
+
data = METHODS[:torrent_verify]
|
174
|
+
data[:arguments] = argument_name_to_api arguments
|
175
|
+
ret = self.do(:post, data)
|
176
|
+
torrents = JSON.parse ret[:response].body, {symbolize_names: true}
|
177
|
+
raise torrents[:result] unless valid? torrents, data[:tag]
|
178
|
+
torrents[:arguments]
|
179
|
+
end
|
180
|
+
|
181
|
+
def torrent_reannounce(ids=[])
|
182
|
+
arguments = {}
|
183
|
+
arguments[:ids] = ids unless ids.empty?
|
184
|
+
data = METHODS[:torrent_reannounce]
|
185
|
+
data[:arguments] = argument_name_to_api arguments
|
186
|
+
ret = self.do(:post, data)
|
187
|
+
torrents = JSON.parse ret[:response].body, {symbolize_names: true}
|
188
|
+
raise torrents[:result] unless valid? torrents, data[:tag]
|
189
|
+
torrents[:arguments]
|
190
|
+
end
|
191
|
+
|
192
|
+
def torrent_set_location(arguments={}, ids=[])
|
193
|
+
arguments[:ids] = ids unless ids.empty?
|
194
|
+
data = METHODS[:torrent_set_location]
|
195
|
+
data[:arguments] = argument_name_to_api arguments
|
196
|
+
ret = self.do(:post, data)
|
197
|
+
torrents = JSON.parse ret[:response].body.gsub("-","_"), {symbolize_names: true}
|
198
|
+
raise torrents[:result] unless valid? torrents, data[:tag]
|
199
|
+
torrents[:arguments]
|
200
|
+
end
|
201
|
+
|
202
|
+
def blocklist_update
|
203
|
+
data = METHODS[:blocklist_update]
|
204
|
+
ret = self.do(:post, data)
|
205
|
+
torrents = JSON.parse ret[:response].body.gsub("-","_"), {symbolize_names: true}
|
206
|
+
raise torrents[:result] unless valid? torrents, data[:tag]
|
207
|
+
torrents[:arguments]
|
208
|
+
end
|
209
|
+
|
210
|
+
def port_test
|
211
|
+
data = METHODS[:port_test]
|
212
|
+
ret = self.do(:post, data)
|
213
|
+
torrents = JSON.parse ret[:response].body.gsub("-","_"), {symbolize_names: true}
|
214
|
+
raise torrents[:result] unless valid? torrents, data[:tag]
|
215
|
+
torrents[:arguments]
|
216
|
+
end
|
217
|
+
|
218
|
+
|
219
|
+
def queue_move_top(ids=[])
|
220
|
+
arguments = {}
|
221
|
+
arguments[:ids] = ids unless ids.empty?
|
222
|
+
data = METHODS[:queue_move_top]
|
223
|
+
data[:arguments] = argument_name_to_api arguments
|
224
|
+
ret = self.do(:post, data)
|
225
|
+
torrents = JSON.parse ret[:response].body.gsub("-","_"), {symbolize_names: true}
|
226
|
+
raise torrents[:result] unless valid? torrents, data[:tag]
|
227
|
+
torrents[:arguments]
|
228
|
+
end
|
229
|
+
|
230
|
+
def queue_move_up(ids=[])
|
231
|
+
arguments = {}
|
232
|
+
arguments[:ids] = ids unless ids.empty?
|
233
|
+
data = METHODS[:queue_move_up]
|
234
|
+
data[:arguments] = argument_name_to_api arguments
|
235
|
+
ret = self.do(:post, data)
|
236
|
+
torrents = JSON.parse ret[:response].body.gsub("-","_"), {symbolize_names: true}
|
237
|
+
raise torrents[:result] unless valid? torrents, data[:tag]
|
238
|
+
torrents[:arguments]
|
239
|
+
end
|
240
|
+
|
241
|
+
def queue_move_down(ids=[])
|
242
|
+
arguments = {}
|
243
|
+
arguments[:ids] = ids unless ids.empty?
|
244
|
+
data = METHODS[:queue_move_down]
|
245
|
+
data[:arguments] = argument_name_to_api arguments
|
246
|
+
ret = self.do(:post, data)
|
247
|
+
torrents = JSON.parse ret[:response].body.gsub("-","_"), {symbolize_names: true}
|
248
|
+
raise torrents[:result] unless valid? torrents, data[:tag]
|
249
|
+
torrents[:arguments]
|
250
|
+
end
|
251
|
+
|
252
|
+
def queue_move_bottom(ids=[])
|
253
|
+
arguments = {}
|
254
|
+
arguments[:ids] = ids unless ids.empty?
|
255
|
+
data = METHODS[:queue_move_bottom]
|
256
|
+
data[:arguments] = argument_name_to_api arguments
|
257
|
+
ret = self.do(:post, data)
|
258
|
+
torrents = JSON.parse ret[:response].body.gsub("-","_"), {symbolize_names: true}
|
259
|
+
raise torrents[:result] unless valid? torrents, data[:tag]
|
260
|
+
torrents[:arguments]
|
261
|
+
end
|
262
|
+
|
263
|
+
|
264
|
+
|
265
|
+
|
266
|
+
# request
|
267
|
+
|
268
|
+
def do(method = :get, data = nil)
|
269
|
+
headers = @conn[:headers]
|
270
|
+
|
271
|
+
uri = URI.parse "http://localhost/"
|
272
|
+
uri.scheme = @conn[:scheme]
|
273
|
+
uri.host = @conn[:host]
|
274
|
+
uri.port = @conn[:port]
|
275
|
+
uri.path = @conn[:path]
|
276
|
+
|
277
|
+
# request
|
278
|
+
http = Net::HTTP.new uri.host, uri.port
|
279
|
+
resp = http.get(uri.request_uri, data.to_json, headers) if method == :get
|
280
|
+
resp = http.post(uri.request_uri, data.to_json, headers) if method == :post
|
281
|
+
raise "not implemented #{method} request!!" if method != :get && method != :post
|
282
|
+
|
283
|
+
# authorize via session id
|
284
|
+
if resp.code.to_i == 409
|
285
|
+
tmp = Nokogiri::HTML resp.body
|
286
|
+
session_id = tmp.search('p code', '//X-Transmission-Session-Id').first.text.gsub("X-Transmission-Session-Id: ",'')
|
287
|
+
@conn[:headers]["X-Transmission-Session-Id"] = session_id
|
288
|
+
# recursion!!!!
|
289
|
+
return self.do(:post, data)
|
290
|
+
end
|
291
|
+
|
292
|
+
ret = {request: http, response: resp}
|
293
|
+
handle_request_error(ret, data)
|
294
|
+
ret
|
295
|
+
end
|
296
|
+
|
297
|
+
|
298
|
+
private
|
299
|
+
|
300
|
+
def valid?(body, tag)
|
301
|
+
body[:result] == "success" && body[:tag] == tag
|
302
|
+
end
|
303
|
+
|
304
|
+
def argument_name_to_api(options = {})
|
305
|
+
#TODO: fix nested arguments
|
306
|
+
ret = {}
|
307
|
+
options.each do |k,v|
|
308
|
+
ret[k.to_s.gsub('_','-')] = v
|
309
|
+
end
|
310
|
+
ret
|
311
|
+
end
|
312
|
+
|
313
|
+
# error handling
|
314
|
+
|
315
|
+
def handle_request_error(ret, data)
|
316
|
+
raise "error handling: #{data[:method]}, #{request_str ret[:response]}" if ret[:response].code.to_i != 200
|
317
|
+
end
|
318
|
+
|
319
|
+
|
320
|
+
def request_str(response)
|
321
|
+
ret = []
|
322
|
+
ret << "[code: #{response.code}]"
|
323
|
+
ret << "[body: #{response.body}]"
|
324
|
+
ret.join "\n"
|
325
|
+
end
|
326
|
+
|
327
|
+
|
328
|
+
end
|
329
|
+
|
330
|
+
end
|
331
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module Trans
|
4
|
+
module Api
|
5
|
+
|
6
|
+
class File
|
7
|
+
|
8
|
+
def initialize(options={})
|
9
|
+
@torrent = options.delete :torrent
|
10
|
+
@torrent_fields = options.delete :fields
|
11
|
+
@fields = options[:file]
|
12
|
+
|
13
|
+
# set default stats
|
14
|
+
@torrent_fields[:files_unwanted] ||= []
|
15
|
+
@torrent_fields[:files_wanted] ||= []
|
16
|
+
# if @fields[:fileStat][:wanted] == false
|
17
|
+
# @torrent_fields[:files_unwanted] << self.id
|
18
|
+
# else
|
19
|
+
# @torrent_fields[:files_wanted] << self.id
|
20
|
+
# end
|
21
|
+
|
22
|
+
@client = Client.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def id
|
26
|
+
@fields[:id]
|
27
|
+
end
|
28
|
+
|
29
|
+
def name
|
30
|
+
@fields[:name]
|
31
|
+
end
|
32
|
+
|
33
|
+
def stat
|
34
|
+
@fields[:fileStat]
|
35
|
+
end
|
36
|
+
|
37
|
+
def unwant
|
38
|
+
@torrent_fields[:files_wanted].delete self.id if @torrent_fields[:files_wanted].include? self.id
|
39
|
+
@torrent_fields[:files_unwanted] << self.id unless @torrent_fields[:files_unwanted].include? self.id
|
40
|
+
@fields[:fileStat][:wanted] = false
|
41
|
+
end
|
42
|
+
|
43
|
+
def want
|
44
|
+
@torrent_fields[:files_wanted] << self.id unless @torrent_fields[:files_wanted].include? self.id
|
45
|
+
@torrent_fields[:files_unwanted].delete self.id if @torrent_fields[:files_unwanted].include? self.id
|
46
|
+
@fields[:fileStat][:wanted] = true
|
47
|
+
end
|
48
|
+
|
49
|
+
def wanted?
|
50
|
+
@fields[:fileStat][:wanted]
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
|
4
|
+
module Trans
|
5
|
+
module Api
|
6
|
+
|
7
|
+
#
|
8
|
+
# Session class
|
9
|
+
#
|
10
|
+
|
11
|
+
class Session
|
12
|
+
|
13
|
+
include Singleton
|
14
|
+
|
15
|
+
ENCRYPTION = [ :required, :preferred, :tolerated ]
|
16
|
+
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@client = Client.new
|
20
|
+
self.reset!
|
21
|
+
|
22
|
+
# map fields to accessors
|
23
|
+
@fields.each do |k, v|
|
24
|
+
# setter
|
25
|
+
self.metaclass.send(:define_method, "#{k}=") do |value|
|
26
|
+
if v.class == value.class
|
27
|
+
@fields[k] = value
|
28
|
+
else
|
29
|
+
msg = "invalid type: #{value.class}, expected: #{v.class}"
|
30
|
+
@last_error[:message] = msg
|
31
|
+
raise msg
|
32
|
+
end
|
33
|
+
end
|
34
|
+
# getter
|
35
|
+
self.metaclass.send(:define_method, "#{k}") do
|
36
|
+
@fields[k]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
def fields
|
43
|
+
@fields.map{|k,v| k}
|
44
|
+
end
|
45
|
+
|
46
|
+
def fields_and_values
|
47
|
+
@fields
|
48
|
+
end
|
49
|
+
|
50
|
+
def errors
|
51
|
+
@last_error
|
52
|
+
end
|
53
|
+
|
54
|
+
def stats!
|
55
|
+
@client.connect.session_stats
|
56
|
+
end
|
57
|
+
|
58
|
+
def save!
|
59
|
+
# reject unchanged fields
|
60
|
+
changed = @fields.reject{|k,v| @old_fields[k] == v }
|
61
|
+
if changed.size > 0
|
62
|
+
# call api, and store changed fields
|
63
|
+
@client.connect.session_set changed
|
64
|
+
# refresh
|
65
|
+
self.reset!
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# reload current object
|
70
|
+
def reset!
|
71
|
+
@fields = @client.connect.session_get
|
72
|
+
@old_fields = @fields.clone
|
73
|
+
@last_error = {error: "", message: ""}
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
def metaclass
|
78
|
+
class << self; self; end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|