sequin 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ test
data/CHANGELOG ADDED
@@ -0,0 +1,5 @@
1
+ v2.1. newest change
2
+
3
+ v2. older change
4
+
5
+ v1.9. oldest change
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in sequin.gemspec
4
+ gemspec
data/LICENSE ADDED
File without changes
data/README.md ADDED
@@ -0,0 +1,210 @@
1
+
2
+
3
+ Sequin
4
+ ======
5
+
6
+ Description
7
+ --------
8
+
9
+ Sequin is a collection of thor tasks that allow you to list, test and modify sequences of files on disk.
10
+
11
+ Installation
12
+ --------
13
+
14
+ $ gem install sequin
15
+
16
+ or
17
+
18
+ $ sudo gem install sequin
19
+
20
+ Usage
21
+ --------
22
+
23
+ The command is seq, followed by a task:
24
+
25
+ Tasks
26
+
27
+ * seq list [FILES] # List files as sequences.
28
+ * seq match [FILES] -r, --regex=REGEX # Match filename naming conventions using a regular expression.
29
+ * seq check [SIGNATURE] # Check the existence of files defined by a sequence signature.
30
+ * seq generate [NAME] # Generate a seq config file containing regular expression definitions used in the seq match task.
31
+ * seq help [TASK] # Describe available tasks or one specific task.
32
+ * seq usage # Print the usage banner.
33
+
34
+ All tasks have a single letter alias. Use m instead of match, c instead of check etc.
35
+
36
+
37
+ ### List Sequences
38
+
39
+ seq list [FILES]
40
+
41
+ List the given files as sequences where possible.
42
+
43
+ Options:
44
+
45
+ * -v, [--verbose=N] Specify how verbose to be when listing missing frames.
46
+
47
+ * 0 Dont mention missing frames
48
+ * 1 show missing frame count
49
+ * 2 list missing frame numbers
50
+ * Default: 1
51
+
52
+ Each line of output from this task is a **sequence signature.** It consists of a **filename template**: a filename where the sequence digits replaced with "#"s, followed by a **range spec**: start, end, and by frames in square brackets.
53
+
54
+ For example: `../test/a/op_063_8900.####.jpg [0 to 25 by 1]`
55
+
56
+
57
+
58
+ If any frames are missing, the range spec will be colored red in the output, as will other information about the missing frames according to the verbose options.
59
+
60
+ $ seq l * -v 2
61
+ h264_960x540_film.mov
62
+ mjpega_1920x1080.mov
63
+ #### [1 to 30 by 1]
64
+ 006_057_s3d_lf_r04.####.dpx [1 to 30 by 1]
65
+ 006_057_s3d_lf_r05.#.dpx [1 to 30 by 1] - missing: [ 11,14,15,18,22 ]
66
+ 006_057_s3d_lf_r06.#.dpx [1 to 30 by 1]
67
+ 008_055_s3d_lf_r04.####.dpx [1 to 50 by 1]
68
+ fn_by_three.#.jpg [1 to 25 by 3] - missing: [ 19 ]
69
+ fn_by_two.#.jpg [1 to 29 by 2]
70
+
71
+ $ seq l 006* -v 1
72
+ 006_057_s3d_lf_r04.####.dpx [1 to 30 by 1]
73
+ 006_057_s3d_lf_r05.#.dpx [1 to 30 by 1] - missing: [ 5 frames ]
74
+ 006_057_s3d_lf_r06.#.dpx [1 to 30 by 1]
75
+
76
+
77
+ ### Match
78
+
79
+ seq match [FILES] -r, --regex=REGEX
80
+
81
+ Make sure filenames conform to file naming conventions by testing them against a regular expression.
82
+
83
+ Options:
84
+
85
+ * -r, --regex=REGEX # Specify a regular expression literally, or by reference to one listed in ~/.seq.yml
86
+ * -m, [--show-matched] # List files that match the regex, in green
87
+ * -f, [--show-failed] # List files that failed to match the regex, in red
88
+ * -a, [--show-all] # List all the files in order with color coding.
89
+
90
+ Normally you wouldn't want to type lengthy regular expressions on the command line, so instead they can be stored in a yaml file and referred to by the keys in the yaml data structure. The keys would typically represent projects and filetypes.
91
+
92
+ The yaml file looks something like this:
93
+
94
+ potter:
95
+ dpx: ^[0-9]{3}_[0-9]{3}_s3d_(rt|lf)_r[0-9]{2}\.[0-9]{4}\.dpx$
96
+ mcx: ^[0-9]{3}_[0-9]{3}_ver[0-9]{2,3}\.[0-9]{4}\.xml$
97
+ conan:
98
+ dpx: ^[a-z]{2}_[0-9]{3}_[0-9]{4}_(L|R|C)\.[0-9]{4,5}\.dpx$
99
+ mov: ^[a-z]{2}_h264_[0-9]{3,4}x[0-9]{3,4}_(film|pal|ntsc)\.mov$
100
+
101
+ The regular expression can then be referred to on the command line with `--regex potter:dpx` and so on.
102
+
103
+ $ seq m * -r potter:dpx
104
+ Testing against regular expression: /^[0-9]{3}_[0-9]{3}_s3d_(rt|lf)_r[0-9]{2}\.[0-9]{4}\.dpx$/
105
+ h264_960x540_film.mov FAILED
106
+ mjpega_1920x1080.mov FAILED
107
+ #### [1 to 30 by 1] (0 MATCHED, 30 FAILED)
108
+ 006_057_s3d_lf_r04.####.dpx [1 to 30 by 1] (30 MATCHED)
109
+ 006_057_s3d_lf_r05.#.dpx [1 to 30 by 1] (0 MATCHED, 25 FAILED)
110
+ 006_057_s3d_lf_r06.#.dpx [1 to 30 by 1] (0 MATCHED, 30 FAILED)
111
+ 008_055_s3d_lf_r04.####.dpx [1 to 50 by 1] (50 MATCHED)
112
+ fn_by_three.#.jpg [1 to 28 by 3] (0 MATCHED, 10 FAILED)
113
+ fn_by_two.#.jpg [1 to 29 by 2] (0 MATCHED, 15 FAILED)
114
+ op_063_8900.####.jpg [0 to 25 by 1] (0 MATCHED, 26 FAILED)
115
+
116
+
117
+
118
+
119
+ ### Check membership
120
+
121
+ seq check [SIGNATURE]
122
+
123
+ Check the existence of files defined by a sequence signature.
124
+
125
+ Options:
126
+
127
+ * -f, [--show-found] # List files from the signature that are on disk.
128
+ * -m, [--show-missing] # List files from the signature that are missing on disk.
129
+ * -e, [--show-extra] # List files that are on disk but not in the signature.
130
+ * -a, [--show-all] # List the union of all files on disk or in the signature.
131
+
132
+
133
+ Files may either be found, missing, or extra. Extra means the file exists on disk but not in the signature. With no flags, the output will be just a single line showing the number of found, missing or extra. By using the flags, you can list the individual files in the sequence with color coding and and in order.
134
+
135
+ $ seq c 006_057_s3d_lf_r04.####.dpx [1 to 30 by 1]
136
+ 006_057_s3d_lf_r04.####.dpx [found: 30] [missing: 0] [extra: 0]
137
+
138
+ $ seq c 006_057_s3d_lf_r04.####.dpx [5 to 35 by 1]
139
+ 006_057_s3d_lf_r04.####.dpx [found: 26] [missing: 5] [extra: 4]
140
+
141
+ $ seq c 006_057_s3d_lf_r04.####.dpx [5 to 35 by 2] -m
142
+ 006_057_s3d_lf_r04.####.dpx [found: 13] [missing: 3] [extra: 17]
143
+ 006_057_s3d_lf_r04.0031.dpx
144
+ 006_057_s3d_lf_r04.0033.dpx
145
+ 006_057_s3d_lf_r04.0035.dpx
146
+
147
+ ### Generate
148
+
149
+ seq generate [NAME]
150
+
151
+ Generate the .sequin config file in your home directory ~/.seq.yml
152
+
153
+ Options:
154
+ * -f, [--force] # Overwrite the file if it exists.
155
+
156
+ Currently, NAME must be config. This file contains regular expression definitions used in the `seq match` task.
157
+
158
+ $ seq g config
159
+ Skipping write of /Users/julianmann/.seq.yml because it exists. Use --force to remove it.
160
+
161
+ $ seq g config -f
162
+ Generating /Users/julianmann/.seq.yml
163
+
164
+ How Sequin finds sequences
165
+ -------
166
+
167
+ The goal of the sequence listing algorithm is to find sequences without the need
168
+ for the user to specify where the numbers are in the filenames, or what characters
169
+ delimit the numbers. This can be tricky because a filename may contain many parts
170
+ containing numbers. Here's a real world example: `004_055_s3d_lf_r04.0001.dpx`
171
+ How can we know which number part correctly represents the sequence?
172
+ It could be any of the following
173
+
174
+ ###_055_s3d_lf_r04.0001.dpx
175
+ 004_###_s3d_lf_r04.0001.dpx
176
+ 004_055_s#d_lf_r04.0001.dpx
177
+ 004_055_s3d_lf_r##.0001.dpx
178
+ 004_055_s3d_lf_r04.####.dpx
179
+
180
+ The strategy we employ is to use the number part that causes the file to be in the largest sequence.
181
+
182
+ Consider the files:
183
+
184
+ file.04.0001.ext
185
+ file.04.0002.ext
186
+ file.04.0003.ext
187
+ file.05.0001.ext
188
+ file.05.0002.ext
189
+ file.05.0003.ext
190
+
191
+ There are 5 possible sequences here. Each file could be in 2 of them, one containing 2 files, one containing 3.
192
+
193
+ file.04.0001.ext could be in file.##.0001.ext or file.04.####.ext
194
+
195
+ There are more files in `file.04.####.ext` than `file.##.0001.ext`, so we resolve to `file.04.####.ext` for this file.
196
+
197
+
198
+ To Do
199
+ -------
200
+
201
+ * Add support for listing recursively
202
+ * Add modifier tasks such as pad, offset, replace etc.
203
+ * Test colors in shells other than bash.
204
+ * Test on Ruby 1.8.7. Currently works under Ruby 1.9.2
205
+
206
+
207
+ License
208
+ -------
209
+
210
+ See the LICENSE file for further details.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
data/bin/seq ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'sequin'
4
+
5
+ Sequin::Seq.start
data/lib/listing.rb ADDED
@@ -0,0 +1,334 @@
1
+ #!/usr/bin/env ruby
2
+ require 'pp'
3
+ require 'fileutils'
4
+
5
+ # add colors to String class
6
+ class String
7
+ def colorize(code)
8
+ "\e[0;#{code.to_s}m#{self}\e[0m"
9
+
10
+ # self
11
+ end
12
+
13
+ # I tried to put "define_method" in a loop but it was having none of it
14
+ # Please accept my humble apologies
15
+ def red; self.colorize(31);end
16
+ def green; self.colorize(32);end
17
+ def yellow; self.colorize(33);end
18
+ def blue; self.colorize(34);end
19
+ def purple; self.colorize(35);end
20
+ def cyan; self.colorize(36);end
21
+ end
22
+
23
+
24
+
25
+ module Sequin
26
+
27
+ class Sequin::Listing
28
+
29
+ # The goal of this sequence listing algorithm is to find sequences without the need
30
+ # for the user to specify where the numbers are in the filenames, or what characters
31
+ # delimit the numbers. This can be tricky because a filename may contain many parts
32
+ # containing numbers. Here's a real world example: 004_055_s3d_lf_r04.0001.dpx
33
+ # How can we know which number part correctly represents the sequence?
34
+ # It could be any of the following
35
+ # ###_055_s3d_lf_r04.0001.dpx
36
+ # 004_###_s3d_lf_r04.0001.dpx
37
+ # 004_055_s#d_lf_r04.0001.dpx
38
+ # 004_055_s3d_lf_r##.0001.dpx
39
+ # 004_055_s3d_lf_r04.####.dpx
40
+ #
41
+ # The strategy we employ is to use the number part that causes the file to be in the largest sequence.
42
+
43
+ # Consider the files:
44
+ # file.04.0001.ext
45
+ # file.04.0002.ext
46
+ # file.04.0003.ext
47
+ # file.05.0001.ext
48
+ # file.05.0002.ext
49
+ # file.05.0003.ext
50
+ #
51
+ # There are 5 possible sequences here. Each file could be in 2 of them, one containing 2 files, one containing 3.
52
+ # file.04.0001.ext could be in file.##.0001.ext or file.04.####.ext
53
+ # There are more files in file.04.####.ext than file.##.0001.ext, so we use that sequence for this file.
54
+
55
+
56
+
57
+ def initialize(files)
58
+ @sequences = Hash.new
59
+
60
+ # make a hash containing a signature key for all possible sequences
61
+ all_seqs = all_possible_sequences files
62
+
63
+ # now select from that hash the best sequences according
64
+ # to the strategy outlined above
65
+ select_best_sequences(all_seqs, files)
66
+
67
+ # we need to make a note of the padding because
68
+ # we will be converting the number part to integers
69
+ each_sequence { |k,v| v[:padding] = v[:numbers].min.length }
70
+
71
+ # convert the number part to integers and sort them
72
+ # so we can figure out the range, by_frame, and missing frames
73
+ each_sequence { |k,v| v[:numbers].collect! {|el| el.to_i} }
74
+ each_sequence { |k,v| v[:numbers].sort! }
75
+
76
+ # find the by_frame so that we can figure out which
77
+ # frames are missing, if any. If we don't get
78
+ # the by_frame for a sequence that is by 2 for example, we would
79
+ # think that every other frame was missing.
80
+ each_sequence {|k,v| v[:by] = calc_by_frame v[:numbers]}
81
+
82
+ # now we know the by frame, we can find out which frames
83
+ # are missing because the user will want to know this.
84
+ each_sequence do |k,v|
85
+ range = v[:numbers].min..v[:numbers].max
86
+ v[:missing] = []
87
+ range.step(v[:by]) do |val|
88
+ v[:missing] << val unless v[:numbers].include? val
89
+ end
90
+ end
91
+
92
+ # we no longer need the filenames. We have enough info to
93
+ # recreate them if necessary
94
+ each_sequence {|k,v| v.delete(:names) }
95
+
96
+ # here we change the hash sequence keys (which are arrays). We make the number slot
97
+ # represent the padding. This is so we can create a sequence key
98
+ # by simply calling join on the array elements
99
+ each_sequence { |k,v| k[v[:numbers_index]] = ("#" *v[:padding]) }
100
+ end
101
+
102
+
103
+ # for debugging
104
+ def inspect
105
+ pp @sequences
106
+ end
107
+
108
+
109
+
110
+ # a sequence string describes the sequence like so.
111
+ # filename [ <start> to <end> by <by_frame> ] - missing [ <missing frames> ]
112
+ # This is what gets printed out in a listing, and can also be used as input
113
+ # to other seq commands such as seq pad, seq renumber, seq substitute
114
+ # The missing frames section is optional. When a sequence_signature is used as input
115
+ # the missing frames will be determined by seq internally.
116
+ def sequence_signature(k,v,verbose=1)
117
+ range_str= "[#{v[:numbers].min} to #{v[:numbers].max} by #{v[:by]}]"
118
+ unless v[:missing].empty?
119
+ range_str = "#{range_str} - missing: [ #{v[:missing].count} frames ]" if verbose == 1
120
+ range_str = "#{range_str} - missing: [ #{v[:missing].join(",")} ]"if verbose == 2
121
+ range_str = range_str.red
122
+ else
123
+ range_str = range_str.green
124
+ end
125
+ str = "#{k.join()} #{range_str}"
126
+ end
127
+
128
+
129
+
130
+ # simple listing of single files and sequences
131
+ def list(verbose=1)
132
+ each_single {|f| puts f}
133
+
134
+ each_sequence do |k,v|
135
+ str = sequence_signature(k,v,verbose)
136
+ puts str
137
+ end
138
+ end
139
+
140
+
141
+ # match a sequence against a regex.
142
+ # create an array where
143
+ # each element is a sub array of the form
144
+ # ["filename", <:matched|:failed>, frame_number]
145
+ #
146
+ # The sequence is more managable as a single
147
+ # array as it can be sorted.
148
+ def self.match_matrix(regex, key , seq)
149
+ prefix, suffix = key.join().split(/#+/)
150
+ matrix = []
151
+ seq[:numbers].each do |n|
152
+ fn = [prefix , ("%0#{seq[:padding]}d" % n) , suffix].join()
153
+ unless regex.match(File.basename(fn)).nil?
154
+ matrix << [fn,:matched,n]
155
+ else
156
+ matrix << [fn,:failed,n]
157
+ end
158
+ end
159
+ matrix.sort! { |a,b| a[2] <=> b[2] }
160
+ end
161
+
162
+
163
+
164
+
165
+
166
+ # make a one line message describing the result of this
167
+ # matching a regex against all files in a sequence. Most of
168
+ # the info we need is in the fn_matrix array, but we also
169
+ # need the sequence key/value pair to create the signature.
170
+ def assessment_message(fn_matrix,k,v)
171
+ matched = fn_matrix.count { |el| el[1] == :matched }
172
+ failed = fn_matrix.count - matched
173
+
174
+ msg = "#{matched} MATCHED"
175
+ msg = "#{msg}, #{failed} FAILED" if failed > 0
176
+ msg = "(#{msg})"
177
+ msg = "#{msg} / #{fn_matrix.count} files" if failed > 0 && matched > 0
178
+
179
+ if failed == 0
180
+ msg = msg.green
181
+ elsif matched == 0
182
+ msg = msg.red
183
+ else
184
+ msg = msg.yellow
185
+ end
186
+
187
+ fn = sequence_signature(k,v,0)
188
+ message = "#{fn} #{msg}"
189
+ end
190
+
191
+
192
+
193
+ # this proc is called from the main script. Matches a regex against
194
+ # all single files and all sequences in the @sequences object and outputs
195
+ # information according to verbosity options
196
+ def match(the_regex, options ={})
197
+ re = Regexp.new(the_regex)
198
+ puts "Testing against regular expression: /#{the_regex}/".cyan
199
+
200
+ each_single do |f|
201
+ msg = re.match(File.basename(f)).nil? ? 'FAILED'.red : 'PASSED'.green
202
+ puts "#{f} #{msg}"
203
+ end
204
+
205
+ each_sequence do |k,v|
206
+ fn_matrix = Sequin::Listing.match_matrix(re, k,v)
207
+ puts "#{assessment_message(fn_matrix,k,v)}"
208
+ unless options["show_all"].nil? && options["show_matched"].nil? && options["show_failed"].nil?
209
+ fn_matrix.each do |el|
210
+ key = "show_#{el[1].to_s}"
211
+ puts el[0].send(Sequin::Listing.alert_colors[el[1]]) unless options[key].nil? && options["show_all"].nil?
212
+ end
213
+ end
214
+ end
215
+
216
+ end
217
+
218
+
219
+ # takes a block and iterates through sequence
220
+ # keys, ignoring single files
221
+ def each_sequence
222
+ @sequences.each do |k,v|
223
+ yield(k,v) unless k == :single
224
+ end
225
+ end
226
+
227
+ # takes a block and iterates through single
228
+ # files, ignoring . and .. and sequences
229
+ def each_single
230
+ @sequences.each do |k,v|
231
+ if k == :single
232
+ v.each { |f| yield(f) unless( f =="." || f == "..") }
233
+ end
234
+ end
235
+ end
236
+
237
+
238
+ private
239
+
240
+
241
+ def all_possible_sequences(files)
242
+ h = Hash.new
243
+ files.each do |f|
244
+
245
+ # an array containing the number parts in the odd slots: [1] [3] etc.
246
+ parts = f.split(/([0-9]+)/)
247
+
248
+ # If there are an even number of parts, half of them are numbers
249
+ # If there are an odd number of parts, there are more not-numbers than numbers
250
+ # By dividing by 2 and relying on integer rounding, we get the right answer in both cases
251
+ number_count = parts.count / 2
252
+
253
+ # For every number position, make our hash key from the remaining parts
254
+ # and make the hash value another hash containing the filenames, numbers, and other info
255
+ (0...number_count).each do |i|
256
+ number_part_index = (i*2)+1
257
+ key =[]
258
+ (0...parts.count).each do |part_index|
259
+ key << (part_index == number_part_index ? "#" : parts[part_index] )
260
+ end
261
+
262
+ h[key] ||= {}
263
+ (h[key][:names] ||= []) << f
264
+ (h[key][:numbers] ||= []) << parts[number_part_index]
265
+ h[key][:numbers_index] = number_part_index
266
+ end
267
+ end
268
+ h
269
+ end
270
+
271
+
272
+ # Find the best sequence that each file belongs to.
273
+ # As mentioned above, the best sequence is the one with
274
+ # the most files out of the set of all sequences that
275
+ # this file is in.
276
+ def select_best_sequences(all_seqs, files)
277
+ files.each do |f|
278
+ most_frames = 0
279
+ best_hash = {}
280
+ best_key = nil
281
+
282
+ all_seqs.each do |k,v|
283
+ curr_frames = all_seqs[k][:numbers].count
284
+ if curr_frames > most_frames
285
+ if all_seqs[k][:names].include?(f)
286
+ best_hash = all_seqs[k]
287
+ best_key = k
288
+ most_frames = curr_frames
289
+ end
290
+ end
291
+ end
292
+ if best_key.nil? || most_frames < 2
293
+ ( @sequences[:single] ||= [] ) << f
294
+ else
295
+ @sequences[best_key] = best_hash
296
+ end
297
+ end
298
+ end
299
+
300
+ # to determine the by_frame, we calculate the greatest common factor of
301
+ # all the gaps between successive frames. If the gap gets to 1, then break
302
+ # because we don't consider fractional frame numbers. 1 is the smallest gap allowed.
303
+ # Note: with this method, we can still know the by_frame even if some frames
304
+ # are missing
305
+ def calc_by_frame(frames)
306
+ gcf = frames.max - frames.min
307
+ last = frames.min
308
+ frames.each do |n|
309
+ if n != frames.min
310
+ new_gcf = greatest_common_factor(gcf, n - last)
311
+ gcf = new_gcf if new_gcf < gcf
312
+ break if gcf == 1
313
+ last = n
314
+ end
315
+ end
316
+ gcf
317
+ end
318
+
319
+
320
+ # recursive GCF
321
+ def greatest_common_factor(n1, n2)
322
+ return ( n1 % n2==0 ) ? n2 : greatest_common_factor(n2, n1 % n2 )
323
+ end
324
+
325
+ def self.alert_colors
326
+ { :matched =>"green", :failed =>"red" }
327
+ end
328
+
329
+
330
+
331
+ end
332
+
333
+
334
+ end
data/lib/membership.rb ADDED
@@ -0,0 +1,83 @@
1
+
2
+
3
+ module Sequin
4
+
5
+
6
+ class Membership
7
+
8
+ # Membership
9
+ # In order to give information about files in relation to
10
+ # the sequence signature, we need to collect all the filenames
11
+ # the user asks for, and all the filenames on disk that
12
+ # match the sequence mask (the filename part of the sequence signature).
13
+ # Thewn we can determine whether the file was (found), (misssing),
14
+ # or (extra) i.e. existing but not asked for
15
+ #
16
+ # Each element in the array returned by this method is a
17
+ # 3 element sub array that has the following form:
18
+ # ["filename", <:found|:missing|:extra>, frame_number]
19
+ #
20
+ # It is sorted on the frame_number, which is an integer
21
+ def initialize(fn_template, options={} )
22
+
23
+ @fn_template = fn_template
24
+ @fn_matrix = Dir.glob(fn_template.gsub(/#+/, "*")).to_a
25
+ prefix, suffix = @fn_template.split(/#+/)
26
+
27
+ @fn_matrix.collect! { |f| [f, :extra] }
28
+
29
+ range = options[:start_frame]..options[:end_frame]
30
+ range.step(options[:by_frame] || 1) do |val|
31
+
32
+ filename = fn_template.gsub(/#+/, ("%0#{options[:padding]}d" % val) )
33
+
34
+ el = @fn_matrix.find {|e| e[0] == filename }
35
+ if el.nil?
36
+ @fn_matrix << [filename , :missing ]
37
+ else
38
+ el[1] = :found
39
+ end
40
+ end
41
+ @fn_matrix.each { |el| el[2] = el[0].sub(prefix,'').sub(suffix,'').to_i }
42
+ @fn_matrix.sort! { |a,b| a[2] <=> b[2] }
43
+ end
44
+
45
+ # generate a message from the filename matrix - something like this:
46
+ # ../test/a/006_057_s3d_lf_r06.#.dpx [found: 13] [missing: 3] [extra: 17]
47
+ # where the found missing and extra sections are color coded according
48
+ # to whether there were files present with that attribute
49
+ def assessment_message
50
+ message = @fn_template
51
+ [:found, :missing, :extra].each do |t|
52
+ num_present = @fn_matrix.count { |el| el[1] == t }
53
+ m = "[#{t.to_s}: #{num_present}]"
54
+ m = (num_present > 0) ? m.send(Sequin::Membership.alert_colors[t][:present]) : m.send(Sequin::Membership.alert_colors[t][:absent])
55
+ message = "#{message} #{m}"
56
+ end
57
+ message
58
+ end
59
+
60
+ # output a brief assessment message and optionally list files
61
+ # that are found, missing, and/or extra
62
+ def output(options={})
63
+ puts "#{assessment_message}"
64
+ @fn_matrix.each do |el|
65
+ key = "show_#{el[1].to_s}"
66
+ puts el[0].send(Sequin::Membership.alert_colors[el[1]][:present]) unless options[key].nil? && options["show_all"].nil?
67
+ end
68
+
69
+ end
70
+
71
+ private
72
+
73
+ def self.alert_colors
74
+ {
75
+ :found => {:present => "green", :absent => "red"} ,
76
+ :missing => {:present =>"red", :absent => "green"} ,
77
+ :extra => {:present =>"yellow", :absent => "green"}
78
+ }
79
+ end
80
+
81
+ end
82
+
83
+ end
data/lib/sequin.rb ADDED
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env ruby
2
+ require 'pp'
3
+ require "thor"
4
+ require 'fileutils'
5
+ require 'yaml'
6
+ require File.expand_path(File.dirname(__FILE__) + '/listing')
7
+ require File.expand_path(File.dirname(__FILE__) + '/membership')
8
+
9
+ module Sequin
10
+
11
+ class Seq < Thor
12
+
13
+
14
+ no_tasks do
15
+ # Some typical regular expressions for use by seq:match.
16
+ # When seq:generate config is called, this hash is dumped to a yaml file
17
+ def regex_hash
18
+ h =
19
+ {'project1' =>
20
+ {'dpx' => '^[0-9]{3}_[0-9]{3}_s3d_(rt|lf)_r[0-9]{2}\.[0-9]{4}\.dpx$',
21
+ 'jpg' => '^[0-9]{3}_[0-9]{3}_s3d_r[0-9]{2}\.[0-9]{4}\.jpg$',
22
+ 'mcx' => '^[0-9]{3}_[0-9]{3}_ver[0-9]{2,3}\.[0-9]{4}\.xml$'},
23
+ 'project2' =>
24
+ { 'dpx' => '^[a-z]{2}_[0-9]{3}_[0-9]{4}_(L|R|C)\.[0-9]{4,5}\.dpx$',
25
+ 'jpg' => '^[a-z]{2}_[0-9]{3}_[0-9]{4}_(L|R|C)\.[0-9]{4,5}\.jpg$',
26
+ 'mov' => '^[a-z]{2}_h264_[0-9]{3,4}x[0-9]{3,4}_(film|pal|ntsc)\.mov$' }
27
+ }
28
+ end
29
+
30
+ def regex_comments
31
+ <<-COMMENTS
32
+ #------------------------------------------------------------------------------------
33
+ #
34
+ # ~/.seq.yml
35
+ #
36
+ # The regular expressions in this file represent naming conventions for various
37
+ # projects and file_types. By matching filenames against these regexes, you can
38
+ # quickly determine if they conform to the desired naming convention.
39
+ #
40
+ # The regexes are accessed on the command line using the hash keys separated by
41
+ # ":". i.e. seq match <files> --regex project1:dpx
42
+ #
43
+ # For regex newbies, here are some disections:
44
+ # ^[a-z]+_[0-9]{3}_s3d_(rt|lf)_r[0,1][0-9]\.[0-9]{4}\.dpx$
45
+ # ^ : the next character must be at the start of the string
46
+ # [a-z]+_ : one or more lower case letters followed by an underscore
47
+ # [0-9]{3}_ : exactly 3 numbers
48
+ # _s3d_ : "_s3d_"
49
+ # (rt|lf) : "rt" or "lf"
50
+ # r[0,1][0-9] : "r" followed by a 2 digit number between 00 and 19
51
+ # \. : "." It must be escaped
52
+ # [0-9]{4} : 4 padded number
53
+ # \.dpx : ".dpx" extension.
54
+ # $ : The preceding character must be the last character. "x" in this case.
55
+ #------------------------------------------------------------------------------------
56
+
57
+ COMMENTS
58
+ end
59
+
60
+ end
61
+
62
+
63
+ # When the user types seq and gives no task, we want to show
64
+ # the default help with our own mods added.
65
+ default_task :usage
66
+
67
+ desc "usage", "Print the usage banner"
68
+ def usage
69
+ help # default Task descriptions
70
+
71
+ puts <<-BANNER
72
+ ------------------------------------------------------------------------------------
73
+ Seq is a collection of commands to help deal with sequences on disk
74
+
75
+ More help here
76
+
77
+ BANNER
78
+
79
+ end
80
+
81
+
82
+
83
+
84
+
85
+
86
+
87
+ # map "i" => :inspect
88
+ # desc "inspect [FILES]", "inspect raw sequence hash for debugging purposes"
89
+ # def inspect(*files)
90
+ # files = Dir.new(".").entries if files.count == 0
91
+ # l = Sequin::Listing.new(files)
92
+ # l.inspect
93
+ # end
94
+
95
+
96
+ map "l" => :list
97
+ desc "list [FILES]", "list files as sequences"
98
+ method_option :verbose,:type => :numeric, :default => 1, :aliases => "-v", :desc => "0 => dont mention missing frames, 1 => show missing frame count, 2 => list missing frame numbers"
99
+ def list(*files)
100
+ unless files.count == 0
101
+ l = Sequin::Listing.new(files)
102
+ l.list(options[:verbose])
103
+ else
104
+ msg = "No files."
105
+ puts msg.red
106
+ help :list
107
+ return
108
+ end
109
+ end
110
+
111
+
112
+
113
+
114
+ map "g" => :generate
115
+ desc "generate [NAME]", "Generate a seq config file containing regular expression definitions used in the seq:match command. Currently, NAME must be config"
116
+ method_option :force, :type => :boolean, :aliases => "-f", :desc => "Overwrite the file if it exists.", :default => false
117
+
118
+ def generate(name)
119
+ if name.downcase == "config"
120
+ yml = File.join(ENV['HOME'],".seq.yml")
121
+ FileUtils.rm(yml) if options[:force] && File.exists?(yml)
122
+ if File.exists?(yml)
123
+ msg = "Skipping write of #{yml} because it exists. Use --force to remove it."
124
+ puts msg.red
125
+ help :generate
126
+ return
127
+ else
128
+ puts "Generating #{yml}"
129
+ File.open(yml , 'w') do |f|
130
+ f.write(regex_comments)
131
+ YAML.dump(regex_hash, f)
132
+ end
133
+ end
134
+ else
135
+ msg = "Error: #{name} is not recognized."
136
+ puts msg.red
137
+ help :generate
138
+ return
139
+ end
140
+ end
141
+
142
+ map "c" => :check
143
+ desc "check [SIGNATURE]", "Check the existence of files defined by a sequence signature."
144
+ method_option :show_missing, :type => :boolean, :aliases => "-m", :desc => "List files from the signature that are missing on disk"
145
+ method_option :show_extra, :type => :boolean, :aliases => "-e", :desc => "List files that are on disk but not in the signature"
146
+ method_option :show_found, :type => :boolean, :aliases => "-f", :desc => "List files from the signature that are on disk"
147
+ method_option :show_all, :type => :boolean, :aliases => "-a", :desc => "List the union of all files on disk and in the signature."
148
+ def check(*args)
149
+ signature = args.join(" ")
150
+
151
+ chunks = signature.split(/\[|\]/)
152
+ if chunks.count < 2
153
+ display_str = (signature.length > 60) ? "#{signature[0, 60]}..." : signature
154
+ msg = "Error: #{display_str} is not a valid sequence signature."
155
+ puts msg.red
156
+ help :check
157
+ return
158
+ end
159
+
160
+ fn_template = chunks[0].strip
161
+ numbers = chunks[1].split(/([0-9]+)/)
162
+
163
+ if numbers.count < 4
164
+ display_str = (signature.length > 60) ? "#{signature[0, 60]}..." : signature
165
+ msg = "Error: #{display_str} is not a valid sequence signature. Should have [<start> to <end> [by <by_frame>]]. "
166
+ puts msg.red
167
+ help :check
168
+ return
169
+ end
170
+
171
+
172
+ match = /#+/.match(fn_template)
173
+ if match.nil?
174
+ msg = "Error: #{fn_template} has no # characters so does not represent a sequence."
175
+ puts msg.red
176
+ help :check
177
+ return
178
+ end
179
+
180
+ range_spec=Hash.new
181
+ range_spec[:padding] = match[0].length
182
+ range_spec[:start_frame] = numbers[1].to_i
183
+ range_spec[:end_frame] = numbers[3].to_i
184
+ range_spec[:by_frame] = numbers[5].to_i || 1
185
+
186
+ if range_spec[:start_frame].nil? || range_spec[:end_frame].nil? || ( range_spec[:start_frame] >= range_spec[:end_frame] )
187
+ msg = "Error: start_frame #{start_frame} must be less than end_frame #{end_frame}."
188
+ puts msg.red
189
+ help :check
190
+ return
191
+ end
192
+
193
+ # range_spec.merge!(options)
194
+ membership =Sequin::Membership.new(fn_template, range_spec)
195
+
196
+ membership.output(options)
197
+
198
+ end
199
+
200
+
201
+
202
+
203
+ map "m" => :match
204
+ desc "match [FILES]", "Match filename naming conventions using a regular expression."
205
+ method_option :regex, :type => :string, :aliases => "-r", :required => true, :desc => "Specify a regular expression literally, or by reference to one listed in ~/.seq.yml"
206
+ method_option :show_matched, :type => :boolean, :aliases => "-m", :desc => "List files that matched the regex in green"
207
+ method_option :show_failed, :type => :boolean, :aliases => "-f", :desc => "List files that failed to match the regex in red"
208
+ method_option :show_all, :type => :boolean, :aliases => "-a", :desc => "List all the files in order with color coding."
209
+ def match(*files)
210
+ keys = options[:regex].split(":")
211
+ if keys.count != 2
212
+ the_regex = options[:regex]
213
+ else
214
+
215
+ begin
216
+ yml = File.join(ENV['HOME'],".seq.yml")
217
+ regexp_hash = YAML::load_file(yml)
218
+ rescue
219
+ msg = "Error: opening file #{yml} - please check the file exists and is valid YAML.\nIf it doesn't exist, run: seq generate config."
220
+ puts msg.red
221
+ help :match
222
+ return
223
+ end
224
+
225
+ unless regexp_hash.has_key?(keys[0])
226
+ msg = "Error: #{keys[0]} not in #{yml} - please check the file is valid YAML and has a top level key: #{keys[0]}."
227
+ puts msg.red
228
+ help :match
229
+ return
230
+ end
231
+
232
+ unless regexp_hash[keys[0]].has_key?(keys[1])
233
+ msg = "Error: #{keys[1]} not in #{yml} - please check the file is valid YAML and has a second level key: #{keys[1]}."
234
+ puts msg.red
235
+ help :match
236
+ return
237
+ end
238
+
239
+ the_regex = regexp_hash[keys[0]][keys[1]]
240
+
241
+ end
242
+
243
+ unless the_regex.nil?
244
+ l = Sequin::Listing.new(files)
245
+ l.match(the_regex, options)
246
+ else
247
+ msg = "Error: No valid regular expression."
248
+ puts msg.red
249
+ help :match
250
+ return
251
+ end
252
+
253
+ end
254
+
255
+ end
256
+
257
+
258
+ end
@@ -0,0 +1,3 @@
1
+ module Sequin
2
+ VERSION = "0.0.1"
3
+ end
data/sequin.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "sequin/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "sequin"
7
+ s.version = Sequin::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Julian Mann"]
10
+ s.email = ["julian.mann@gmail.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{Tools to deal with file sequences.}
13
+ s.description = %q{Sequin is a collection of commands to list, test and modify sequences of files on disk.}
14
+ s.add_dependency('thor')
15
+ s.rubyforge_project = "sequin"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sequin
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Julian Mann
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-04-21 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: thor
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ type: :runtime
25
+ version_requirements: *id001
26
+ description: Sequin is a collection of commands to list, test and modify sequences of files on disk.
27
+ email:
28
+ - julian.mann@gmail.com
29
+ executables:
30
+ - seq
31
+ extensions: []
32
+
33
+ extra_rdoc_files: []
34
+
35
+ files:
36
+ - .gitignore
37
+ - CHANGELOG
38
+ - Gemfile
39
+ - LICENSE
40
+ - README.md
41
+ - Rakefile
42
+ - bin/seq
43
+ - lib/listing.rb
44
+ - lib/membership.rb
45
+ - lib/sequin.rb
46
+ - lib/sequin/version.rb
47
+ - sequin.gemspec
48
+ homepage: ""
49
+ licenses: []
50
+
51
+ post_install_message:
52
+ rdoc_options: []
53
+
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ requirements: []
69
+
70
+ rubyforge_project: sequin
71
+ rubygems_version: 1.7.2
72
+ signing_key:
73
+ specification_version: 3
74
+ summary: Tools to deal with file sequences.
75
+ test_files: []
76
+