kat 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a6ad7f5809e7cfbdc5379f566d93c61a361a51e6
4
- data.tar.gz: 852186a99d1af3374c4de52f0bb91c45d3c3a5bc
3
+ metadata.gz: 2169e0aa1bb57e770b1f18c28c7e303a1d873f91
4
+ data.tar.gz: ef1a8c23da1ffbf1a178b82a1296bdbd98f42124
5
5
  SHA512:
6
- metadata.gz: cd2896742673ed05ccec2bd1ec63b2af9498911683a9b21e8ad0a7352d8311a2532f7ef8a26960266e4514d9a5aae979762acd710fde36b252159c74cb41d301
7
- data.tar.gz: fc2812d838cc89f103fa9a52e6f2a3ae1345f466d9a732539eb1d26bc6adfa0971ff76961cd7296606aff6b83af9d8fa47639f34b66b2cb17a21905a9477b1a2
6
+ metadata.gz: 63795bf527fc336114b525c5d0de19bab3cb5079c761fd8261383801a1cf204a2e6079cd54f3e9819ea2e464bb4c434785da21ace8b063cd34d29913f91270bc
7
+ data.tar.gz: e6be7512cb382544a0614ec6dfce16bb713326412f72b25f877eaf71519b1eb9f47454d68a4691c19e5246594d33f52d69c4cc8a5164f60c819d117514fd08da
data/.gitignore CHANGED
@@ -4,6 +4,7 @@
4
4
  .config
5
5
  .yardoc
6
6
  Gemfile.lock
7
+ Guardfile
7
8
  InstalledFiles
8
9
  _yardoc
9
10
  coverage
data/Gemfile CHANGED
@@ -5,4 +5,5 @@ gemspec
5
5
 
6
6
  group :test do
7
7
  gem 'rake'
8
+ gem 'minitest', '~> 4.3'
8
9
  end
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
+ [![Build Status](https://secure.travis-ci.org/fissionxuiptz/kat.png)](http://travis-ci.org/fissionxuiptz/kat)
2
+
1
3
  # Kat
2
4
 
3
- TODO: Write a gem description
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
- TODO: Write usage instructions here
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
@@ -5,5 +5,5 @@ task :default => :test
5
5
 
6
6
  Rake::TestTask.new do |t|
7
7
  t.libs.push 'lib'
8
- t.test_files = FileList['test/*_test.rb']
8
+ t.test_files = FileList['test/kat/test_*.rb']
9
9
  end
data/bin/kat CHANGED
@@ -1,127 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- e = []
3
+ require File.dirname(__FILE__) + '/../lib/kat/app'
4
4
 
5
- require 'open-uri'
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 'nokogiri'
2
- require 'open-uri'
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'