rarbg 0.1.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RARBG
4
+ # Gem version
5
+ VERSION = '1.0.0'
6
+ end
@@ -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