rarbg 0.1.4 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.editorconfig +12 -0
- data/.gitignore +38 -0
- data/.rspec +4 -0
- data/.travis.yml +22 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +61 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +116 -0
- data/Rakefile +8 -0
- data/bin/console +8 -0
- data/bin/setup +7 -0
- data/lib/rarbg.rb +3 -122
- data/lib/rarbg/api.rb +219 -0
- data/lib/rarbg/version.rb +6 -0
- data/rarbg.gemspec +44 -0
- data/spec/rarbg/list_spec.rb +95 -0
- data/spec/rarbg/rarbg_spec.rb +19 -0
- data/spec/rarbg/search_spec.rb +107 -0
- data/spec/rarbg/token_spec.rb +54 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/stubs.rb +42 -0
- metadata +138 -7
data/lib/rarbg/api.rb
ADDED
@@ -0,0 +1,219 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'faraday'
|
4
|
+
require 'faraday_middleware'
|
5
|
+
|
6
|
+
# Main namespace for RARBG
|
7
|
+
module RARBG
|
8
|
+
# Default error class for the module.
|
9
|
+
class APIError < StandardError; end
|
10
|
+
|
11
|
+
# Base class for RARBG API.
|
12
|
+
class API
|
13
|
+
# RARBG API endpoint.
|
14
|
+
API_ENDPOINT = 'https://torrentapi.org/pubapi_v2.php'
|
15
|
+
|
16
|
+
# App name identifier.
|
17
|
+
APP_ID = 'rarbg-rubygem'
|
18
|
+
|
19
|
+
# Default token expiration time.
|
20
|
+
TOKEN_EXPIRATION = 800
|
21
|
+
|
22
|
+
# @return [Faraday::Connection] the Faraday connection object.
|
23
|
+
attr_reader :conn
|
24
|
+
|
25
|
+
# @return [String] the token used for authentication.
|
26
|
+
attr_reader :token
|
27
|
+
|
28
|
+
# @return [Integer] the monotonic timestamp of the token request.
|
29
|
+
attr_reader :token_time
|
30
|
+
|
31
|
+
# @return [Integer] the monotonic timestamp of the last request performed.
|
32
|
+
attr_reader :last_request
|
33
|
+
|
34
|
+
# Initialize a new istance of `RARBG::API`.
|
35
|
+
#
|
36
|
+
# @example
|
37
|
+
# rarbg = RARBG::API.new
|
38
|
+
def initialize
|
39
|
+
@conn = Faraday.new(url: API_ENDPOINT) do |conn|
|
40
|
+
conn.request :json
|
41
|
+
conn.response :json
|
42
|
+
conn.adapter Faraday.default_adapter
|
43
|
+
|
44
|
+
conn.options.timeout = 90
|
45
|
+
conn.options.open_timeout = 10
|
46
|
+
|
47
|
+
conn.params[:app_id] = APP_ID
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# List torrents.
|
52
|
+
#
|
53
|
+
# @param params [Hash] A customizable set of parameters.
|
54
|
+
#
|
55
|
+
# @option params [Array<Integer>] :category Filter results by category.
|
56
|
+
# @option params [Symbol] :format Format results.
|
57
|
+
# Valid values: `:json`, `:json_extended`. Default: `:json`.
|
58
|
+
# @option params [Integer] :limit Limit results number.
|
59
|
+
# Valid values: `25`, `50`, `100`. Default: `25`.
|
60
|
+
# @option params [Integer] :min_seeders Filter results by minimum seeders.
|
61
|
+
# @option params [Integer] :min_leechers Filter results by minimum leechers.
|
62
|
+
# @option params [Boolean] :ranked Include/exclude unranked results.
|
63
|
+
# @option params [Symbol] :sort Sort results.
|
64
|
+
# Valid values: `:last`, `:seeders`, `:leechers`. Default: `:last`.
|
65
|
+
#
|
66
|
+
# @return [Array<Hash>] Return torrents that match the specified parameters.
|
67
|
+
#
|
68
|
+
# @raise [ArgumentError] Exception raised if `params` is not an `Hash`.
|
69
|
+
#
|
70
|
+
# @raise [RARBG::APIError] Exception raised when request fails or endpoint
|
71
|
+
# responds with an error.
|
72
|
+
#
|
73
|
+
# @example List last 100 ranked torrents in `Movies/x264/1080`
|
74
|
+
# rarbg = RARBG::API.new
|
75
|
+
# rarbg.list(limit: 100, ranked: true, category: [44])
|
76
|
+
#
|
77
|
+
# @example List all torrent with minimum 50 seeders
|
78
|
+
# rarbg = RARBG::API.new
|
79
|
+
# rarbg.list(min_seeders: 50)
|
80
|
+
def list(params = {})
|
81
|
+
raise ArgumentError, 'Expected params hash' unless params.is_a?(Hash)
|
82
|
+
|
83
|
+
params.update(
|
84
|
+
mode: 'list',
|
85
|
+
token: token?
|
86
|
+
)
|
87
|
+
call(params)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Search torrents.
|
91
|
+
#
|
92
|
+
# @param params [Hash] A customizable set of parameters.
|
93
|
+
#
|
94
|
+
# @option params [String] :string Search results by string.
|
95
|
+
# @option params [String] :imdb Search results by IMDb id.
|
96
|
+
# @option params [String] :tvdb Search results by TVDB id.
|
97
|
+
# @option params [String] :themoviedb Search results by The Movie DB id.
|
98
|
+
# @option params [Array<Integer>] :category Filter results by category.
|
99
|
+
# @option params [Symbol] :format Format results.
|
100
|
+
# Valid values: `:json`, `:json_extended`. Default: `:json`
|
101
|
+
# @option params [Integer] :limit Limit results number.
|
102
|
+
# Valid values: `25`, `50`, `100`. Default: `25`.
|
103
|
+
# @option params [Integer] :min_seeders Filter results by minimum seeders.
|
104
|
+
# @option params [Integer] :min_leechers Filter results by minimum leechers.
|
105
|
+
# @option params [Boolean] :ranked Include/exclude unranked results.
|
106
|
+
# @option params [Symbol] :sort Sort results.
|
107
|
+
# Valid values: `:last`, `:seeders`, `:leechers`. Default: `:last`.
|
108
|
+
#
|
109
|
+
# @return [Array<Hash>] Return torrents that match the specified parameters.
|
110
|
+
#
|
111
|
+
# @raise [ArgumentError] Exception raised if `params` is not an `Hash`.
|
112
|
+
#
|
113
|
+
# @raise [ArgumentError] Exception raised if no search type param is passed
|
114
|
+
# (among `string`, `imdb`, `tvdb`, `themoviedb`).
|
115
|
+
#
|
116
|
+
# @raise [RARBG::APIError] Exception raised when request fails or endpoint
|
117
|
+
# responds with an error.
|
118
|
+
#
|
119
|
+
# @example Search by IMDb ID, sorted by leechers and in extended format.
|
120
|
+
# rarbg = RARBG::API.new
|
121
|
+
# rarbg.search(imdb: 'tt012831', sort: :leechers, format: :json_extended)
|
122
|
+
#
|
123
|
+
# @example Search unranked torrents by string, with at least 2 seeders.
|
124
|
+
# rarbg = RARBG::API.new
|
125
|
+
# rarbg.search(string: 'Star Wars', ranked: false, min_seeders: 2)
|
126
|
+
def search(params = {})
|
127
|
+
raise ArgumentError, 'Expected params hash' unless params.is_a?(Hash)
|
128
|
+
|
129
|
+
params.update(
|
130
|
+
mode: 'search',
|
131
|
+
token: token?
|
132
|
+
)
|
133
|
+
call(params)
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
# Wrap request for error handling.
|
139
|
+
def call(params)
|
140
|
+
response = request(validate(params))
|
141
|
+
|
142
|
+
return [] if response['error'] == 'No results found'
|
143
|
+
raise APIError, response['error'] if response.key?('error')
|
144
|
+
response.fetch('torrent_results', [])
|
145
|
+
end
|
146
|
+
|
147
|
+
# Validate parameters.
|
148
|
+
def validate(params)
|
149
|
+
params = stringify(params)
|
150
|
+
params = validate_search!(params) if params['mode'] == 'search'
|
151
|
+
|
152
|
+
normalize.each_pair do |key, proc|
|
153
|
+
params[key] = proc.call(params[key]) if params.key?(key)
|
154
|
+
end
|
155
|
+
params
|
156
|
+
end
|
157
|
+
|
158
|
+
# Convert symbol keys to string and remove nil values.
|
159
|
+
def stringify(params)
|
160
|
+
Hash[params.reject { |_k, v| v.nil? }.map { |k, v| [k.to_s, v] }]
|
161
|
+
end
|
162
|
+
|
163
|
+
# Validate search type parameter.
|
164
|
+
def validate_search!(params)
|
165
|
+
search_keys = %w[string imdb tvdb themoviedb]
|
166
|
+
|
167
|
+
raise(
|
168
|
+
ArgumentError,
|
169
|
+
"At least one parameter required among #{search_keys.join(', ')} " \
|
170
|
+
'for search mode.'
|
171
|
+
) if (params.keys & search_keys).none?
|
172
|
+
|
173
|
+
search_keys.each do |k|
|
174
|
+
params["search_#{k}"] = params.delete(k) if params.key?(k)
|
175
|
+
end
|
176
|
+
params
|
177
|
+
end
|
178
|
+
|
179
|
+
# Convert ruby sugar to expected value style.
|
180
|
+
def normalize
|
181
|
+
{
|
182
|
+
'category' => (->(v) { v.join(';') }),
|
183
|
+
'imdb' => (->(v) { v.to_s[/^tt/] ? v.to_s : "tt#{v}" }),
|
184
|
+
'ranked' => (->(v) { v == false ? 0 : 1 })
|
185
|
+
}
|
186
|
+
end
|
187
|
+
|
188
|
+
# Return or renew auth token.
|
189
|
+
def token?
|
190
|
+
if @token.nil? || time >= (@token_time + TOKEN_EXPIRATION)
|
191
|
+
response = request(get_token: 'get_token')
|
192
|
+
@token = response.fetch('token')
|
193
|
+
@token_time = time
|
194
|
+
end
|
195
|
+
@token
|
196
|
+
end
|
197
|
+
|
198
|
+
# Perform API request.
|
199
|
+
def request(params)
|
200
|
+
rate_limit!(2.1)
|
201
|
+
|
202
|
+
response = @conn.get(nil, params)
|
203
|
+
@last_request = time
|
204
|
+
|
205
|
+
return response.body if response.success?
|
206
|
+
raise APIError, "#{response.reason_phrase} (#{response.status})"
|
207
|
+
end
|
208
|
+
|
209
|
+
# Rate-limit requests to comply with endpoint limits.
|
210
|
+
def rate_limit!(seconds)
|
211
|
+
sleep(0.3) until time >= ((@last_request || 0) + seconds)
|
212
|
+
end
|
213
|
+
|
214
|
+
# Monotonic clock for elapsed time calculations.
|
215
|
+
def time
|
216
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
data/rarbg.gemspec
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'rarbg/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'rarbg'
|
9
|
+
spec.version = RARBG::VERSION
|
10
|
+
spec.author = 'Tommaso Barbato'
|
11
|
+
spec.email = 'epistrephein@gmail.com'
|
12
|
+
|
13
|
+
spec.summary = 'RARBG Ruby client.'
|
14
|
+
spec.description = 'Ruby wrapper for RARBG Torrent API.'
|
15
|
+
spec.homepage = 'https://github.com/epistrephein/rarbg'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
19
|
+
f.match(%r{^(test|spec|features)/})
|
20
|
+
end
|
21
|
+
spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
22
|
+
spec.require_path = 'lib'
|
23
|
+
|
24
|
+
spec.metadata = {
|
25
|
+
'bug_tracker_uri' => 'https://github.com/epistrephein/rarbg/issues',
|
26
|
+
'changelog_uri' => 'https://github.com/epistrephein/rarbg/blob/master/CHANGELOG.md',
|
27
|
+
'documentation_uri' => 'http://www.rubydoc.info/gems/rarbg',
|
28
|
+
'homepage_uri' => 'https://github.com/epistrephein/rarbg',
|
29
|
+
'source_code_uri' => 'https://github.com/epistrephein/rarbg'
|
30
|
+
}
|
31
|
+
|
32
|
+
spec.required_ruby_version = '>= 2.0'
|
33
|
+
|
34
|
+
spec.add_runtime_dependency 'faraday', '~> 0.10'
|
35
|
+
spec.add_runtime_dependency 'faraday_middleware', '~> 0.10'
|
36
|
+
|
37
|
+
spec.add_development_dependency 'bundler', '~> 1.16'
|
38
|
+
spec.add_development_dependency 'pry', '~> 0'
|
39
|
+
spec.add_development_dependency 'rake', '~> 12.0'
|
40
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
41
|
+
spec.add_development_dependency 'simplecov', '~> 0'
|
42
|
+
spec.add_development_dependency 'webmock', '~> 3.3'
|
43
|
+
spec.add_development_dependency 'yard', '~> 0.9'
|
44
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe RARBG::API do
|
4
|
+
before(:all) do
|
5
|
+
@rarbg = RARBG::API.new
|
6
|
+
@token = SecureRandom.hex(5)
|
7
|
+
end
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
stub_token(@token)
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'when list request succeeds' do
|
14
|
+
before(:example) do
|
15
|
+
stub_list(
|
16
|
+
@token, {},
|
17
|
+
{ torrent_results: [
|
18
|
+
{
|
19
|
+
filename: 'first stubbed name',
|
20
|
+
category: 'first stubbed category',
|
21
|
+
download: 'first stubbed magnet link'
|
22
|
+
},
|
23
|
+
{
|
24
|
+
filename: 'second stubbed name',
|
25
|
+
category: 'second stubbed category',
|
26
|
+
download: 'second stubbed magnet link'
|
27
|
+
}
|
28
|
+
] }
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'returns and array of hashes' do
|
33
|
+
expect(@rarbg.list).to all(be_an(Hash))
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'returns hashes with filename and download link' do
|
37
|
+
expect(@rarbg.list).to all(include('filename').and include('download'))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'when list request returns no result' do
|
42
|
+
before(:example) do
|
43
|
+
stub_list(
|
44
|
+
@token, {},
|
45
|
+
{ error: 'No results found' }
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'returns an empty array' do
|
50
|
+
expect(@rarbg.list).to eq([])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'when list request parameters is not an hash' do
|
55
|
+
before(:example) do
|
56
|
+
stub_list(
|
57
|
+
@token
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'raises an ArgumentError exception' do
|
62
|
+
expect { @rarbg.list('string') }.to raise_error(
|
63
|
+
ArgumentError, 'Expected params hash'
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'when list request has invalid parameters' do
|
69
|
+
before(:example) do
|
70
|
+
stub_list(
|
71
|
+
@token,
|
72
|
+
{ min_seeders: 'string' },
|
73
|
+
{ error: 'Invalid value for min_seeders' }
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'raises a RARBG::APIError exception' do
|
78
|
+
expect { @rarbg.list(min_seeders: 'string') }.to raise_error(
|
79
|
+
RARBG::APIError, 'Invalid value for min_seeders'
|
80
|
+
)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'when list request fails' do
|
85
|
+
before(:example) do
|
86
|
+
stub_error(500, 'Internal Server Error')
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'raises a RARBG::APIError exception' do
|
90
|
+
expect { @rarbg.list }.to raise_error(
|
91
|
+
RARBG::APIError, 'Internal Server Error (500)'
|
92
|
+
)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe RARBG::API do
|
4
|
+
it 'has a version number' do
|
5
|
+
expect(RARBG::VERSION).not_to be nil
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'has an app id' do
|
9
|
+
expect(RARBG::API::APP_ID).not_to be nil
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'has the correct API endpoint' do
|
13
|
+
expect(RARBG::API::API_ENDPOINT).to eq('https://torrentapi.org/pubapi_v2.php')
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'has a token expiration' do
|
17
|
+
expect(RARBG::API::TOKEN_EXPIRATION).to be_kind_of(Numeric)
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe RARBG::API do
|
4
|
+
before(:all) do
|
5
|
+
@rarbg = RARBG::API.new
|
6
|
+
@token = SecureRandom.hex(5)
|
7
|
+
end
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
stub_token(@token)
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'when search request succeeds' do
|
14
|
+
before(:example) do
|
15
|
+
stub_search(
|
16
|
+
@token, {},
|
17
|
+
{ torrent_results: [
|
18
|
+
{
|
19
|
+
filename: 'first stubbed name',
|
20
|
+
category: 'first stubbed category',
|
21
|
+
download: 'first stubbed magnet link'
|
22
|
+
},
|
23
|
+
{
|
24
|
+
filename: 'second stubbed name',
|
25
|
+
category: 'second stubbed category',
|
26
|
+
download: 'second stubbed magnet link'
|
27
|
+
}
|
28
|
+
] }
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'returns and array of hashes' do
|
33
|
+
expect(@rarbg.search(string: 'a search string')).to all(be_an(Hash))
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'returns hashes with filename and download link' do
|
37
|
+
expect(@rarbg.search(imdb: 'tt0000000'))
|
38
|
+
.to all(include('filename').and include('download'))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'when search request returns no result' do
|
43
|
+
before(:example) do
|
44
|
+
stub_search(
|
45
|
+
@token, {},
|
46
|
+
{ error: 'No results found' }
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'returns an empty array' do
|
51
|
+
expect(@rarbg.search(string: 'awrongquery')).to eq([])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'when search request parameters is not an hash' do
|
56
|
+
before(:example) do
|
57
|
+
stub_search(
|
58
|
+
@token
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'raises an ArgumentError exception' do
|
63
|
+
expect { @rarbg.search('string') }.to raise_error(
|
64
|
+
ArgumentError, 'Expected params hash'
|
65
|
+
)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'when search request is missing search type' do
|
70
|
+
before(:example) do
|
71
|
+
stub_search(
|
72
|
+
@token
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'raises an ArgumentError exception' do
|
77
|
+
expect { @rarbg.search(category: [45, 46], sort: :last) }
|
78
|
+
.to raise_error(ArgumentError)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context 'when search request has invalid parameters' do
|
83
|
+
before(:example) do
|
84
|
+
stub_search(
|
85
|
+
@token, {},
|
86
|
+
{ error: 'Invalid sort' }
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'raises a RARBG::APIError exception' do
|
91
|
+
expect { @rarbg.search(string: 'string', sort: 'wrongsort') }
|
92
|
+
.to raise_error(RARBG::APIError, 'Invalid sort')
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context 'when search request fails' do
|
97
|
+
before(:example) do
|
98
|
+
stub_error(503, 'Service unavailable')
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'raises a RARBG::APIError exception' do
|
102
|
+
expect { @rarbg.search(string: 'string') }.to raise_error(
|
103
|
+
RARBG::APIError, 'Service unavailable (503)'
|
104
|
+
)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|