rarbg 0.1.4 → 1.0.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.
- 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
|