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.
@@ -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