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.
@@ -0,0 +1,276 @@
1
+ require File.dirname(__FILE__) + '/../kat'
2
+ require File.dirname(__FILE__) + '/options'
3
+ require File.dirname(__FILE__) + '/colour'
4
+
5
+ require 'highline'
6
+ require 'yaml'
7
+
8
+ module Kat
9
+
10
+ class << self
11
+ #
12
+ # Convenience method for the App class
13
+ #
14
+ def app(args = ARGV)
15
+ App.new(args).main
16
+ end
17
+ end
18
+
19
+ class App
20
+ MIN_WIDTH = 80
21
+
22
+ # The current page number (0-based)
23
+ attr_accessor :page
24
+
25
+ # The +Kat::Search+ search object
26
+ attr_reader :kat
27
+
28
+ # The app options hash
29
+ attr_reader :options
30
+
31
+ #
32
+ # Create a new +Kat::App+ object, using command-line switches as options by default
33
+ #
34
+ def initialize(args = ARGV)
35
+ @kat = nil
36
+ @page = 0
37
+ @window_width = 0
38
+ @show_info = !hide_info?
39
+ @h = HighLine.new
40
+
41
+ init_options args
42
+ init_search
43
+ end
44
+
45
+ #
46
+ # Initialise the app's options
47
+ #
48
+ def init_options(args = nil)
49
+ @options = {}
50
+ @args = case args
51
+ when nil then []
52
+ when String then args.split
53
+ else args
54
+ end
55
+
56
+ load_config
57
+
58
+ Kat.options(@args).tap { |o|
59
+ @options.merge!(o) { |k, ov, nv| o["#{ k }_given".intern] ? nv : ov }
60
+ }
61
+
62
+ Kat::Colour.colour = @options[:colour]
63
+ end
64
+
65
+ #
66
+ # Initialise the +Kat::Search+ object with the query and it's options
67
+ #
68
+ def init_search
69
+ @kat ||= Kat.search
70
+ @kat.query = @args.join(' ')
71
+ @kat.options = @options
72
+ end
73
+
74
+ #
75
+ # The main method. Prints a list of options for categories, platforms,
76
+ # languages or times if the user has asked for them, otherwise will loop
77
+ # over the running method until the user quits (or there's no results).
78
+ #
79
+ def main
80
+ puts VERSION_STR
81
+
82
+ Kat::Search.selects.select { |k, v| @options[v[:select]] }.tap { |lists|
83
+ if lists.empty?
84
+ while running; end
85
+ else
86
+ puts format_lists lists
87
+ end
88
+ }
89
+ end
90
+
91
+ private
92
+
93
+ #
94
+ # Get the width of the terminal window
95
+ #
96
+ def set_window_width
97
+ @window_width = @h.terminal_size[0]
98
+ end
99
+
100
+ #
101
+ # Hide extra info if window width is 80 chars or less
102
+ #
103
+ def hide_info?
104
+ @window_width < 81
105
+ end
106
+
107
+ #
108
+ # Is there a next page?
109
+ #
110
+ def next?
111
+ @page < @kat.pages - 1
112
+ end
113
+
114
+ #
115
+ # Is there a previous page?
116
+ #
117
+ def prev?
118
+ @page > 0
119
+ end
120
+
121
+ #
122
+ # Do the search, output the results and prompt the user for what to do next.
123
+ # Returns false on error or the user enters 'q', otherwise returns true to
124
+ # signify to the main loop it should run again.
125
+ #
126
+ def running
127
+ puts
128
+ set_window_width
129
+
130
+ searching = true
131
+ [
132
+ -> {
133
+ @kat.search @page
134
+ searching = false
135
+ },
136
+
137
+ -> {
138
+ i = 0
139
+ while searching do
140
+ print "\rSearching...".yellow + '\\|/-'[i % 4]
141
+ i += 1
142
+ sleep 0.1
143
+ end
144
+ }
145
+ ].map { |w| Thread.new { w.call } }.each(&:join)
146
+
147
+ unless @kat.results[@page] && !@kat.error
148
+ puts "\rNo results ".red
149
+ puts @kat.error[:error] if @kat.error
150
+ return false
151
+ end
152
+
153
+ puts format_results
154
+
155
+ case (answer = prompt)
156
+ when 'i' then @show_info = !@show_info
157
+ when 'n' then @page += 1 if next?
158
+ when 'p' then @page -= 1 if prev?
159
+ when 'q' then return false
160
+ else
161
+ if (1..@kat.results[@page].size).include? (answer = answer.to_i)
162
+ print "\nDownloading".yellow <<
163
+ ": #{ @kat.results[@page][answer - 1][:title] }... "
164
+ puts download @kat.results[@page][answer - 1]
165
+ end
166
+ end
167
+
168
+ true
169
+ end
170
+
171
+ #
172
+ # Format a list of options
173
+ #
174
+ def format_lists(lists)
175
+ lists.inject([nil]) { |buf, (k, v)|
176
+ opts = Kat::Search.send(v[:select])
177
+ buf << v[:select].to_s.capitalize
178
+ buf << nil unless Array === opts.values.first
179
+ width = opts.keys.sort { |a, b| b.size <=> a.size }.first.size
180
+ opts.each { |k, v|
181
+ buf += if Array === v
182
+ [nil, "%#{ width }s => #{ v.shift }" % k] +
183
+ v.map { |e| ' ' * (width + 4) + e }
184
+ else
185
+ ["%-#{ width }s => #{ v }" % k]
186
+ end
187
+ }
188
+ buf << nil
189
+ }
190
+ end
191
+
192
+ #
193
+ # Format the list of results with header information
194
+ #
195
+ def format_results
196
+ main_width = @window_width - (!hide_info? || @show_info ? 42 : 4)
197
+
198
+ buf = ["\r%-#{ main_width + 5 }s#{ ' Size Age Seeds Leeches' if !hide_info? || @show_info }" %
199
+ "Page #{ page + 1 } of #{ @kat.pages }", nil].yellow!
200
+
201
+ @kat.results[@page].each_with_index { |t, i|
202
+ age = t[:age].split "\xC2\xA0"
203
+ age = "%3d %-6s" % age
204
+ # Filter out the crap that invariably infests torrent names
205
+ title = t[:title].codepoints.map { |c| c > 31 && c < 127 ? c.chr : '?' }.join[0...main_width]
206
+ buf << ("%2d. %-#{ main_width }s#{ ' %10s %10s %7d %7d' if !hide_info? or @show_info }" %
207
+ [i + 1, title, t[:size], age, t[:seeds], t[:leeches]]).tap { |s| s.red! if t[:seeds] == 0 }
208
+ }
209
+
210
+ buf << nil
211
+ end
212
+
213
+ #
214
+ # Create a regex to validate the user's input
215
+ #
216
+ def validation_regex
217
+ n = @kat.results[@page].size
218
+ commands = "[#{ 'i' if hide_info? }#{ 'n' if next? }#{ 'p' if prev? }q]|"
219
+ _01to09 = "[1-#{ [n, 9].min }]"
220
+ _10to19 = "#{ "|1[0-#{ [n - 10, 9].min }]" if n > 9 }"
221
+ _20to25 = "#{ "|2[0-#{ n - 20 }]" if n > 19 }"
222
+
223
+ /^(#{ commands }#{ _01to09 }#{ _10to19 }#{ _20to25 })$/
224
+ end
225
+
226
+ #
227
+ # Set the prompt after the results list has been printed
228
+ #
229
+ def prompt
230
+ n = @kat.results[@page].size
231
+ @h.ask("1#{ "-#{n}" if n > 1}".cyan(true) << ' to download' <<
232
+ "#{ ', ' << '(n)'.cyan(true) << 'ext' if next? }" <<
233
+ "#{ ', ' << '(p)'.cyan(true) << 'rev' if prev? }" <<
234
+ "#{ ", #{ @show_info ? 'hide' : 'show' } " << '(i)'.cyan(true) << 'nfo' if hide_info? }" <<
235
+ ', ' << '(q)'.cyan(true) << 'uit: ') { |q|
236
+ q.responses[:not_valid] = 'Invalid option.'
237
+ q.validate = validation_regex
238
+ }
239
+ end
240
+
241
+ #
242
+ # Download the torrent to either the output directory or the working directory
243
+ #
244
+ def download(torrent)
245
+ uri = URI(URI::encode torrent[:download])
246
+ uri.query = nil
247
+ file = "#{ @options[:output] || '.' }/" <<
248
+ "#{ torrent[:title].tr(' ', ?.).gsub(/[^a-z0-9()_.-]/i, '') }.torrent"
249
+
250
+ fail '404 File Not Found' if (res = Net::HTTP.start(uri.host) { |http|
251
+ http.get uri
252
+ }).code == '404'
253
+
254
+ File.open(File.expand_path(file), 'w') { |f| f.write res.body }
255
+
256
+ :done.green
257
+ rescue => e
258
+ [:failed, e.message].red
259
+ end
260
+
261
+ #
262
+ # Load options from ~/.katrc if it exists
263
+ #
264
+ def load_config
265
+ config = File.join(ENV['HOME'], '.katrc')
266
+
267
+ @options = (symbolise = -> h {
268
+ Hash === h ? Hash[h.map { |k, v| [k.intern, symbolise[v]] }] : h
269
+ })[YAML.load_file config] if File.readable? config
270
+ rescue => e
271
+ warn "Failed to load #{config}: #{e}"
272
+ end
273
+
274
+ end
275
+
276
+ end
@@ -0,0 +1,77 @@
1
+ module Kat
2
+
3
+ module Colour
4
+
5
+ COLOURS = %w(black red green yellow blue magenta cyan white)
6
+
7
+ class << self
8
+ # From AwesomePrint.colorize? by Michael Dvorkin
9
+ # https://github.com/michaeldv/awesome_print/blob/master/lib/awesome_print/inspector.rb
10
+ def capable?
11
+ STDOUT.tty? && (ENV['TERM'] && ENV['TERM'] != 'dumb' || ENV['ANSICON'])
12
+ end
13
+
14
+ def colour=(f)
15
+ @@colour = f && capable?
16
+ end
17
+
18
+ def colour?
19
+ @@colour
20
+ end
21
+ end
22
+
23
+ @@colour = capable?
24
+
25
+ def colour?
26
+ @@colour
27
+ end
28
+
29
+ COLOURS.each { |c|
30
+ define_method(c) { |*args| colour c, args[0] }
31
+ define_method("#{ c }!") { |*args| colour! c, args[0] }
32
+ }
33
+
34
+ def uncolour
35
+ case self
36
+ when String then gsub /\e\[[0-9;]+?m(.*?)\e\[0m/, '\\1'
37
+ when Array then map { |e| e.uncolour if e }
38
+ else self
39
+ end
40
+ end
41
+
42
+ def uncolour!
43
+ case self
44
+ when String then replace uncolour
45
+ when Array then each { |e| e.uncolour! if e }
46
+ end
47
+
48
+ self
49
+ end
50
+
51
+ private
52
+
53
+ def colour(name, intense = false)
54
+ return case self
55
+ when String, Symbol then "\e[#{ intense ? 1 : 0 };#{ 30 + COLOURS.index(name) }m#{ self }\e[0m"
56
+ when Array then map { |e| e.send name.to_s, intense if e }
57
+ end if colour?
58
+
59
+ self
60
+ end
61
+
62
+ def colour!(name, intense = false)
63
+ case self
64
+ when String then replace send(name.to_s, intense)
65
+ when Array then each { |e| e.send "#{ name.to_s }!", intense if e }
66
+ end if colour?
67
+
68
+ self
69
+ end
70
+
71
+ end
72
+
73
+ end
74
+
75
+ class String; include Kat::Colour end
76
+ class Symbol; include Kat::Colour end
77
+ class Array; include Kat::Colour end
@@ -0,0 +1,122 @@
1
+ require 'yaml'
2
+
3
+ module Kat
4
+
5
+ FIELD_MAP = (symbolise = -> h {
6
+ case h
7
+ when Hash then Hash[h.map { |k, v| [k.to_sym, k == 'desc' ? v : symbolise[v]] }]
8
+ when String then h.to_sym
9
+ else h
10
+ end
11
+ })[YAML.load(<<-FIELD_MAP
12
+ ---
13
+ exact:
14
+ type: string
15
+ desc: Exact phrase
16
+
17
+ or:
18
+ type: string
19
+ desc: Optional words
20
+ multi: true
21
+
22
+ without:
23
+ type: string
24
+ desc: Without this word
25
+ multi: true
26
+
27
+ sort:
28
+ type: string
29
+ desc: Sort field (size, files, added, seeds, leeches)
30
+
31
+ asc:
32
+ desc: Ascending sort order (descending is default)
33
+
34
+ category:
35
+ select: categories
36
+ type: string
37
+ desc: Category
38
+ short: c
39
+
40
+ added:
41
+ select: times
42
+ sort: time_add
43
+ type: string
44
+ desc: Age of the torrent
45
+ id: age
46
+ short: a
47
+
48
+ size:
49
+ sort: size
50
+
51
+ user:
52
+ input: true
53
+ type: string
54
+ desc: Uploader
55
+
56
+ files:
57
+ input: true
58
+ sort: files_count
59
+ type: int
60
+ desc: Number of files
61
+
62
+ imdb:
63
+ input: true
64
+ type: int
65
+ desc: IMDB ID
66
+
67
+ seeds:
68
+ input: true
69
+ sort: seeders
70
+ type: int
71
+ desc: Min no of seeders
72
+ short: s
73
+
74
+ leeches:
75
+ sort: leechers
76
+
77
+ season:
78
+ input: true
79
+ type: int
80
+ desc: Television season
81
+
82
+ episode:
83
+ input: true
84
+ type: int
85
+ desc: Television episode
86
+ short: e
87
+
88
+ language:
89
+ select: languages
90
+ type: int
91
+ desc: Language
92
+ id: lang_id
93
+
94
+ platform:
95
+ select: platforms
96
+ type: int
97
+ desc: Game platform
98
+ id: platform_id
99
+
100
+ safe:
101
+ check: true
102
+ desc: Family safe filter
103
+ short: none
104
+
105
+ verified:
106
+ check: true
107
+ desc: Verified torrent
108
+ short: none
109
+
110
+ output:
111
+ type: string
112
+ desc: Directory to save torrents in
113
+ short: o
114
+
115
+ colour:
116
+ type: boolean
117
+ desc: Output with colour
118
+ short: none
119
+ FIELD_MAP
120
+ )].freeze
121
+
122
+ end