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 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'