elch_scan 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +112 -0
- data/Rakefile +1 -0
- data/VERSION +1 -0
- data/bin/elch_scan +14 -0
- data/doc/config.yml +44 -0
- data/doc/filter.rb +70 -0
- data/elch_scan.gemspec +29 -0
- data/elch_scan.rb +2 -0
- data/lib/active_support/number_helper/number_converter.rb +182 -0
- data/lib/active_support/number_helper/number_to_delimited_converter.rb +21 -0
- data/lib/active_support/number_helper/number_to_human_size_converter.rb +58 -0
- data/lib/active_support/number_helper/number_to_rounded_converter.rb +92 -0
- data/lib/banana/logger.rb +258 -0
- data/lib/elch_scan/application/dispatch.rb +190 -0
- data/lib/elch_scan/application/filter.rb +34 -0
- data/lib/elch_scan/application.rb +125 -0
- data/lib/elch_scan/formatter/base.rb +17 -0
- data/lib/elch_scan/formatter/html.rb +9 -0
- data/lib/elch_scan/formatter/plain.rb +76 -0
- data/lib/elch_scan/movie.rb +92 -0
- data/lib/elch_scan/version.rb +4 -0
- data/lib/elch_scan.rb +31 -0
- metadata +169 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
module ActiveSupport
|
2
|
+
module NumberHelper
|
3
|
+
class NumberToRoundedConverter < NumberConverter # :nodoc:
|
4
|
+
self.namespace = :precision
|
5
|
+
self.validate_float = true
|
6
|
+
|
7
|
+
def convert
|
8
|
+
precision = options.delete :precision
|
9
|
+
significant = options.delete :significant
|
10
|
+
|
11
|
+
case number
|
12
|
+
when Float, String
|
13
|
+
@number = BigDecimal(number.to_s)
|
14
|
+
when Rational
|
15
|
+
if significant
|
16
|
+
@number = BigDecimal(number, digit_count(number.to_i) + precision)
|
17
|
+
else
|
18
|
+
@number = BigDecimal(number, precision)
|
19
|
+
end
|
20
|
+
else
|
21
|
+
@number = number.to_d
|
22
|
+
end
|
23
|
+
|
24
|
+
if significant && precision > 0
|
25
|
+
digits, rounded_number = digits_and_rounded_number(precision)
|
26
|
+
precision -= digits
|
27
|
+
precision = 0 if precision < 0 # don't let it be negative
|
28
|
+
else
|
29
|
+
rounded_number = number.round(precision)
|
30
|
+
rounded_number = rounded_number.to_i if precision == 0
|
31
|
+
rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros
|
32
|
+
end
|
33
|
+
|
34
|
+
formatted_string =
|
35
|
+
case rounded_number
|
36
|
+
when BigDecimal
|
37
|
+
s = rounded_number.to_s('F') + '0'*precision
|
38
|
+
a, b = s.split('.', 2)
|
39
|
+
a + '.' + b[0, precision]
|
40
|
+
else
|
41
|
+
"%01.#{precision}f" % rounded_number
|
42
|
+
end
|
43
|
+
|
44
|
+
delimited_number = NumberToDelimitedConverter.convert(formatted_string, options)
|
45
|
+
format_number(delimited_number)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def digits_and_rounded_number(precision)
|
51
|
+
if zero?
|
52
|
+
[1, 0]
|
53
|
+
else
|
54
|
+
digits = digit_count(number)
|
55
|
+
multiplier = 10 ** (digits - precision)
|
56
|
+
rounded_number = calculate_rounded_number(multiplier)
|
57
|
+
digits = digit_count(rounded_number) # After rounding, the number of digits may have changed
|
58
|
+
[digits, rounded_number]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def calculate_rounded_number(multiplier)
|
63
|
+
(number / BigDecimal.new(multiplier.to_f.to_s)).round * multiplier
|
64
|
+
end
|
65
|
+
|
66
|
+
def digit_count(number)
|
67
|
+
(Math.log10(absolute_number(number)) + 1).floor
|
68
|
+
end
|
69
|
+
|
70
|
+
def strip_insignificant_zeros
|
71
|
+
options[:strip_insignificant_zeros]
|
72
|
+
end
|
73
|
+
|
74
|
+
def format_number(number)
|
75
|
+
if strip_insignificant_zeros
|
76
|
+
escaped_separator = Regexp.escape(options[:separator])
|
77
|
+
number.sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '')
|
78
|
+
else
|
79
|
+
number
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def absolute_number(number)
|
84
|
+
number.respond_to?(:abs) ? number.abs : number.to_d.abs
|
85
|
+
end
|
86
|
+
|
87
|
+
def zero?
|
88
|
+
number.respond_to?(:zero?) ? number.zero? : number.to_d.zero?
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,258 @@
|
|
1
|
+
module Banana
|
2
|
+
# This class provides a simple logger which maintains and displays the runtime of the logger instance.
|
3
|
+
# It is intended to be used with the colorize gem but it brings its own colorization to eliminate a
|
4
|
+
# dependency. You should initialize the logger as soon as possible to get an accurate runtime.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# logger = Banana::Logger.new
|
8
|
+
# logger.log "foo"
|
9
|
+
# logger.prefix = "[a] "
|
10
|
+
# logger.debug "bar"
|
11
|
+
# sleep 2
|
12
|
+
# logger.ensure_prefix("[b] ") { logger.warn "baz" }
|
13
|
+
# logger.log("rab!", :abort)
|
14
|
+
#
|
15
|
+
# # Result:
|
16
|
+
# # [00:00:00.000 INFO] foo
|
17
|
+
# # [00:00:00.000 DEBUG] [a] bar
|
18
|
+
# # [00:00:02.001 WARN] [b] baz
|
19
|
+
# # [00:00:02.001 ABORT] [a] rab!
|
20
|
+
#
|
21
|
+
# @!attribute [r] startup
|
22
|
+
# @return [DateTime] Point of time where the logger was initiated.
|
23
|
+
# @!attribute [r] channel
|
24
|
+
# @return [Object] The object where messages are getting send to.
|
25
|
+
# @!attribute [r] method
|
26
|
+
# @return [Object] The method used to send messages to {#channel}.
|
27
|
+
# @!attribute [r] logged
|
28
|
+
# @return [Integer] Amount of messages send to channel.
|
29
|
+
# @note This counter starts from 0 in every {#ensure_method} or {#log_with_print} block but gets
|
30
|
+
# added to the main counter after the call.
|
31
|
+
# @!attribute timestr
|
32
|
+
# @return [Boolean] Set to false if the runtime indicator should not be printed (default: true).
|
33
|
+
# @!attribute colorize
|
34
|
+
# @return [Boolean] Set to false if output should not be colored (default: true).
|
35
|
+
# @!attribute prefix
|
36
|
+
# @return [String] Current prefix string for logged messages.
|
37
|
+
class Logger
|
38
|
+
attr_reader :startup, :channel, :method, :logged
|
39
|
+
attr_accessor :colorize, :prefix
|
40
|
+
|
41
|
+
# Foreground color values
|
42
|
+
COLORMAP = {
|
43
|
+
black: 30,
|
44
|
+
red: 31,
|
45
|
+
green: 32,
|
46
|
+
yellow: 33,
|
47
|
+
blue: 34,
|
48
|
+
magenta: 35,
|
49
|
+
cyan: 36,
|
50
|
+
white: 37,
|
51
|
+
}
|
52
|
+
|
53
|
+
# Initializes a new logger instance. The internal runtime measurement starts here!
|
54
|
+
#
|
55
|
+
# There are 4 default log levels: info (yellow), warn & abort (red) and debug (blue).
|
56
|
+
# All are enabled by default. You propably want to {#disable disable(:debug)}.
|
57
|
+
#
|
58
|
+
# @param scope Only for backward compatibility, not used
|
59
|
+
# @todo add option hash
|
60
|
+
def initialize scope = nil
|
61
|
+
@startup = Time.now.utc
|
62
|
+
@colorize = true
|
63
|
+
@prefix = ""
|
64
|
+
@enabled = true
|
65
|
+
@timestr = true
|
66
|
+
@channel = ::Kernel
|
67
|
+
@method = :puts
|
68
|
+
@logged = 0
|
69
|
+
@levels = {
|
70
|
+
info: { color: "yellow", enabled: true },
|
71
|
+
warn: { color: "red", enabled: true },
|
72
|
+
abort: { color: "red", enabled: true },
|
73
|
+
debug: { color: "blue", enabled: true },
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
# The default channel is `Kernel` which is Ruby's normal `puts`.
|
78
|
+
# Attach it to a open file with write access and it will write into
|
79
|
+
# the file. Ensure to close the file in your code.
|
80
|
+
#
|
81
|
+
# @param channel Object which responds to puts and print
|
82
|
+
def attach channel
|
83
|
+
@channel = channel
|
84
|
+
end
|
85
|
+
|
86
|
+
# Print raw message onto {#channel} using {#method}.
|
87
|
+
#
|
88
|
+
# @param [String] msg Message to send to {#channel}.
|
89
|
+
# @param [Symbol] method Override {#method}.
|
90
|
+
def raw msg, method = @method
|
91
|
+
@channel.send(method, msg)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Add additional log levels which are automatically enabled.
|
95
|
+
# @param [Hash] levels Log levels in the format `{ debug: "red" }`
|
96
|
+
def log_level levels = {}
|
97
|
+
levels.each do |level, color|
|
98
|
+
@levels[level.to_sym] ||= { enabled: true }
|
99
|
+
@levels[level.to_sym][:color] = color
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Calls the block with the given prefix and ensures that the prefix
|
104
|
+
# will be the same as before.
|
105
|
+
#
|
106
|
+
# @param [String] prefix Prefix to use for the block
|
107
|
+
# @param [Proc] block Block to call
|
108
|
+
def ensure_prefix prefix, &block
|
109
|
+
old_prefix, @prefix = @prefix, prefix
|
110
|
+
block.call
|
111
|
+
ensure
|
112
|
+
@prefix = old_prefix
|
113
|
+
end
|
114
|
+
|
115
|
+
# Calls the block after changing the output method.
|
116
|
+
# It also ensures to set back the method to what it was before.
|
117
|
+
#
|
118
|
+
# @param [Symbol] method Method to call on {#channel}
|
119
|
+
def ensure_method method, &block
|
120
|
+
old_method, old_logged = @method, @logged
|
121
|
+
@method, @logged = method, 0
|
122
|
+
block.call
|
123
|
+
ensure
|
124
|
+
@method = old_method
|
125
|
+
@logged += old_logged
|
126
|
+
end
|
127
|
+
|
128
|
+
# Calls the block after changing the output method to `:print`.
|
129
|
+
# It also ensures to set back the method to what it was before.
|
130
|
+
#
|
131
|
+
# @param [Boolean] clear If set to true and any message was printed inside the block
|
132
|
+
# a \n newline character will be printed.
|
133
|
+
def log_with_print clear = true, &block
|
134
|
+
ensure_method :print do
|
135
|
+
begin
|
136
|
+
block.call
|
137
|
+
ensure
|
138
|
+
puts if clear && @logged > 0
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Calls the block after disabling the runtime indicator.
|
144
|
+
# It also ensures to set back the old setting after execution.
|
145
|
+
def log_without_timestr &block
|
146
|
+
timestr, @timestr = @timestr, false
|
147
|
+
block.call
|
148
|
+
ensure
|
149
|
+
@timestr = timestr
|
150
|
+
end
|
151
|
+
|
152
|
+
# @return [Boolean] returns true if the log level format :debug is enabled.
|
153
|
+
def debug?
|
154
|
+
enabled? :debug
|
155
|
+
end
|
156
|
+
|
157
|
+
# If a `level` is provided it will return true if this log level is enabled.
|
158
|
+
# If no `level` is provided it will return true if the logger is enabled generally.
|
159
|
+
#
|
160
|
+
# @return [Boolean] returns true if the given log level is enabled
|
161
|
+
def enabled? level = nil
|
162
|
+
level.nil? ? @enabled : @levels[level.to_sym][:enabled]
|
163
|
+
end
|
164
|
+
|
165
|
+
# Same as {#enabled?} just negated.
|
166
|
+
def disabled? level = nil
|
167
|
+
!enabled?(level)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Same as {#enable} just negated.
|
171
|
+
#
|
172
|
+
# @param [Symbol, String] level Loglevel to disable.
|
173
|
+
def disable level = nil
|
174
|
+
if level.nil?
|
175
|
+
@enabled = false
|
176
|
+
else
|
177
|
+
@levels[level.to_sym][:enabled] = false
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Enables the given `level` or the logger generally if no `level` is given.
|
182
|
+
# If the logger is disabled no messages will be printed. If it is enabled
|
183
|
+
# only messages for enabled log levels will be printed.
|
184
|
+
#
|
185
|
+
# @param [Symbol, String] level Loglevel to enable.
|
186
|
+
def enable level = nil
|
187
|
+
if level.nil?
|
188
|
+
@enabled = true
|
189
|
+
else
|
190
|
+
@levels[level.to_sym][:enabled] = true
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# Colorizes the given string with the given color. It uses the build-in
|
195
|
+
# colorization feature. You may want to use the colorize gem.
|
196
|
+
#
|
197
|
+
# @param [String] str The string to colorize
|
198
|
+
# @param [Symbol, String] color The color to use (see {COLORMAP})
|
199
|
+
# @raise [ArgumentError] if color does not exist in {COLORMAP}.
|
200
|
+
def colorize str, color
|
201
|
+
ccode = COLORMAP[color.to_sym] || raise(ArgumentError, "Unknown color #{color}!")
|
202
|
+
"\e[#{ccode}m#{str}\e[0m"
|
203
|
+
end
|
204
|
+
|
205
|
+
def colorize?
|
206
|
+
@colorize
|
207
|
+
end
|
208
|
+
|
209
|
+
# This method is the only method which sends the message `msg` to `@channel` via `@method`.
|
210
|
+
# It also increments the message counter `@logged` by one.
|
211
|
+
#
|
212
|
+
# This method instantly returns nil if the logger or the given log level `type` is disabled.
|
213
|
+
#
|
214
|
+
# @param [String] msg The message to send to the channel
|
215
|
+
# @param [Symbol] type The log level
|
216
|
+
def log msg, type = :info
|
217
|
+
return if !@enabled || !@levels[type][:enabled]
|
218
|
+
if @levels[type.to_sym] || !@levels.key?(type.to_sym)
|
219
|
+
time = Time.at(Time.now.utc - @startup).utc
|
220
|
+
timestr = @timestr ? "[#{time.strftime("%H:%M:%S.%L")} #{type.to_s.upcase}]\t" : ""
|
221
|
+
|
222
|
+
if @colorize
|
223
|
+
msg = "#{colorize(timestr, @levels[type.to_sym][:color])}" <<
|
224
|
+
"#{@prefix}" <<
|
225
|
+
"#{colorize(msg, @levels[type.to_sym][:color])}"
|
226
|
+
else
|
227
|
+
msg = "#{timestr}#{@prefix}#{msg}"
|
228
|
+
end
|
229
|
+
@logged += 1
|
230
|
+
@channel.send(@method, msg)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
alias_method :info, :log
|
234
|
+
|
235
|
+
# Shortcut for {#log #log(msg, :debug)}
|
236
|
+
#
|
237
|
+
# @param [String] msg The message to send to {#log}.
|
238
|
+
def debug msg
|
239
|
+
log(msg, :debug)
|
240
|
+
end
|
241
|
+
|
242
|
+
# Shortcut for {#log #log(msg, :warn)} but sets channel method to "warn".
|
243
|
+
#
|
244
|
+
# @param [String] msg The message to send to {#log}.
|
245
|
+
def warn msg
|
246
|
+
ensure_method(:warn) { log(msg, :warn) }
|
247
|
+
end
|
248
|
+
|
249
|
+
# Shortcut for {#log #log(msg, :abort)} but sets channel method to "warn".
|
250
|
+
#
|
251
|
+
# @param [String] msg The message to send to {#log}.
|
252
|
+
# @param [Integer] exit_code Exits with given code or does nothing when falsy.
|
253
|
+
def abort msg, exit_code = false
|
254
|
+
ensure_method(:warn) { log(msg, :abort) }
|
255
|
+
exit(exit_code) if exit_code
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
module ElchScan
|
2
|
+
class Application
|
3
|
+
module Dispatch
|
4
|
+
def dispatch action = (@opts[:dispatch] || :help)
|
5
|
+
case action
|
6
|
+
when :help then @optparse.to_s.split("\n").each(&method(:log))
|
7
|
+
when :version, :info then dispatch_info
|
8
|
+
else
|
9
|
+
if respond_to?("dispatch_#{action}")
|
10
|
+
send("dispatch_#{action}")
|
11
|
+
else
|
12
|
+
abort("unknown action #{action}", 1)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def dispatch_generate_config
|
18
|
+
if File.exist?(@config_src)
|
19
|
+
log "Configuration already exists, please delete it do generate another one."
|
20
|
+
else
|
21
|
+
FileUtils.cp("#{ROOT}/doc/config.yml", @config_src)
|
22
|
+
log "Sample configuration created!"
|
23
|
+
log "You will need to add at least 1 one movie directory."
|
24
|
+
answer = ask("Do you want to open the configuration file now? [Yes/no]")
|
25
|
+
if ["", "y", "yes"].include?(answer.downcase)
|
26
|
+
if RUBY_PLATFORM.include?("darwin")
|
27
|
+
exec("open #{@config_src}")
|
28
|
+
else
|
29
|
+
system "#{cfg :application, :editor} #{@config_src}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def dispatch_info
|
36
|
+
logger.log_without_timestr do
|
37
|
+
log ""
|
38
|
+
log " Your version: #{your_version = Gem::Version.new(ElchScan::VERSION)}"
|
39
|
+
|
40
|
+
# get current version
|
41
|
+
logger.log_with_print do
|
42
|
+
log " Current version: "
|
43
|
+
if cfg("application.check_version")
|
44
|
+
require "net/http"
|
45
|
+
log c("checking...", :blue)
|
46
|
+
|
47
|
+
begin
|
48
|
+
current_version = Gem::Version.new Net::HTTP.get_response(URI.parse(ElchScan::UPDATE_URL)).body.strip
|
49
|
+
|
50
|
+
if current_version > your_version
|
51
|
+
status = c("#{current_version} (consider update)", :red)
|
52
|
+
elsif current_version < your_version
|
53
|
+
status = c("#{current_version} (ahead, beta)", :green)
|
54
|
+
else
|
55
|
+
status = c("#{current_version} (up2date)", :green)
|
56
|
+
end
|
57
|
+
rescue
|
58
|
+
status = c("failed (#{$!.message})", :red)
|
59
|
+
end
|
60
|
+
|
61
|
+
logger.raw "#{"\b" * 11}#{" " * 11}#{"\b" * 11}", :print # reset cursor
|
62
|
+
log status
|
63
|
+
else
|
64
|
+
log c("check disabled", :red)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# more info
|
69
|
+
log ""
|
70
|
+
log " ElchScan is brought to you by #{c "bmonkeys.net", :green}"
|
71
|
+
log " Contribute @ #{c "github.com/2called-chaos/elch_scan", :cyan}"
|
72
|
+
log " Eat bananas every day!"
|
73
|
+
log ""
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def dispatch_edit_script
|
78
|
+
record_filter(filter_script(@opts[:select_script]))
|
79
|
+
end
|
80
|
+
|
81
|
+
def dispatch_index
|
82
|
+
if cfg(:movies).empty?
|
83
|
+
log "You will need at least 1 one movie directory defined in your configuration."
|
84
|
+
answer = ask("Do you want to open the file now? [Yes/no]")
|
85
|
+
if ["", "y", "yes"].include?(answer.downcase)
|
86
|
+
if RUBY_PLATFORM.include?("darwin")
|
87
|
+
exec("open #{@config_src}")
|
88
|
+
else
|
89
|
+
system "#{cfg :application, :editor} #{@config_src}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
else
|
93
|
+
movies = _index_movies(cfg(:movies))
|
94
|
+
old_count = movies.count
|
95
|
+
return log("No movies found :(") if old_count.zero?
|
96
|
+
log(
|
97
|
+
"We have found " << c("#{movies.count}", :magenta) <<
|
98
|
+
c(" movies in ") << c("#{cfg(:movies).count}", :magenta) << c(" directories")
|
99
|
+
)
|
100
|
+
|
101
|
+
if @opts[:console]
|
102
|
+
log "You have access to the collection with " << c("movies", :magenta)
|
103
|
+
log "Apply existent select script with " << c("apply_filter(movies, 'filter_name')", :magenta)
|
104
|
+
log "Type " << c("exit", :magenta) << c(" to leave the console.")
|
105
|
+
binding.pry(quiet: true)
|
106
|
+
else
|
107
|
+
# ask to filter
|
108
|
+
if !@opts[:quiet] && @opts[:select_scripts].empty?
|
109
|
+
answer = ask("Do you want to filter the results? [yes/No]")
|
110
|
+
if ["y", "yes"].include?(answer.downcase)
|
111
|
+
movies = apply_filter(movies, record_filter)
|
112
|
+
old_count = movies.count
|
113
|
+
collection_size_changed old_count, movies.count, "custom filter"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# filter
|
118
|
+
@opts[:select_scripts].each do |filter|
|
119
|
+
movies = apply_filter(movies, filter_script(filter))
|
120
|
+
collection_size_changed old_count, movies.count, "filter: #{filter}"
|
121
|
+
old_count = movies.count
|
122
|
+
end
|
123
|
+
|
124
|
+
# permute
|
125
|
+
permute_script(movies) if @opts[:permute]
|
126
|
+
|
127
|
+
# ask to save
|
128
|
+
if !@opts[:quiet] && !@opts[:output_file]
|
129
|
+
answer = ask("Enter filename to save output or leave blank to print to STDOUT:")
|
130
|
+
if !answer.empty?
|
131
|
+
@opts[:output_file] = answer
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# format results
|
136
|
+
begin
|
137
|
+
formatter = "ElchScan::Formatter::#{@opts[:formatter]}".constantize.new(self)
|
138
|
+
rescue LoadError
|
139
|
+
warn "Unknown formatter " << c("#{@opts[:formatter]}", :magenta) << c(", using Plain...", :red)
|
140
|
+
formatter = "ElchScan::Formatter::Plain".constantize.new(self)
|
141
|
+
end
|
142
|
+
results = formatter.format(movies)
|
143
|
+
|
144
|
+
# save
|
145
|
+
if @opts[:output_file]
|
146
|
+
File.open(@opts[:output_file], "w+") {|f| f.write(results.join("\n")) }
|
147
|
+
else
|
148
|
+
logger.log_without_timestr do
|
149
|
+
results.each {|line| log line }
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def collection_size_changed cold, cnew, reason = nil
|
157
|
+
if cold != cnew
|
158
|
+
log(
|
159
|
+
"We have filtered " << c("#{cnew}", :magenta) <<
|
160
|
+
c(" movies") << c(" (#{cnew - cold})", :red) <<
|
161
|
+
c(" from originally ") << c("#{cold}", :magenta) <<
|
162
|
+
(reason ? c(" (#{reason})", :blue) : "")
|
163
|
+
)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def _index_movies directories
|
168
|
+
directories.each_with_object({}) do |dir, result|
|
169
|
+
if FileTest.directory?(dir)
|
170
|
+
Find.find(dir) do |path|
|
171
|
+
# calculate depth
|
172
|
+
depth = path.scan(?/).count - dir.scan(?/).count
|
173
|
+
depth -= 1 unless File.directory?(path)
|
174
|
+
|
175
|
+
if depth == 1 && File.directory?(path)
|
176
|
+
result[File.basename(path)] = Movie.new(
|
177
|
+
self,
|
178
|
+
dir: dir,
|
179
|
+
path: path,
|
180
|
+
)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
else
|
184
|
+
log(c("Directory unreadable, skipping ", :red) << c("#{dir}", :magenta))
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module ElchScan
|
2
|
+
module Filter
|
3
|
+
def filter_script name
|
4
|
+
"#{ROOT}/tmp/#{name}.esss"
|
5
|
+
end
|
6
|
+
|
7
|
+
def apply_filter collection, file
|
8
|
+
app = @app
|
9
|
+
eval File.read(file), binding, file
|
10
|
+
collection
|
11
|
+
end
|
12
|
+
|
13
|
+
def permute_script collection, file = nil
|
14
|
+
file ||= "#{Dir.tmpdir}/#{SecureRandom.urlsafe_base64}"
|
15
|
+
FileUtils.mkdir(File.dirname(file)) if !File.exist?(File.dirname(file))
|
16
|
+
if !File.exist?(file) || File.read(file).strip.empty?
|
17
|
+
File.open(file, "w") {|f| f.puts("# Permute your collection, same as with the selector script filters.") }
|
18
|
+
end
|
19
|
+
system "#{cfg :application, :editor} #{file}"
|
20
|
+
eval File.read(file), binding, file
|
21
|
+
collection
|
22
|
+
end
|
23
|
+
|
24
|
+
def record_filter file = nil
|
25
|
+
file ||= "#{Dir.tmpdir}/#{SecureRandom.urlsafe_base64}"
|
26
|
+
FileUtils.mkdir(File.dirname(file)) if !File.exist?(File.dirname(file))
|
27
|
+
if !File.exist?(file) || File.read(file).strip.empty?
|
28
|
+
FileUtils.cp("#{ROOT}/doc/filter.rb", file)
|
29
|
+
end
|
30
|
+
system "#{cfg :application, :editor} #{file}"
|
31
|
+
file
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module ElchScan
|
2
|
+
# Logger Singleton
|
3
|
+
MAIN_THREAD = ::Thread.main
|
4
|
+
def MAIN_THREAD.app_logger
|
5
|
+
MAIN_THREAD[:app_logger] ||= Banana::Logger.new
|
6
|
+
end
|
7
|
+
|
8
|
+
class Application
|
9
|
+
include Dispatch
|
10
|
+
include Filter
|
11
|
+
|
12
|
+
# =========
|
13
|
+
# = Setup =
|
14
|
+
# =========
|
15
|
+
def self.dispatch *a
|
16
|
+
new(*a) do |app|
|
17
|
+
app.load_config "~/.elch_scan.yml"
|
18
|
+
app.apply_config
|
19
|
+
app.parse_params
|
20
|
+
app.dispatch
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize env, argv
|
25
|
+
@env, @argv = env, argv
|
26
|
+
@opts = {
|
27
|
+
dispatch: :index,
|
28
|
+
quiet: false,
|
29
|
+
output_file: nil,
|
30
|
+
formatter: "Plain",
|
31
|
+
select_scripts: [],
|
32
|
+
}
|
33
|
+
yield(self)
|
34
|
+
end
|
35
|
+
|
36
|
+
def parse_params
|
37
|
+
@optparse = OptionParser.new do |opts|
|
38
|
+
opts.banner = "Usage: elch_scan [options]"
|
39
|
+
|
40
|
+
opts.on("--generate-config", "Generate sample configuration file in ~/.elch_scan.yml") { @opts[:dispatch] = :generate_config }
|
41
|
+
opts.on("-h", "--help", "Shows this help") { @opts[:dispatch] = :help }
|
42
|
+
opts.on("-v", "--version", "Shows version and other info") { @opts[:dispatch] = :info }
|
43
|
+
opts.on("-f", "--formatter HTML", "Use formatter") {|f| @opts[:formatter] = f }
|
44
|
+
opts.on("-o", "--output FILENAME", "Write formatted results to file") {|f| @opts[:output_file] = f }
|
45
|
+
opts.on("-e", "--edit SELECT_SCRIPT", "Edit selector script") {|s| @opts[:dispatch] = :edit_script; @opts[:select_script] = s }
|
46
|
+
opts.on("-s", "--select [WITH_SUBS,NO_NFO]", Array, "Filter movies with saved selector scripts") {|s| @opts[:select_scripts] = s }
|
47
|
+
opts.on("-p", "--permute", "Open editor to write permutation code for collection") {|s| @opts[:permute] = true }
|
48
|
+
opts.on("-q", "--quiet", "Don't ask to filter or save results") { @opts[:quiet] = true }
|
49
|
+
opts.on("-c", "--console", "Start console to play around with the collection") {|f| @opts[:console] = true }
|
50
|
+
end
|
51
|
+
|
52
|
+
begin
|
53
|
+
@optparse.parse!(@argv)
|
54
|
+
rescue OptionParser::ParseError => e
|
55
|
+
abort(e.message)
|
56
|
+
dispatch(:help)
|
57
|
+
exit 1
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def load_config file
|
62
|
+
@config_src = File.expand_path(file)
|
63
|
+
@config = YAML.load_file(@config_src)
|
64
|
+
raise "empty config" if !@config || @config.empty? || !@config.is_a?(Hash)
|
65
|
+
@config = @config.with_indifferent_access
|
66
|
+
rescue Exception => e
|
67
|
+
if e.message =~ /no such file or directory/i
|
68
|
+
if @argv.include?("--generate-config")
|
69
|
+
@config = { application: { logger: { colorize: true } } }.with_indifferent_access
|
70
|
+
else
|
71
|
+
log "Please create or generate a configuration file."
|
72
|
+
log(
|
73
|
+
c("Use ") << c("elch_scan --generate-config", :magenta) <<
|
74
|
+
c(" or create ") << c("~/.elch_scan.yml", :magenta) << c(" manually.")
|
75
|
+
)
|
76
|
+
abort "No configuration file found.", 1
|
77
|
+
end
|
78
|
+
elsif e.message =~ //i
|
79
|
+
abort "Configuration file is invalid.", 1
|
80
|
+
else
|
81
|
+
raise
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def apply_config
|
86
|
+
logger.colorize = cfg(:application, :logger, :colorize)
|
87
|
+
(cfg(:formatters) || []).each do |f|
|
88
|
+
begin
|
89
|
+
require File.expand_path(f)
|
90
|
+
rescue LoadError
|
91
|
+
abort "The custom formatter file wasn't found: " << c("#{f}", :magenta)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def cfg *keys
|
97
|
+
keys = keys.flatten.join(".").split(".")
|
98
|
+
keys.inject(@config) {|cfg, skey| cfg.try(:[], skey) }
|
99
|
+
end
|
100
|
+
|
101
|
+
# ==========
|
102
|
+
# = Logger =
|
103
|
+
# ==========
|
104
|
+
[:log, :warn, :abort, :debug].each do |meth|
|
105
|
+
define_method meth, ->(*a, &b) { Thread.main.app_logger.send(meth, *a, &b) }
|
106
|
+
end
|
107
|
+
|
108
|
+
def logger
|
109
|
+
Thread.main.app_logger
|
110
|
+
end
|
111
|
+
|
112
|
+
# Shortcut for logger.colorize
|
113
|
+
def c str, color = :yellow
|
114
|
+
logger.colorize? ? logger.colorize(str, color) : str
|
115
|
+
end
|
116
|
+
|
117
|
+
def ask question
|
118
|
+
logger.log_with_print(false) do
|
119
|
+
log c("#{question} ", :blue)
|
120
|
+
STDOUT.flush
|
121
|
+
gets.chomp
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|