kat 2.0.5 → 2.0.6
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/Rakefile +1 -1
- data/kat.gemspec +3 -3
- data/lib/kat/app.rb +43 -48
- data/lib/kat/colour.rb +20 -23
- data/lib/kat/field_map.rb +0 -2
- data/lib/kat/options.rb +37 -44
- data/lib/kat/search.rb +113 -109
- data/lib/kat/version.rb +1 -1
- data/test/kat/test_app.rb +39 -32
- data/test/kat/test_colour.rb +8 -8
- data/test/kat/test_options.rb +13 -13
- data/test/kat/test_search.rb +6 -6
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ef9cbd9bc5edfd5bcbbab270423445df0e0ffec2
|
4
|
+
data.tar.gz: 57b4de26084d944ad5e369f38e8727442bad663a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 509efe18911af40dfdaf17f3c86196d4990f833a500ebc2c68925d6fc0710fa86690beb266343dbb7d1134c6e171d2c578a31ded5830a2eb837378178663a135
|
7
|
+
data.tar.gz: 588d18d574b744c7f026d51fcfb2da40e8e0689dc64a94bf9f2ca9722ed3c8fac230281ee0c59007d30d2eadc9b2c65f74c97465ee6044ea3ac0ac3ed0e719b7
|
data/Rakefile
CHANGED
data/kat.gemspec
CHANGED
@@ -15,9 +15,9 @@ Gem::Specification.new do |s|
|
|
15
15
|
s.summary = 'Kickass Torrents Interface'
|
16
16
|
s.description = 'A Ruby interface to Kickass Torrents'
|
17
17
|
|
18
|
-
s.files = `git ls-files`.split
|
19
|
-
s.executables = s.files.grep(
|
20
|
-
s.test_files = s.files.grep(
|
18
|
+
s.files = `git ls-files`.split "\n"
|
19
|
+
s.executables = s.files.grep(/^bin\//) { |f| File.basename f }
|
20
|
+
s.test_files = s.files.grep(/^(test|spec|features)\//)
|
21
21
|
s.require_paths = ['lib']
|
22
22
|
|
23
23
|
s.add_runtime_dependency 'nokogiri', '~> 1.6'
|
data/lib/kat/app.rb
CHANGED
@@ -6,14 +6,11 @@ require 'highline'
|
|
6
6
|
require 'yaml'
|
7
7
|
|
8
8
|
module Kat
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
def app(args = ARGV)
|
15
|
-
App.new(args).main
|
16
|
-
end
|
9
|
+
#
|
10
|
+
# Convenience method for the App class
|
11
|
+
#
|
12
|
+
def self.app(args = ARGV)
|
13
|
+
App.new(args).main
|
17
14
|
end
|
18
15
|
|
19
16
|
class App
|
@@ -48,16 +45,16 @@ module Kat
|
|
48
45
|
#
|
49
46
|
def init_options(args = nil)
|
50
47
|
@args = case args
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
48
|
+
when nil then []
|
49
|
+
when String then args.split
|
50
|
+
else args
|
51
|
+
end
|
55
52
|
|
56
53
|
@options = load_config || {}
|
57
54
|
|
58
|
-
Kat.options(@args).tap
|
55
|
+
Kat.options(@args).tap do |o|
|
59
56
|
@options.merge!(o) { |k, ov, nv| o["#{ k }_given".intern] ? nv : ov }
|
60
|
-
|
57
|
+
end
|
61
58
|
|
62
59
|
Kat::Colour.colour = @options[:colour]
|
63
60
|
rescue NoMethodError => e
|
@@ -82,16 +79,16 @@ module Kat
|
|
82
79
|
def main
|
83
80
|
puts VERSION_STR
|
84
81
|
|
85
|
-
Kat::Search.selects.select { |k, v| @options[v[:select]] }.tap
|
82
|
+
Kat::Search.selects.select { |k, v| @options[v[:select]] }.tap do |lists|
|
86
83
|
if lists.empty?
|
87
84
|
while running; end
|
88
85
|
else
|
89
86
|
puts format_lists lists
|
90
87
|
end
|
91
|
-
|
88
|
+
end
|
92
89
|
end
|
93
90
|
|
94
|
-
|
91
|
+
private
|
95
92
|
|
96
93
|
#
|
97
94
|
# Get the width of the terminal window
|
@@ -139,7 +136,7 @@ module Kat
|
|
139
136
|
|
140
137
|
-> {
|
141
138
|
i = 0
|
142
|
-
while searching
|
139
|
+
while searching
|
143
140
|
print "\rSearching...".yellow + '\\|/-'[i % 4]
|
144
141
|
i += 1
|
145
142
|
sleep 0.1
|
@@ -147,7 +144,7 @@ module Kat
|
|
147
144
|
}
|
148
145
|
].map { |w| Thread.new { w.call } }.each(&:join)
|
149
146
|
|
150
|
-
puts
|
147
|
+
puts((res = format_results))
|
151
148
|
|
152
149
|
if res.size > 1
|
153
150
|
case (answer = prompt)
|
@@ -156,7 +153,7 @@ module Kat
|
|
156
153
|
when 'p' then @page -= 1 if prev?
|
157
154
|
when 'q' then return false
|
158
155
|
else
|
159
|
-
if (1..@kat.results[@page].size).include?
|
156
|
+
if (1..@kat.results[@page].size).include?((answer = answer.to_i))
|
160
157
|
print "\nDownloading".yellow <<
|
161
158
|
": #{ @kat.results[@page][answer - 1][:title] }... "
|
162
159
|
puts download @kat.results[@page][answer - 1]
|
@@ -173,21 +170,21 @@ module Kat
|
|
173
170
|
# Format a list of options
|
174
171
|
#
|
175
172
|
def format_lists(lists)
|
176
|
-
lists.inject([nil])
|
177
|
-
opts = Kat::Search.send(
|
178
|
-
buf <<
|
179
|
-
buf << nil unless
|
173
|
+
lists.inject([nil]) do |buf, (_, val)|
|
174
|
+
opts = Kat::Search.send(val[:select])
|
175
|
+
buf << val[:select].to_s.capitalize
|
176
|
+
buf << nil unless opts.values.first.is_a? Array
|
180
177
|
width = opts.keys.sort { |a, b| b.size <=> a.size }.first.size
|
181
|
-
opts.each
|
182
|
-
buf += if Array
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
178
|
+
opts.each do |k, v|
|
179
|
+
buf += if v.is_a? Array
|
180
|
+
[nil, "%#{ width }s => #{ v.shift }" % k] +
|
181
|
+
v.map { |e| ' ' * (width + 4) + e }
|
182
|
+
else
|
183
|
+
["%-#{ width }s => #{ v }" % k]
|
184
|
+
end
|
185
|
+
end
|
189
186
|
buf << nil
|
190
|
-
|
187
|
+
end
|
191
188
|
end
|
192
189
|
|
193
190
|
#
|
@@ -208,14 +205,14 @@ module Kat
|
|
208
205
|
buf << ("\r%-#{ main_width + 5 }s#{ ' Size Age Seeds Leeches' if !hide_info? || @show_info }" %
|
209
206
|
["Page #{ page + 1 } of #{ @kat.pages }", nil]).yellow
|
210
207
|
|
211
|
-
@kat.results[@page].each_with_index
|
208
|
+
@kat.results[@page].each_with_index do |t, i|
|
212
209
|
age = t[:age].split "\xC2\xA0"
|
213
|
-
age =
|
210
|
+
age = '%3d %-6s' % age
|
214
211
|
# Filter out the crap that invariably infests torrent names
|
215
212
|
title = t[:title].codepoints.map { |c| c > 31 && c < 127 ? c.chr : '?' }.join[0...main_width]
|
216
213
|
buf << ("%2d. %-#{ main_width }s#{ ' %10s %10s %7d %7d' if !hide_info? or @show_info }" %
|
217
214
|
[i + 1, title, t[:size], age, t[:seeds], t[:leeches]]).tap { |s| s.red! if t[:seeds] == 0 }
|
218
|
-
|
215
|
+
end
|
219
216
|
|
220
217
|
buf << nil
|
221
218
|
end
|
@@ -242,12 +239,12 @@ module Kat
|
|
242
239
|
"#{ ', ' << '(n)'.cyan(true) << 'ext' if next? }" <<
|
243
240
|
"#{ ', ' << '(p)'.cyan(true) << 'rev' if prev? }" <<
|
244
241
|
"#{ ", #{ @show_info ? 'hide' : 'show' } " << '(i)'.cyan(true) << 'nfo' if hide_info? }" <<
|
245
|
-
', ' << '(q)'.cyan(true) << 'uit: ')
|
242
|
+
', ' << '(q)'.cyan(true) << 'uit: ') do |q|
|
246
243
|
q.responses[:not_valid] = 'Invalid option.'
|
247
244
|
q.validate = validation_regex
|
248
|
-
|
249
|
-
rescue RegexpError
|
250
|
-
puts
|
245
|
+
end
|
246
|
+
rescue RegexpError
|
247
|
+
puts((@kat.pages > 0 ? "Error reading the page\n" : "Could not connect to the site\n").red)
|
251
248
|
|
252
249
|
return 'q'
|
253
250
|
end
|
@@ -260,12 +257,12 @@ module Kat
|
|
260
257
|
|
261
258
|
uri = URI(URI.encode torrent[:download])
|
262
259
|
uri.query = nil
|
263
|
-
file = "#{ @options[:output] || '.' }/"
|
260
|
+
file = "#{ @options[:output] || '.' }/" \
|
264
261
|
"#{ torrent[:title].tr(' ', ?.).gsub(/[^a-z0-9()_.-]/i, '') }.torrent"
|
265
262
|
|
266
|
-
fail '404 File Not Found' if (res = Net::HTTP.start(uri.host)
|
263
|
+
fail '404 File Not Found' if (res = Net::HTTP.start(uri.host) do |http|
|
267
264
|
http.get uri
|
268
|
-
|
265
|
+
end).code == '404'
|
269
266
|
|
270
267
|
File.open(File.expand_path(file), 'w') { |f| f.write res.body }
|
271
268
|
|
@@ -278,13 +275,11 @@ module Kat
|
|
278
275
|
# Load options from CONFIG if it exists
|
279
276
|
#
|
280
277
|
def load_config
|
281
|
-
(symbolise =
|
282
|
-
Hash
|
283
|
-
|
278
|
+
(symbolise = lambda do |h|
|
279
|
+
h.is_a?(Hash) ? Hash[h.map { |k, v| [k.intern, symbolise[v]] }] : h
|
280
|
+
end)[YAML.load_file CONFIG] if File.readable? CONFIG
|
284
281
|
rescue => e
|
285
282
|
warn "Failed to load #{ CONFIG }: #{ e }"
|
286
283
|
end
|
287
|
-
|
288
284
|
end
|
289
|
-
|
290
285
|
end
|
data/lib/kat/colour.rb
CHANGED
@@ -1,23 +1,19 @@
|
|
1
1
|
module Kat
|
2
|
-
|
3
2
|
module Colour
|
4
|
-
|
5
3
|
COLOURS = %w(black red green yellow blue magenta cyan white)
|
6
4
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
end
|
5
|
+
# From AwesomePrint.colorize? by Michael Dvorkin
|
6
|
+
# https://github.com/michaeldv/awesome_print/blob/master/lib/awesome_print/inspector.rb
|
7
|
+
def self.capable?
|
8
|
+
STDOUT.tty? && (ENV['TERM'] && ENV['TERM'] != 'dumb' || ENV['ANSICON'])
|
9
|
+
end
|
13
10
|
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
def self.colour=(f)
|
12
|
+
@@colour = f && capable?
|
13
|
+
end
|
17
14
|
|
18
|
-
|
19
|
-
|
20
|
-
end
|
15
|
+
def self.colour?
|
16
|
+
@@colour
|
21
17
|
end
|
22
18
|
|
23
19
|
@@colour = capable?
|
@@ -26,14 +22,14 @@ module Kat
|
|
26
22
|
@@colour
|
27
23
|
end
|
28
24
|
|
29
|
-
COLOURS.each
|
25
|
+
COLOURS.each do |c|
|
30
26
|
define_method(c) { |*args| colour c, args[0] }
|
31
27
|
define_method("#{ c }!") { |*args| colour! c, args[0] }
|
32
|
-
|
28
|
+
end
|
33
29
|
|
34
30
|
def uncolour
|
35
31
|
case self
|
36
|
-
when String then gsub
|
32
|
+
when String then gsub(/\e\[[0-9;]+?m(.*?)\e\[0m/, '\\1')
|
37
33
|
when Array then map { |e| e.uncolour if e }
|
38
34
|
else self
|
39
35
|
end
|
@@ -48,12 +44,15 @@ module Kat
|
|
48
44
|
self
|
49
45
|
end
|
50
46
|
|
51
|
-
|
47
|
+
private
|
52
48
|
|
53
49
|
def colour(name, intense = false)
|
54
50
|
return case self
|
55
|
-
when String, Symbol
|
56
|
-
|
51
|
+
when String, Symbol
|
52
|
+
"\e[#{ intense ? 1 : 0 };" \
|
53
|
+
"#{ 30 + COLOURS.index(name) }m#{ self }\e[0m"
|
54
|
+
when Array
|
55
|
+
map { |e| e.send name.to_s, intense if e }
|
57
56
|
end if colour?
|
58
57
|
|
59
58
|
self
|
@@ -62,14 +61,12 @@ module Kat
|
|
62
61
|
def colour!(name, intense = false)
|
63
62
|
case self
|
64
63
|
when String then replace send(name.to_s, intense)
|
65
|
-
when Array then each { |e| e.send "#{ name
|
64
|
+
when Array then each { |e| e.send "#{ name }!", intense if e }
|
66
65
|
end if colour?
|
67
66
|
|
68
67
|
self
|
69
68
|
end
|
70
|
-
|
71
69
|
end
|
72
|
-
|
73
70
|
end
|
74
71
|
|
75
72
|
class String; include Kat::Colour end
|
data/lib/kat/field_map.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
module Kat
|
2
|
-
|
3
2
|
FIELD_MAP = {
|
4
3
|
exact: { type: :string, desc: 'Exact phrase' },
|
5
4
|
or: { type: :string, desc: 'Optional words', multi: true },
|
@@ -29,5 +28,4 @@ module Kat
|
|
29
28
|
output: { type: :string, desc: 'Directory to save torrents in', short: :o },
|
30
29
|
colour: { desc: 'Output with colour', short: :none }
|
31
30
|
}.freeze
|
32
|
-
|
33
31
|
end
|
data/lib/kat/options.rb
CHANGED
@@ -3,57 +3,50 @@ require File.dirname(__FILE__) + '/version'
|
|
3
3
|
require 'trollop'
|
4
4
|
|
5
5
|
module Kat
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
def options(args)
|
12
|
-
Options.parse args
|
13
|
-
end
|
6
|
+
#
|
7
|
+
# Convenience method for the Options class
|
8
|
+
#
|
9
|
+
def self.options(args)
|
10
|
+
Options.parse args
|
14
11
|
end
|
15
12
|
|
16
13
|
class Options
|
14
|
+
#
|
15
|
+
# Pick out the invocation options from the field map
|
16
|
+
#
|
17
|
+
def self.options_map
|
18
|
+
fields = %i(desc type multi select short)
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
-
# Pick out the invocation options from the field map
|
21
|
-
#
|
22
|
-
def options_map
|
23
|
-
fields = %i(desc type multi select short)
|
24
|
-
|
25
|
-
FIELD_MAP.inject({}) { |hash, (k, v)|
|
26
|
-
hash.tap { |h| h[k] = v.select { |f| fields.include? f } if v[:desc] }
|
27
|
-
}
|
20
|
+
FIELD_MAP.reduce({}) do |hash, (k, v)|
|
21
|
+
hash.tap { |h| h[k] = v.select { |f| fields.include? f } if v[:desc] }
|
28
22
|
end
|
23
|
+
end
|
29
24
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
}
|
25
|
+
def self.parse(args)
|
26
|
+
Trollop.options(args) do
|
27
|
+
version VERSION_STR
|
28
|
+
banner <<-USAGE.gsub(/^\s+\|/, '')
|
29
|
+
|#{ VERSION_STR }
|
30
|
+
|
|
31
|
+
|Usage: #{ File.basename __FILE__ } [options] <query>+
|
32
|
+
|
|
33
|
+
| Options:
|
34
|
+
USAGE
|
35
|
+
|
36
|
+
Options.options_map.each do |k, v|
|
37
|
+
opt k,
|
38
|
+
v[:desc],
|
39
|
+
type: v[:type] || :boolean,
|
40
|
+
multi: v[:multi],
|
41
|
+
short: v[:short]
|
42
|
+
end
|
43
|
+
|
44
|
+
Options.options_map.each do |k, v|
|
45
|
+
opt v[:select],
|
46
|
+
"List the #{ v[:select] } that may be used with --#{ k }",
|
47
|
+
short: :none if v[:select]
|
48
|
+
end
|
55
49
|
end
|
56
50
|
end
|
57
|
-
|
58
51
|
end
|
59
52
|
end
|
data/lib/kat/search.rb
CHANGED
@@ -5,23 +5,20 @@ require 'net/http'
|
|
5
5
|
require 'andand'
|
6
6
|
|
7
7
|
module Kat
|
8
|
-
|
9
8
|
BASE_URL = 'http://kickass.to'
|
10
9
|
RECENT_PATH = 'new'
|
11
10
|
SEARCH_PATH = 'usearch'
|
12
11
|
ADVANCED_URL = "#{ BASE_URL }/torrents/search/advanced/"
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
13
|
+
#
|
14
|
+
# Convenience methods for the Search class
|
15
|
+
#
|
16
|
+
def self.search(search_term = nil, opts = {})
|
17
|
+
Search.new search_term, opts
|
18
|
+
end
|
21
19
|
|
22
|
-
|
23
|
-
|
24
|
-
end
|
20
|
+
def self.quick_search(search_term = nil)
|
21
|
+
Search.quick_search search_term
|
25
22
|
end
|
26
23
|
|
27
24
|
class Search
|
@@ -35,86 +32,35 @@ module Kat
|
|
35
32
|
|
36
33
|
@@doc = nil
|
37
34
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
new(search_term).search
|
45
|
-
end
|
46
|
-
|
47
|
-
def field_map(type = nil)
|
48
|
-
return FIELD_MAP.dup unless type
|
49
|
-
|
50
|
-
FIELD_MAP.inject({}) { |hash, (k, v)|
|
51
|
-
hash.tap { |h|
|
52
|
-
case type
|
53
|
-
when :select then h[k] = { select: v[:select], id: v[:id] || k }
|
54
|
-
when :sort then h[k] = v[:sort] and h[v[:sort]] = v[:sort]
|
55
|
-
h[v[:id]] = v[:sort] if v[:id]
|
56
|
-
else h[k] = v[type]
|
57
|
-
end if v[type]
|
58
|
-
}
|
59
|
-
}
|
60
|
-
end
|
61
|
-
|
62
|
-
def checks; field_map :check end
|
63
|
-
def inputs; field_map :input end
|
64
|
-
def selects; field_map :select end
|
65
|
-
def sorts; field_map :sort end
|
66
|
-
|
67
|
-
private
|
35
|
+
#
|
36
|
+
# Kat.quick_search will do a quick search and return the results
|
37
|
+
#
|
38
|
+
def self.quick_search(search_term = nil)
|
39
|
+
new(search_term).search
|
40
|
+
end
|
68
41
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
unless (group = opts.css('optgroup')).empty?
|
84
|
-
# Categories
|
85
|
-
group.inject({}) { |cat, og|
|
86
|
-
cat.tap { |c|
|
87
|
-
c[og.attributes['label'].value] = og.children.map { |o|
|
88
|
-
o.attributes['value'].value
|
89
|
-
}
|
90
|
-
}
|
91
|
-
}
|
92
|
-
else
|
93
|
-
# Times, languages, platforms
|
94
|
-
opts.reject { |o| o.attributes.empty? }.inject({}) { |p, o|
|
95
|
-
p.tap { |p| p[o.text] = o.attributes['value'].value }
|
96
|
-
}
|
42
|
+
def self.field_map(type = nil)
|
43
|
+
return FIELD_MAP.dup unless type
|
44
|
+
|
45
|
+
FIELD_MAP.reduce({}) do |hash, (k, v)|
|
46
|
+
hash.tap do |h|
|
47
|
+
case type
|
48
|
+
when :select
|
49
|
+
h[k] = { select: v[:select], id: v[:id] || k }
|
50
|
+
when :sort
|
51
|
+
h[k] = v[:sort] && h[v[:sort]] = v[:sort]
|
52
|
+
h[v[:id]] = v[:sort] if v[:id]
|
53
|
+
else
|
54
|
+
h[k] = v[type]
|
55
|
+
end if v[type]
|
97
56
|
end
|
98
|
-
rescue => e
|
99
|
-
{ error: e }
|
100
|
-
end
|
101
|
-
|
102
|
-
#
|
103
|
-
# If method is a field name in SELECT_FIELDS, fetch the list of values.
|
104
|
-
#
|
105
|
-
def method_missing(method, *args, &block)
|
106
|
-
return super unless respond_to? method
|
107
|
-
field_options selects.find { |k, v| v[:select] == method }.first
|
108
|
-
end
|
109
|
-
|
110
|
-
#
|
111
|
-
# If method is a field name in SELECT_FIELDS, we can fetch the list of values
|
112
|
-
#
|
113
|
-
def respond_to_missing?(method, include_private = false)
|
114
|
-
!!selects.find { |k, v| v[:select] == method } || super
|
115
57
|
end
|
58
|
+
end
|
116
59
|
|
117
|
-
|
60
|
+
def self.checks; field_map :check end
|
61
|
+
def self.inputs; field_map :input end
|
62
|
+
def self.selects; field_map :select end
|
63
|
+
def self.sorts; field_map :sort end
|
118
64
|
|
119
65
|
#
|
120
66
|
# Create a new +Kat::Search+ object to search Kickass Torrents.
|
@@ -139,9 +85,9 @@ module Kat
|
|
139
85
|
str = [RECENT_PATH] if str[1].empty?
|
140
86
|
str << page + 1 if page > 0
|
141
87
|
|
142
|
-
sorts.
|
88
|
+
sorts.detect { |k, v| @options[:sort] && k == @options[:sort].intern }.tap do |k, v|
|
143
89
|
str << (k ? "?field=#{ v }&sorder=#{ options[:asc] ? 'asc' : 'desc' }" : '')
|
144
|
-
|
90
|
+
end
|
145
91
|
|
146
92
|
str.join '/'
|
147
93
|
end
|
@@ -152,13 +98,14 @@ module Kat
|
|
152
98
|
# Raises ArgumentError if search_term is not a String, Symbol or Array
|
153
99
|
#
|
154
100
|
def query=(search_term)
|
155
|
-
@search_term =
|
101
|
+
@search_term =
|
102
|
+
case search_term
|
156
103
|
when nil, '' then []
|
157
104
|
when String, Symbol then [search_term]
|
158
105
|
when Array then search_term.flatten.select { |e| [String, Symbol].include? e.class }
|
159
|
-
else fail ArgumentError,
|
106
|
+
else fail ArgumentError, 'search_term must be a String, Symbol or Array. ' \
|
160
107
|
"#{ search_term.inspect } given."
|
161
|
-
|
108
|
+
end
|
162
109
|
|
163
110
|
build_query
|
164
111
|
end
|
@@ -177,8 +124,8 @@ module Kat
|
|
177
124
|
# Raises ArgumentError if options is not a Hash
|
178
125
|
#
|
179
126
|
def options=(options)
|
180
|
-
fail ArgumentError,
|
181
|
-
"#{ options.inspect } given." unless Hash
|
127
|
+
fail ArgumentError, 'options must be a Hash. ' \
|
128
|
+
"#{ options.inspect } given." unless options.is_a?(Hash)
|
182
129
|
|
183
130
|
@options.merge! options
|
184
131
|
|
@@ -194,13 +141,13 @@ module Kat
|
|
194
141
|
# a result set limited to the 25 results Kickass Torrents returns itself. Will
|
195
142
|
# cache results for subsequent calls of search with the same query string.
|
196
143
|
#
|
197
|
-
def search(
|
144
|
+
def search(page_num = 0)
|
198
145
|
@error = nil
|
199
146
|
@message = nil
|
200
147
|
|
201
|
-
search_proc =
|
148
|
+
search_proc = lambda do |page|
|
202
149
|
begin
|
203
|
-
uri = URI(URI
|
150
|
+
uri = URI(URI.encode(to_s page))
|
204
151
|
res = Net::HTTP.get_response(uri)
|
205
152
|
if res.code == '301'
|
206
153
|
path = Net::HTTP::Get.new(res.header['location'])
|
@@ -212,7 +159,7 @@ module Kat
|
|
212
159
|
|
213
160
|
doc = Nokogiri::HTML(res.body)
|
214
161
|
|
215
|
-
@results[page] = doc.xpath('//table[@class="data"]/tr[position()>1]/td[1]').map
|
162
|
+
@results[page] = doc.xpath('//table[@class="data"]/tr[position()>1]/td[1]').map do |node|
|
216
163
|
{ path: node.css('a.torType').first.andand.attr('href'),
|
217
164
|
title: node.css('a.cellMainLink').text,
|
218
165
|
magnet: node.css('a.imagnet').first.andand.attr('href'),
|
@@ -221,8 +168,8 @@ module Kat
|
|
221
168
|
files: (node = node.next_element).text.to_i,
|
222
169
|
age: (node = node.next_element).text,
|
223
170
|
seeds: (node = node.next_element).text.to_i,
|
224
|
-
leeches:
|
225
|
-
|
171
|
+
leeches: node.next_element.text.to_i }
|
172
|
+
end
|
226
173
|
|
227
174
|
# If we haven't previously performed a search with this query string, get the
|
228
175
|
# number of pages from the pagination bar at the bottom of the results page.
|
@@ -234,16 +181,16 @@ module Kat
|
|
234
181
|
rescue => e
|
235
182
|
@error = { error: e }
|
236
183
|
end unless @results[page] || (@pages > -1 && page >= @pages)
|
237
|
-
|
184
|
+
end
|
238
185
|
|
239
186
|
# Make sure we do a query for the first page of results before getting
|
240
187
|
# subsequent pages in order to correctly figure out the total number of
|
241
188
|
# pages of results.
|
242
|
-
pages = (Range
|
189
|
+
pages = (page_num.is_a?(Range) ? page_num.to_a : [page_num])
|
243
190
|
pages.unshift(0) if @pages == -1 && !pages.include?(0)
|
244
191
|
pages.each { |i| search_proc.call i }
|
245
192
|
|
246
|
-
results[Range
|
193
|
+
results[page_num.is_a?(Range) ? page_num.max : page_num]
|
247
194
|
end
|
248
195
|
|
249
196
|
#
|
@@ -276,7 +223,61 @@ module Kat
|
|
276
223
|
"#{ BASE_URL }/#{ query_str page }"
|
277
224
|
end
|
278
225
|
|
279
|
-
|
226
|
+
private
|
227
|
+
|
228
|
+
#
|
229
|
+
# Get a list of options for a particular selection field from the advanced search form
|
230
|
+
#
|
231
|
+
def self.field_options(label)
|
232
|
+
fail 'Unknown search field' unless selects.detect do |k, v|
|
233
|
+
k == label.intern
|
234
|
+
end
|
235
|
+
|
236
|
+
url = URI(ADVANCED_URL)
|
237
|
+
|
238
|
+
req = Net::HTTP.start(url.host) { |http| http.get url }
|
239
|
+
@@doc ||= Nokogiri::HTML(req.body)
|
240
|
+
|
241
|
+
opts = @@doc.css('table.formtable td').detect do |e|
|
242
|
+
e.text[/#{ label }/i]
|
243
|
+
end
|
244
|
+
|
245
|
+
opts = opts.next_element.first_element_child.children
|
246
|
+
|
247
|
+
if (group = opts.css('optgroup')).empty?
|
248
|
+
# Times, languages, platforms
|
249
|
+
opts.reject { |o| o.attributes.empty? }.reduce({}) do |p, o|
|
250
|
+
p.tap { |p1| p1[o.text] = o.attributes['value'].value }
|
251
|
+
end
|
252
|
+
else
|
253
|
+
# Categories
|
254
|
+
group.reduce({}) do |cat, og|
|
255
|
+
cat.tap do |c|
|
256
|
+
c[og.attributes['label'].value] = og.children.map do |o|
|
257
|
+
o.attributes['value'].value
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
rescue => e
|
263
|
+
{ error: e }
|
264
|
+
end
|
265
|
+
|
266
|
+
#
|
267
|
+
# If method is a field name in SELECT_FIELDS, fetch the list of values.
|
268
|
+
#
|
269
|
+
def self.method_missing(method, *args, &block)
|
270
|
+
return super unless respond_to? method
|
271
|
+
field_options selects.detect { |k, v| v[:select] == method }.first
|
272
|
+
end
|
273
|
+
|
274
|
+
#
|
275
|
+
# If method is a field name in SELECT_FIELDS,
|
276
|
+
# we can fetch the list of values
|
277
|
+
#
|
278
|
+
def self.respond_to_missing?(method, include_private = false)
|
279
|
+
!!selects.detect { |k, v| v[:select] == method } || super
|
280
|
+
end
|
280
281
|
|
281
282
|
#
|
282
283
|
# Clear out the query and rebuild it from the various stored options. Also clears out the
|
@@ -293,17 +294,22 @@ module Kat
|
|
293
294
|
|
294
295
|
@query += inputs.select { |k, v| @options[k] }.map { |k, v| "#{ k }:#{ @options[k] }" }
|
295
296
|
@query += checks.select { |k, v| @options[k] }.map { |k, v| "#{ k }:1" }
|
296
|
-
|
297
|
+
|
298
|
+
byzantine = selects.select do |k, v|
|
297
299
|
(v[:id].to_s[/^.*_id$/] && @options[k].to_s.to_i > 0) ||
|
298
300
|
(v[:id].to_s[/^[^_]+$/] && @options[k])
|
299
|
-
|
301
|
+
end
|
302
|
+
|
303
|
+
@query += byzantine.map { |k, v| "#{ v[:id] }:#{ @options[k] }" }
|
300
304
|
end
|
301
305
|
|
302
306
|
#
|
303
307
|
# Fetch a list of values from the results set given by name
|
304
308
|
#
|
305
309
|
def results_column(name)
|
306
|
-
@results.compact.map
|
310
|
+
@results.compact.map do |rs|
|
311
|
+
rs.map { |r| r[name] || r[name[0...-1].intern] }
|
312
|
+
end.flatten
|
307
313
|
end
|
308
314
|
|
309
315
|
#
|
@@ -318,7 +324,5 @@ module Kat
|
|
318
324
|
!(@results.empty? || @results.first.empty?) &&
|
319
325
|
(@results.first.first[method] || @results.first.first[method[0..-2].intern]) || super
|
320
326
|
end
|
321
|
-
|
322
327
|
end
|
323
|
-
|
324
328
|
end
|
data/lib/kat/version.rb
CHANGED
data/test/kat/test_app.rb
CHANGED
@@ -23,7 +23,7 @@ describe Kat::App do
|
|
23
23
|
|
24
24
|
it 'creates a validation regex' do
|
25
25
|
app.page.must_equal 0
|
26
|
-
app.instance_exec
|
26
|
+
app.instance_exec do
|
27
27
|
@window_width = 80
|
28
28
|
|
29
29
|
prev?.wont_equal true
|
@@ -42,8 +42,9 @@ describe Kat::App do
|
|
42
42
|
|
43
43
|
prev?.must_equal true
|
44
44
|
next?.wont_equal true
|
45
|
-
# Skip the test if there's no results. We really only want to test in
|
46
|
-
# network conditions and no results here are an indication that's
|
45
|
+
# Skip the test if there's no results. We really only want to test in
|
46
|
+
# ideal network conditions and no results here are an indication that's
|
47
|
+
# not the case
|
47
48
|
validation_regex.must_equal(
|
48
49
|
/^([pq]|[1-#{ [9, n].min }]#{
|
49
50
|
"|1[0-#{ [9, n - 10].min }]" if n > 9
|
@@ -51,19 +52,19 @@ describe Kat::App do
|
|
51
52
|
) if n > 0
|
52
53
|
|
53
54
|
@page = 0
|
54
|
-
|
55
|
+
end
|
55
56
|
end
|
56
57
|
|
57
58
|
it 'deals with terminal width' do
|
58
|
-
app.instance_exec
|
59
|
+
app.instance_exec do
|
59
60
|
set_window_width
|
60
|
-
hide_info?.must_equal
|
61
|
-
|
61
|
+
hide_info?.must_equal(@window_width < 81)
|
62
|
+
end
|
62
63
|
end
|
63
64
|
|
64
65
|
it 'formats a list of options' do
|
65
|
-
app.instance_exec
|
66
|
-
%i(category added platform language).each
|
66
|
+
app.instance_exec do
|
67
|
+
%i(category added platform language).each do |s|
|
67
68
|
list = format_lists(s => Kat::Search.selects[s])
|
68
69
|
|
69
70
|
list.must_be_instance_of Array
|
@@ -71,22 +72,28 @@ describe Kat::App do
|
|
71
72
|
|
72
73
|
[0, 2, list.size - 1].each { |i| list[i].must_be_nil }
|
73
74
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
3.upto(list.size - 2)
|
82
|
-
|
83
|
-
|
75
|
+
str = case s
|
76
|
+
when :added then 'Times'
|
77
|
+
when :category then 'Categories'
|
78
|
+
else s.to_s.capitalize << 's'
|
79
|
+
end
|
80
|
+
list[1].must_equal str
|
81
|
+
|
82
|
+
3.upto(list.size - 2) do |i|
|
83
|
+
list[i].must_be_instance_of String
|
84
|
+
end unless s == :category
|
85
|
+
|
86
|
+
3.upto(list.size - 2) do |i|
|
87
|
+
list[i].must_match(/^\s*([A-Z]+ => )?[a-z0-9-]+/) if list[i]
|
88
|
+
end if s == :category
|
89
|
+
end
|
90
|
+
end
|
84
91
|
end
|
85
92
|
|
86
93
|
it 'formats a list of torrents' do
|
87
94
|
Kat::Colour.colour = false
|
88
95
|
|
89
|
-
app.instance_exec
|
96
|
+
app.instance_exec do
|
90
97
|
set_window_width
|
91
98
|
list = format_results
|
92
99
|
|
@@ -95,33 +102,33 @@ describe Kat::App do
|
|
95
102
|
|
96
103
|
list.last.must_be_nil
|
97
104
|
|
98
|
-
(2..list.size - 2).each
|
99
|
-
list[i].must_match
|
100
|
-
|
101
|
-
|
105
|
+
(2..list.size - 2).each do |i|
|
106
|
+
list[i].must_match(/^(\s[1-9]|[12][0-9])\. .*/)
|
107
|
+
end
|
108
|
+
end
|
102
109
|
end
|
103
110
|
|
104
111
|
it 'downloads data from a URL' do
|
105
112
|
Kat::Colour.colour = false
|
106
113
|
|
107
|
-
app.instance_exec
|
114
|
+
app.instance_exec do
|
108
115
|
s = 'foobar'
|
109
|
-
result = download(
|
116
|
+
result = download(download: 'http://google.com', title: s)
|
110
117
|
result.must_equal :done
|
111
|
-
File.
|
118
|
+
File.exist?(File.expand_path "./#{ s }.torrent").must_equal true
|
112
119
|
File.delete(File.expand_path "./#{ s }.torrent")
|
113
|
-
|
120
|
+
end
|
114
121
|
end
|
115
122
|
|
116
123
|
it 'returns an error message when a download fails' do
|
117
124
|
Kat::Colour.colour = false
|
118
125
|
|
119
|
-
app.instance_exec
|
120
|
-
result = download(
|
126
|
+
app.instance_exec do
|
127
|
+
result = download(download: 'http://foo.bar', title: 'foobar')
|
121
128
|
result.must_be_instance_of Array
|
122
129
|
result.first.must_equal :failed
|
123
|
-
result.last.must_match
|
124
|
-
|
130
|
+
result.last.must_match(/^getaddrinfo/)
|
131
|
+
end
|
125
132
|
end
|
126
133
|
end
|
127
134
|
end
|
data/test/kat/test_colour.rb
CHANGED
@@ -14,18 +14,18 @@ describe Kat::Colour do
|
|
14
14
|
end
|
15
15
|
|
16
16
|
it 'has colour methods' do
|
17
|
-
colours.each
|
17
|
+
colours.each do |c|
|
18
18
|
''.respond_to?(c).must_equal true
|
19
19
|
:s.respond_to?(c).must_equal true
|
20
20
|
[].respond_to?(c).must_equal true
|
21
21
|
{}.respond_to?(c).wont_equal true
|
22
|
-
|
22
|
+
end
|
23
23
|
end
|
24
24
|
|
25
25
|
it 'colours strings' do
|
26
26
|
Kat::Colour.colour = true
|
27
27
|
|
28
|
-
colours.each_with_index
|
28
|
+
colours.each_with_index do |c, i|
|
29
29
|
str = 'foobar'
|
30
30
|
result = "\e[0;#{ 30 + i }mfoobar\e[0m"
|
31
31
|
intense_result = "\e[1;#{ 30 + i }mfoobar\e[0m"
|
@@ -42,7 +42,7 @@ describe Kat::Colour do
|
|
42
42
|
str = 'foobar'
|
43
43
|
str.send("#{ c }!", true).must_equal intense_result
|
44
44
|
str.must_equal intense_result
|
45
|
-
|
45
|
+
end
|
46
46
|
end
|
47
47
|
|
48
48
|
it 'uncolour strings' do
|
@@ -59,7 +59,7 @@ describe Kat::Colour do
|
|
59
59
|
it 'colours symbols' do
|
60
60
|
Kat::Colour.colour = true
|
61
61
|
|
62
|
-
colours.each_with_index
|
62
|
+
colours.each_with_index do |c, i|
|
63
63
|
sym = :foobar
|
64
64
|
result = "\e[0;#{ 30 + i }mfoobar\e[0m"
|
65
65
|
intense_result = "\e[1;#{ 30 + i }mfoobar\e[0m"
|
@@ -75,7 +75,7 @@ describe Kat::Colour do
|
|
75
75
|
|
76
76
|
sym.send("#{ c }!", true).must_equal :foobar
|
77
77
|
sym.must_equal :foobar
|
78
|
-
|
78
|
+
end
|
79
79
|
end
|
80
80
|
|
81
81
|
it 'does not uncolour symbols' do
|
@@ -91,7 +91,7 @@ describe Kat::Colour do
|
|
91
91
|
it 'colours arrays of strings and symbols' do
|
92
92
|
Kat::Colour.colour = true
|
93
93
|
|
94
|
-
colours.each_with_index
|
94
|
+
colours.each_with_index do |c, i|
|
95
95
|
s = ['foobar', :foobar, nil, ['foobar', :foobar, nil]]
|
96
96
|
t = ['foobar', :foobar, nil, ['foobar', :foobar, nil]]
|
97
97
|
result = [
|
@@ -110,7 +110,7 @@ describe Kat::Colour do
|
|
110
110
|
result[3][1] = :foobar
|
111
111
|
s.send("#{ c }!").must_equal result
|
112
112
|
s.must_equal result
|
113
|
-
|
113
|
+
end
|
114
114
|
end
|
115
115
|
|
116
116
|
it 'uncolours arrays of strings' do
|
data/test/kat/test_options.rb
CHANGED
@@ -13,22 +13,22 @@ describe Kat::Options do
|
|
13
13
|
end
|
14
14
|
|
15
15
|
it 'has symbolised keys' do
|
16
|
-
opts.keys.each
|
16
|
+
opts.keys.each do |key|
|
17
17
|
key.must_be_instance_of Symbol
|
18
18
|
key.wont_equal :size
|
19
|
-
|
19
|
+
end
|
20
20
|
end
|
21
21
|
|
22
22
|
it 'has a hash for values, each with a description' do
|
23
|
-
opts.values.each
|
23
|
+
opts.values.each do |value|
|
24
24
|
value.must_be_instance_of Hash
|
25
25
|
value.keys.must_include :desc
|
26
|
-
|
26
|
+
end
|
27
27
|
end
|
28
28
|
|
29
29
|
it 'has symbolised keys in each value' do
|
30
|
-
opts.each
|
31
|
-
value.each
|
30
|
+
opts.each do |key, value|
|
31
|
+
value.each do |k, v|
|
32
32
|
k.must_be_instance_of Symbol
|
33
33
|
case k
|
34
34
|
when :desc
|
@@ -37,12 +37,12 @@ describe Kat::Options do
|
|
37
37
|
v.must_equal true
|
38
38
|
when :short
|
39
39
|
v.must_be_instance_of Symbol
|
40
|
-
v.must_match
|
40
|
+
v.must_match(/\A([a-z]|none)\Z/)
|
41
41
|
else
|
42
42
|
v.must_be_instance_of Symbol
|
43
43
|
end
|
44
|
-
|
45
|
-
|
44
|
+
end
|
45
|
+
end
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
@@ -55,9 +55,9 @@ describe Kat::Options do
|
|
55
55
|
|
56
56
|
opts.must_be_instance_of Hash
|
57
57
|
opts.wont_be_empty
|
58
|
-
opts.values.each
|
58
|
+
opts.values.each do |value|
|
59
59
|
[[], nil, false].must_include value
|
60
|
-
|
60
|
+
end
|
61
61
|
end
|
62
62
|
|
63
63
|
it 'returns an options hash with some options switched on' do
|
@@ -68,9 +68,9 @@ describe Kat::Options do
|
|
68
68
|
|
69
69
|
opts[:or].must_equal %w(baz)
|
70
70
|
opts[:sort].must_equal 'age'
|
71
|
-
%i(colour colour_given or_given sort_given).each
|
71
|
+
%i(colour colour_given or_given sort_given).each do |key|
|
72
72
|
opts[key].must_equal true
|
73
|
-
|
73
|
+
end
|
74
74
|
end
|
75
75
|
end
|
76
76
|
|
data/test/kat/test_search.rb
CHANGED
@@ -7,7 +7,7 @@ blue_peter.go.go 1
|
|
7
7
|
describe Kat::Search do
|
8
8
|
|
9
9
|
let(:kat) { Kat.search 'test' }
|
10
|
-
let(:kat_opts) { Kat.search 'test',
|
10
|
+
let(:kat_opts) { Kat.search 'test', category: 'books' }
|
11
11
|
|
12
12
|
describe 'basic search' do
|
13
13
|
it 'returns a full result set' do
|
@@ -33,15 +33,15 @@ describe Kat::Search do
|
|
33
33
|
end
|
34
34
|
|
35
35
|
it 'wont respond to result fields before a search' do
|
36
|
-
%i(titles files).each
|
36
|
+
%i(titles files).each do |s|
|
37
37
|
kat.respond_to?(s).must_equal false
|
38
|
-
|
38
|
+
end
|
39
39
|
end
|
40
40
|
|
41
41
|
it 'responds to result fields after a search' do
|
42
|
-
%i(titles files).each
|
42
|
+
%i(titles files).each do |s|
|
43
43
|
blue_peter.respond_to?(s).must_equal true
|
44
|
-
|
44
|
+
end
|
45
45
|
end
|
46
46
|
|
47
47
|
it 'returns identical result sets' do
|
@@ -56,7 +56,7 @@ describe Kat::Search do
|
|
56
56
|
it 'returns a valid query string with options' do
|
57
57
|
bp = blue_peter.dup
|
58
58
|
bp.options = { user: :foobar }
|
59
|
-
bp.options.must_equal(
|
59
|
+
bp.options.must_equal(user: :foobar)
|
60
60
|
bp.query_str(1).must_equal 'usearch/user:foobar/2/'
|
61
61
|
bp.results.must_be_empty
|
62
62
|
bp.pages.must_equal(-1)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kat
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Fission Xuiptz
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-07-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|
@@ -112,7 +112,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
112
112
|
version: '0'
|
113
113
|
requirements: []
|
114
114
|
rubyforge_project:
|
115
|
-
rubygems_version: 2.2.
|
115
|
+
rubygems_version: 2.2.2
|
116
116
|
signing_key:
|
117
117
|
specification_version: 4
|
118
118
|
summary: Kickass Torrents Interface
|