kat 1.0.0 → 2.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/.gitignore +1 -0
- data/Gemfile +1 -0
- data/README.md +45 -2
- data/Rakefile +1 -1
- data/bin/kat +2 -124
- data/lib/kat.rb +2 -247
- data/lib/kat/app.rb +276 -0
- data/lib/kat/colour.rb +77 -0
- data/lib/kat/field_map.rb +122 -0
- data/lib/kat/options.rb +59 -0
- data/lib/kat/search.rb +307 -0
- data/lib/kat/version.rb +5 -3
- data/test/kat/test_app.rb +121 -0
- data/test/kat/test_colour.rb +136 -0
- data/test/kat/test_field_map.rb +46 -0
- data/test/kat/test_options.rb +77 -0
- data/test/{kat_test.rb → kat/test_search.rb} +22 -24
- metadata +17 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2169e0aa1bb57e770b1f18c28c7e303a1d873f91
|
4
|
+
data.tar.gz: ef1a8c23da1ffbf1a178b82a1296bdbd98f42124
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 63795bf527fc336114b525c5d0de19bab3cb5079c761fd8261383801a1cf204a2e6079cd54f3e9819ea2e464bb4c434785da21ace8b063cd34d29913f91270bc
|
7
|
+
data.tar.gz: e6be7512cb382544a0614ec6dfce16bb713326412f72b25f877eaf71519b1eb9f47454d68a4691c19e5246594d33f52d69c4cc8a5164f60c819d117514fd08da
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
+
[](http://travis-ci.org/fissionxuiptz/kat)
|
2
|
+
|
1
3
|
# Kat
|
2
4
|
|
3
|
-
|
5
|
+
A Ruby interface to Kickass Torrents
|
4
6
|
|
5
7
|
## Installation
|
6
8
|
|
@@ -18,7 +20,48 @@ Or install it yourself as:
|
|
18
20
|
|
19
21
|
## Usage
|
20
22
|
|
21
|
-
|
23
|
+
### Quick search
|
24
|
+
|
25
|
+
Kat.search('game of thrones')
|
26
|
+
|
27
|
+
### Search for torrents
|
28
|
+
|
29
|
+
kat = Kat.new('game of thrones', { :category => 'tv' })
|
30
|
+
kat.search
|
31
|
+
|
32
|
+
### Specifying pages
|
33
|
+
|
34
|
+
Page searching is 0-based. The number of pages is set after a search is performed and returned
|
35
|
+
with the `pages` method.
|
36
|
+
|
37
|
+
kat.search(2) # Third page of results
|
38
|
+
kat.results[0] # First page of results
|
39
|
+
kat.pages # Total number of pages
|
40
|
+
|
41
|
+
### Results
|
42
|
+
|
43
|
+
The `results` method returns a list of torrent information per page. Each result has
|
44
|
+
title, magnet, download, size, files, age, seeds and leeches information. Complete lists
|
45
|
+
of each can be returned with:
|
46
|
+
|
47
|
+
kat.titles # List all titles...
|
48
|
+
kat.downloads # ...downloads...
|
49
|
+
kat.seeds # ...seeds etc
|
50
|
+
|
51
|
+
### Requerying
|
52
|
+
|
53
|
+
The Kat instance can be reused with the `query=` and `options=` methods.
|
54
|
+
|
55
|
+
kat.query = 'hell on wheels'
|
56
|
+
kat.options = { :seeds => 100 }
|
57
|
+
|
58
|
+
Either method resets the number of pages and the results cache.
|
59
|
+
|
60
|
+
### Executable
|
61
|
+
|
62
|
+
In addition to the Kat class, there is also a binary which makes use of the class to do
|
63
|
+
some rudimentary searching and downloading of torrents. Invoke `kat --help` to get a
|
64
|
+
complete list of options.
|
22
65
|
|
23
66
|
## Contributing
|
24
67
|
|
data/Rakefile
CHANGED
data/bin/kat
CHANGED
@@ -1,127 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
3
|
+
require File.dirname(__FILE__) + '/../lib/kat/app'
|
4
4
|
|
5
|
-
|
6
|
-
%w(kat trollop highline).sort.each do |lib|
|
7
|
-
begin
|
8
|
-
require lib
|
9
|
-
rescue LoadError
|
10
|
-
e << lib.sub(/_/, '')
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
unless e.empty?
|
15
|
-
puts <<-EOS
|
16
|
-
Kickass Torrents Search relies on #{e.join(', ').sub(/^(.*), /, '\1 and ')}. To download the gem#{'s' if e.size > 1} type:
|
17
|
-
|
18
|
-
gem install #{e.join ' '}
|
19
|
-
|
20
|
-
EOS
|
21
|
-
exit
|
22
|
-
end
|
23
|
-
|
24
|
-
VERSION_STR = "#{Kat::NAME} #{Kat::VERSION} (c) 2013 #{Kat::AUTHOR}"
|
25
|
-
|
26
|
-
list_args = { :categories => :category, :times => :added,
|
27
|
-
:languages => :language, :platforms => :platform }
|
28
|
-
|
29
|
-
options = Trollop::options do
|
30
|
-
version VERSION_STR
|
31
|
-
banner <<-EOS
|
32
|
-
#{VERSION_STR}
|
33
|
-
|
34
|
-
Usage: #{File.basename __FILE__} [options] <query>+
|
35
|
-
|
36
|
-
Options:
|
37
|
-
EOS
|
38
|
-
|
39
|
-
opt :exact, 'Exact phrase', :type => :string
|
40
|
-
opt :or, 'Optional words', :type => :string, :multi => true
|
41
|
-
opt :without, 'Without this word', :type => :string, :multi => true
|
42
|
-
|
43
|
-
opt :sort, 'Sort field (size, files, time_add, seeders, leechers)', :type => :string
|
44
|
-
opt :asc, 'Ascending sort order (descending is default)', :type => :boolean
|
45
|
-
|
46
|
-
opt :added, 'Age of the torrent', :type => :string, :short => :a
|
47
|
-
opt :category, 'Category', :type => :string, :short => :c
|
48
|
-
opt :files, 'Number of files', :type => :int
|
49
|
-
opt :imdb, 'IMDB ID', :type => :int
|
50
|
-
opt :seeds, 'Minimum number of seeders', :type => :int, :short => :s
|
51
|
-
opt :user, 'Uploader', :type => :string
|
52
|
-
opt :season, 'Television season', :type => :int
|
53
|
-
opt :episode, 'Television episode', :type => :int, :short => :e
|
54
|
-
|
55
|
-
opt :language, 'Language', :type => :int
|
56
|
-
opt :platform, 'Game platform', :type => :int
|
57
|
-
|
58
|
-
opt :safe, 'Family safety filter', :type => :boolean
|
59
|
-
opt :verified, 'Verified', :type => :boolean, :short => :m
|
60
|
-
|
61
|
-
opt :output, 'Directory to save torrents in', :type => :string, :short => :o
|
62
|
-
|
63
|
-
list_args.each {|k, v| opt k, "List the #{k} that may be used with --#{v}", :type => :boolean }
|
64
|
-
end
|
65
|
-
|
66
|
-
unless list_args.select! {|k, v| options[k] }.empty?
|
67
|
-
puts VERSION_STR
|
68
|
-
list_args.each do |opt, label|
|
69
|
-
args = Kat.send opt
|
70
|
-
puts "\n #{label.to_s.capitalize}"
|
71
|
-
puts unless args.values.first.is_a? Array
|
72
|
-
args.each {|k, v| puts v.is_a?(Array) ? "\n %12s => #{v.join "\n\t\t "}" % k : " %-23s => #{v}" % k }
|
73
|
-
puts
|
74
|
-
end
|
75
|
-
else
|
76
|
-
k = Kat.new ARGV.join(' '), options
|
77
|
-
h = HighLine.new
|
78
|
-
page = 0
|
79
|
-
puts VERSION_STR
|
80
|
-
|
81
|
-
loop do
|
82
|
-
r = k.search page
|
83
|
-
if r.nil?
|
84
|
-
puts "\nNo results"
|
85
|
-
break
|
86
|
-
end
|
87
|
-
|
88
|
-
n = page < k.pages - 1
|
89
|
-
p = page > 0
|
90
|
-
|
91
|
-
puts "\n%-72s S L\n\n" % "Page #{page + 1} of #{k.pages}"
|
92
|
-
r.each_with_index do |t, i|
|
93
|
-
puts "%2d. %-64s %5d %5d" % [ i + 1, t[:title][0..63], t[:seeds], t[:leeches] ]
|
94
|
-
end
|
95
|
-
|
96
|
-
commands = "[#{'n' if n}#{'p' if p}q]|"
|
97
|
-
_01to09 = "[1#{r.size > 9 ? '-9' : '-' + r.size.to_s}]"
|
98
|
-
_10to19 = "#{r.size > 9 ? '|1[0-' + (r.size > 19 ? '9' : (r.size - 10).to_s) + ']' : ''}"
|
99
|
-
_20to25 = "#{r.size > 19 ? '|2[0-' + (r.size - 20).to_s + ']' : ''}"
|
100
|
-
prompt = "\n1#{r.size > 1 ? '-' + r.size.to_s : ''} to download" +
|
101
|
-
"#{', (n)ext' if n}" +
|
102
|
-
"#{', (p)rev' if p}" +
|
103
|
-
', (q)uit: '
|
104
|
-
|
105
|
-
case (answer = h.ask(prompt) {|q| q.validate = /^(#{commands}#{_01to09}#{_10to19}#{_20to25})$/ })
|
106
|
-
when 'q' then break
|
107
|
-
when 'n' then page += 1 if n
|
108
|
-
when 'p' then page -= 1 if p
|
109
|
-
else
|
110
|
-
if (1..r.size).include? answer.to_i
|
111
|
-
torrent = k.results[page][answer.to_i - 1]
|
112
|
-
puts "\nDownloading: #{torrent[:title]}"
|
113
|
-
|
114
|
-
begin
|
115
|
-
uri = URI torrent[:download]
|
116
|
-
uri.query = nil
|
117
|
-
response = uri.read
|
118
|
-
file = "#{File.expand_path(options[:output] || '.')}/#{torrent[:title].gsub(/ /, '.').gsub(/[^a-z0-9()_.-]/i, '')}.torrent"
|
119
|
-
File.open(file, 'w') {|f| f.write response }
|
120
|
-
rescue => e
|
121
|
-
puts e.message
|
122
|
-
end
|
123
|
-
end
|
124
|
-
end
|
125
|
-
end
|
126
|
-
puts
|
127
|
-
end
|
5
|
+
Kat.app
|
data/lib/kat.rb
CHANGED
@@ -1,247 +1,2 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
require 'kat/version'
|
4
|
-
|
5
|
-
class Kat
|
6
|
-
KAT_URL = 'http://kickass.to'
|
7
|
-
EMPTY_URL = 'new'
|
8
|
-
SEARCH_URL = 'usearch'
|
9
|
-
ADVANCED_URL = "#{KAT_URL}/torrents/search/advanced/"
|
10
|
-
|
11
|
-
STRING_FIELDS = [ :seeds, :user, :files, :imdb, :season, :episode ]
|
12
|
-
|
13
|
-
# If these are set to anything but nil or false, they're turned on in the query
|
14
|
-
SWITCH_FIELDS = [ :safe, :verified ]
|
15
|
-
|
16
|
-
# The names of these fields are transposed for ease of use
|
17
|
-
SELECT_FIELDS = [ { :name => :categories, :label => :category, :id => :category },
|
18
|
-
{ :name => :times, :label => :added, :id => :age },
|
19
|
-
{ :name => :languages, :label => :language, :id => :lang_id },
|
20
|
-
{ :name => :platforms, :label => :platform, :id => :platform_id } ]
|
21
|
-
|
22
|
-
SORT_FIELDS = %w(size files_count time_add seeders leechers)
|
23
|
-
|
24
|
-
# The number of pages of results
|
25
|
-
attr_reader :pages
|
26
|
-
|
27
|
-
# Any error in searching is stored here
|
28
|
-
attr_reader :error
|
29
|
-
|
30
|
-
@@doc = nil
|
31
|
-
|
32
|
-
#
|
33
|
-
# Create a new +Kat+ object to search Kickass Torrents.
|
34
|
-
# The search_term can be nil, a string/symbol, or an array of strings/symbols.
|
35
|
-
# Valid options are in STRING_FIELDS, SELECT_FIELDS or SWITCH_FIELDS.
|
36
|
-
#
|
37
|
-
def initialize search_term = nil, opts = {}
|
38
|
-
@search_term = []
|
39
|
-
@options = {}
|
40
|
-
|
41
|
-
self.query = search_term
|
42
|
-
self.options = opts
|
43
|
-
end
|
44
|
-
|
45
|
-
#
|
46
|
-
# Kat.search will do a quick search and return the results
|
47
|
-
#
|
48
|
-
def self.search search_term
|
49
|
-
self.new(search_term).search
|
50
|
-
end
|
51
|
-
|
52
|
-
#
|
53
|
-
# Generate a query string from the stored options, supplying an optional page number
|
54
|
-
#
|
55
|
-
def query_str page = 0
|
56
|
-
str = [ SEARCH_URL, @query.join(' ').gsub(/[^a-z0-9: _-]/i, '') ]
|
57
|
-
str = [ EMPTY_URL ] if str[1].empty?
|
58
|
-
str << page + 1 if page > 0
|
59
|
-
str << if SORT_FIELDS.include? @options[:sort].to_s
|
60
|
-
"?field=#{options[:sort].to_s}&sorder=#{options[:asc] ? 'asc' : 'desc'}"
|
61
|
-
else
|
62
|
-
'' # ensure a trailing slash after the search terms or page number
|
63
|
-
end
|
64
|
-
str.join '/'
|
65
|
-
end
|
66
|
-
|
67
|
-
#
|
68
|
-
# Change the search term, triggering a query rebuild and clearing past results.
|
69
|
-
#
|
70
|
-
# Raises ArgumentError if search_term is not a String, Symbol or Array
|
71
|
-
#
|
72
|
-
def query= search_term
|
73
|
-
@search_term = case search_term
|
74
|
-
when nil then []
|
75
|
-
when String, Symbol then [ search_term ]
|
76
|
-
when Array then search_term.flatten.select {|el| [ String, Symbol ].include? el.class }
|
77
|
-
else raise ArgumentError, "search_term must be a String, Symbol or Array. #{search_term.inspect} given."
|
78
|
-
end
|
79
|
-
build_query
|
80
|
-
end
|
81
|
-
|
82
|
-
#
|
83
|
-
# Get a copy of the search options hash
|
84
|
-
#
|
85
|
-
def options
|
86
|
-
@options.dup
|
87
|
-
end
|
88
|
-
|
89
|
-
#
|
90
|
-
# Change search options with a hash, triggering a query string rebuild and
|
91
|
-
# clearing past results.
|
92
|
-
#
|
93
|
-
# Raises ArgumentError if opts is not a Hash
|
94
|
-
#
|
95
|
-
def options= opts
|
96
|
-
raise ArgumentError, "opts must be a Hash. #{opts.inspect} given." unless opts.is_a? Hash
|
97
|
-
@options.merge! opts
|
98
|
-
build_query
|
99
|
-
end
|
100
|
-
|
101
|
-
#
|
102
|
-
# Perform the search, supplying an optional page number to search on. Returns
|
103
|
-
# a result set limited to the 25 results Kickass Torrents returns itself. Will
|
104
|
-
# cache results for subsequent calls of search with the same query string.
|
105
|
-
#
|
106
|
-
def search page = 0
|
107
|
-
unless @results[page] or (@pages > -1 and page >= @pages)
|
108
|
-
begin
|
109
|
-
doc = Nokogiri::HTML(open("#{KAT_URL}/#{URI::encode(query_str page)}"))
|
110
|
-
@results[page] = doc.css('td.torrentnameCell').map do |node|
|
111
|
-
{ :title => node.css('a.normalgrey').text,
|
112
|
-
:magnet => node.css('a.imagnet').first.attributes['href'].value,
|
113
|
-
:download => node.css('a.idownload').last.attributes['href'].value,
|
114
|
-
:size => (node = node.next_element).text,
|
115
|
-
:files => (node = node.next_element).text.to_i,
|
116
|
-
:age => (node = node.next_element).text,
|
117
|
-
:seeds => (node = node.next_element).text.to_i,
|
118
|
-
:leeches => (node = node.next_element).text.to_i }
|
119
|
-
end
|
120
|
-
|
121
|
-
# If we haven't previously performed a search with this query string, get the
|
122
|
-
# number of pages from the pagination bar at the bottom of the results page.
|
123
|
-
@pages = doc.css('div.pages > a').last.text.to_i if @pages < 0
|
124
|
-
|
125
|
-
# If there was no pagination bar and the previous statement didn't trigger
|
126
|
-
# a NoMethodError, there are results but only 1 page worth.
|
127
|
-
@pages = 1 if @pages <= 0
|
128
|
-
rescue NoMethodError
|
129
|
-
# The results page had no pagination bar, but did return some results.
|
130
|
-
@pages = 1
|
131
|
-
rescue => e
|
132
|
-
# No result throws a 404 error.
|
133
|
-
@pages = 0 if e.class == OpenURI::HTTPError and e.message['404 Not Found']
|
134
|
-
@error = { :error => e, :query => query_str(page) }
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
results[page]
|
139
|
-
end
|
140
|
-
|
141
|
-
#
|
142
|
-
# For message chaining
|
143
|
-
#
|
144
|
-
def do_search page = 0
|
145
|
-
search page
|
146
|
-
self
|
147
|
-
end
|
148
|
-
|
149
|
-
#
|
150
|
-
# Get a copy of the results
|
151
|
-
#
|
152
|
-
def results
|
153
|
-
@results.dup
|
154
|
-
end
|
155
|
-
|
156
|
-
#
|
157
|
-
# If method_sym is a field name in SELECT_FIELDS, we can fetch the list of values.
|
158
|
-
#
|
159
|
-
def self.respond_to? method_sym, include_private = false
|
160
|
-
SELECT_FIELDS.find {|field| field[:name] == method_sym } ? true : super
|
161
|
-
end
|
162
|
-
|
163
|
-
#
|
164
|
-
# If method_sym or its plural is a field name in the results list, this will tell us
|
165
|
-
# if we can fetch the list of values. It'll only happen after a successful search.
|
166
|
-
#
|
167
|
-
def respond_to? method_sym, include_private = false
|
168
|
-
if not (@results.empty? or @results.last.empty?) and
|
169
|
-
(@results.last.first[method_sym] or @results.last.first[method_sym.to_s.chop.to_sym])
|
170
|
-
return true
|
171
|
-
end
|
172
|
-
super
|
173
|
-
end
|
174
|
-
|
175
|
-
private
|
176
|
-
|
177
|
-
#
|
178
|
-
# Clear out the query and rebuild it from the various stored options. Also clears out the
|
179
|
-
# results set and sets pages back to -1
|
180
|
-
#
|
181
|
-
def build_query
|
182
|
-
@query = @search_term.dup
|
183
|
-
@pages = -1
|
184
|
-
@results = []
|
185
|
-
|
186
|
-
@query << "\"#{@options[:exact]}\"" if @options[:exact]
|
187
|
-
@query << @options[:or].join(' OR ') unless @options[:or].nil? or @options[:or].empty?
|
188
|
-
@query += @options[:without].map {|s| "-#{s}" } if @options[:without]
|
189
|
-
|
190
|
-
STRING_FIELDS.each {|f| @query << "#{f}:#{@options[f]}" if @options[f] }
|
191
|
-
SWITCH_FIELDS.each {|f| @query << "#{f}:1" if @options[f] }
|
192
|
-
SELECT_FIELDS.each do |f|
|
193
|
-
if (@options[f[:label]].to_s.to_i > 0 and f[:id].to_s['_id']) or
|
194
|
-
(@options[f[:label]] and not f[:id].to_s['_id'])
|
195
|
-
@query << "#{f[:id]}:#{@options[f[:label]]}"
|
196
|
-
end
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
#
|
201
|
-
# Get a list of options for a particular selection field from the advanced search form
|
202
|
-
#
|
203
|
-
# Raises an error unless the label is in SELECT_FIELDS
|
204
|
-
#
|
205
|
-
def self.field_options label
|
206
|
-
begin
|
207
|
-
raise 'Unknown search field' unless SELECT_FIELDS.find {|f| f[:label] == label.to_sym }
|
208
|
-
|
209
|
-
opts = (@@doc ||= Nokogiri::HTML(open(ADVANCED_URL))).css('table.formtable td').find do |e|
|
210
|
-
e.text[/#{label.to_s}/i]
|
211
|
-
end.next_element.first_element_child.children
|
212
|
-
|
213
|
-
unless (group = opts.css('optgroup')).empty?
|
214
|
-
# Categories
|
215
|
-
group.inject({}) {|c, og| c[og.attributes['label'].value] = og.children.map {|o| o.attributes['value'].value }; c }
|
216
|
-
else
|
217
|
-
# Times, languages, platforms
|
218
|
-
opts.reject {|o| o.attributes.empty? }.inject({}) {|p, o| p[o.text] = o.attributes['value'].value; p }
|
219
|
-
end
|
220
|
-
rescue => e
|
221
|
-
{ :error => e }
|
222
|
-
end
|
223
|
-
end
|
224
|
-
|
225
|
-
#
|
226
|
-
# If method_sym is a field name in SELECT_FIELDS, fetch the list of values.
|
227
|
-
#
|
228
|
-
def self.method_missing method_sym, *args, &block
|
229
|
-
if respond_to? method_sym
|
230
|
-
return self.field_options SELECT_FIELDS.find {|field| field[:name] == method_sym }[:label]
|
231
|
-
end
|
232
|
-
super
|
233
|
-
end
|
234
|
-
|
235
|
-
#
|
236
|
-
# If method_sym or its plural form is a field name in the results list, fetch the list of values.
|
237
|
-
# Can only happen after a successful search.
|
238
|
-
#
|
239
|
-
def method_missing method_sym, *args, &block
|
240
|
-
if respond_to? method_sym
|
241
|
-
# Don't need no fancy schmancy singularizing method. Just try chopping off the 's'.
|
242
|
-
return @results.compact.map {|rs| rs.map {|r| r[method_sym] || r[method_sym.to_s.chop.to_sym] } }.flatten
|
243
|
-
end
|
244
|
-
super
|
245
|
-
end
|
246
|
-
|
247
|
-
end
|
1
|
+
require File.dirname(__FILE__) + '/kat/search'
|
2
|
+
require File.dirname(__FILE__) + '/kat/version'
|