lossfully 0.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.
- data/.bnsignore +19 -0
- data/.gitmodules +3 -0
- data/CHANGELOG +4 -0
- data/COPYING +674 -0
- data/README.rdoc +96 -0
- data/Rakefile +27 -0
- data/lib/lossfully/audio_file.rb +134 -0
- data/lib/lossfully/generator.rb +614 -0
- data/lib/lossfully/input_rules.rb +176 -0
- data/lib/lossfully/thread_pool.rb +162 -0
- data/lib/lossfully.rb +79 -0
- data/test/test_audio_file.rb +47 -0
- data/test/test_input_rules.rb +125 -0
- data/test/test_thread_pool.rb +77 -0
- data/version.txt +1 -0
- metadata +100 -0
@@ -0,0 +1,614 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C) 2011 Don March
|
3
|
+
#
|
4
|
+
# This file is part of Lossfully.
|
5
|
+
#
|
6
|
+
# Lossfully is free software: you can redistribute it and/or modify it
|
7
|
+
# under the terms of the GNU General Public License as published by
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# Lossfully is distributed in the hope that it will be useful, but
|
12
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
14
|
+
# General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU General Public License
|
17
|
+
# along with this program. If not, see
|
18
|
+
# <http://www.gnu.org/licenses/>.
|
19
|
+
#++
|
20
|
+
|
21
|
+
require 'find'
|
22
|
+
require 'pathname'
|
23
|
+
require 'erb'
|
24
|
+
require 'fileutils'
|
25
|
+
|
26
|
+
# TODO: test if necessary binaries are installed
|
27
|
+
# TODO: test if sox compiled with LAME
|
28
|
+
# check and update metadata
|
29
|
+
|
30
|
+
module Lossfully
|
31
|
+
|
32
|
+
# Lossfully works in a sort of declarative way of adding rules that
|
33
|
+
# match certain files and indicate what action to take on those
|
34
|
+
# files.
|
35
|
+
#
|
36
|
+
# There are six main methods that are used to add rules for how to
|
37
|
+
# handle different types of files, and they all behave relatively
|
38
|
+
# similarly: encode, options, effect_options, path, clobber, and
|
39
|
+
# remove_missing. All of the rules created by a given method are
|
40
|
+
# collected as InputRules (which see) and are sorted in rough order
|
41
|
+
# of strictness. When the generator is finally run, each rule is
|
42
|
+
# tried in succession (in order of strictness) until one matches the
|
43
|
+
# file, and that action is used. Each method adds rules for how to
|
44
|
+
# encode files, except for remove_missing, which determines what
|
45
|
+
# pre-existing files to remove from the target directory upon
|
46
|
+
# completion.
|
47
|
+
#
|
48
|
+
# Each rule-making method takes a hash where the key specifies what
|
49
|
+
# files the new rule should apply to, and the value specifies what
|
50
|
+
# the action is. The key part of the hash is a single object or an
|
51
|
+
# array of the following:
|
52
|
+
#
|
53
|
+
# * A symbol, which matches the type of the file as returned by
|
54
|
+
# `soxi -t' (e.g., :vorbis) or a type class (:everything, :audio,
|
55
|
+
# :nonaudio, :lossy, :lossless). The symbol :ogg is treated as a
|
56
|
+
# synonym for :vorbis.
|
57
|
+
#
|
58
|
+
# * A string that specifies a file extension to match.
|
59
|
+
#
|
60
|
+
# * A regular expression.
|
61
|
+
#
|
62
|
+
# * A number, which specifies the minimum bitrate in kbps a matching
|
63
|
+
# file must have.
|
64
|
+
#
|
65
|
+
# The allowed values for the key specifying the action depends on
|
66
|
+
# the method.
|
67
|
+
#
|
68
|
+
# A rule can omit the key part of the hash and simply specify the
|
69
|
+
# rule, and a default will be used (:everything for clobber and
|
70
|
+
# remove_missing, :audio for the others).
|
71
|
+
#
|
72
|
+
# A rule can also omit the value (action) part of the hash if a
|
73
|
+
# block is given. The block must return nil or false when the rule
|
74
|
+
# does not apply to a file; otherwise the block should return the
|
75
|
+
# action to perform on the matching files. An AudioFile instance
|
76
|
+
# will be yielded to the block regardless of whether the current
|
77
|
+
# file is actually audio or not. See the documentation for
|
78
|
+
# AudioFile for available methods.
|
79
|
+
#
|
80
|
+
# So, for example, the following are possible uses of the encode
|
81
|
+
# method:
|
82
|
+
#
|
83
|
+
# encode [:lossy, 320, /Bach/] => '.mp3'
|
84
|
+
#
|
85
|
+
# encode [:lossy, 320, /Bach/] do
|
86
|
+
# if # conditions here
|
87
|
+
# ['.ogg', 6]
|
88
|
+
# elsif # other conditions
|
89
|
+
# ['.mp3', -192.2]
|
90
|
+
# else
|
91
|
+
# false
|
92
|
+
# end
|
93
|
+
# end
|
94
|
+
#
|
95
|
+
# The methods copy and skip are aliases for encode with :skip or
|
96
|
+
# :copy as the action.
|
97
|
+
#
|
98
|
+
# The methods quiet, threads, and rel_path just set options and do
|
99
|
+
# not create rules.
|
100
|
+
#
|
101
|
+
class Generator
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def self.sort_by_rules array
|
106
|
+
array.sort { |x,y| (x[0] <=> y[0]) rescue 0 }
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.sort_by_rules! array
|
110
|
+
array.sort! { |x,y| (x[0] <=> y[0]) rescue 0 }
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.determine_rule array, file
|
114
|
+
array.each do |r|
|
115
|
+
if result = r[0].test(file)
|
116
|
+
return (result == true) ? r[1] : result
|
117
|
+
end
|
118
|
+
end
|
119
|
+
return nil
|
120
|
+
end
|
121
|
+
|
122
|
+
public
|
123
|
+
|
124
|
+
# Create a new Generator instance to store rules and run them.
|
125
|
+
#
|
126
|
+
# If a block is given, yeild self if arity of block is 1,
|
127
|
+
# otherwise run instance_eval on the block.
|
128
|
+
#
|
129
|
+
# Normally you would just call Lossfully.generate (which is a
|
130
|
+
# wrapper around this and #generate) with a source
|
131
|
+
# library/playlist and a target.
|
132
|
+
#
|
133
|
+
def initialize &block
|
134
|
+
@encode_rules = Array.new
|
135
|
+
@path_rules = Array.new
|
136
|
+
@option_rules = Array.new
|
137
|
+
@effect_option_rules = Array.new
|
138
|
+
@clobber_rules = Array.new
|
139
|
+
@remove_missing_rules = Array.new
|
140
|
+
|
141
|
+
@verbosity = 1
|
142
|
+
@threads = 1
|
143
|
+
|
144
|
+
# set default rules for commands
|
145
|
+
encode :everything => :copy
|
146
|
+
path :everything => :original
|
147
|
+
clobber :everything => true
|
148
|
+
remove_missing :everything => true
|
149
|
+
options :audio => ''
|
150
|
+
effect_options :audio => ''
|
151
|
+
|
152
|
+
if block_given?
|
153
|
+
if block.arity == 1
|
154
|
+
yield self
|
155
|
+
else
|
156
|
+
instance_eval(&block)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
attr_accessor :verbosity
|
162
|
+
attr_writer :rel_path, :threads
|
163
|
+
|
164
|
+
# Return the number of threads to use for encoding to n_threads,
|
165
|
+
# or set the number of threads to n_threads if called with an
|
166
|
+
# argument.
|
167
|
+
#
|
168
|
+
def threads n_threads=nil
|
169
|
+
if n_threads
|
170
|
+
@threads = n_threads
|
171
|
+
end
|
172
|
+
@threads
|
173
|
+
end
|
174
|
+
|
175
|
+
# Return or set the relative path used when expanding the source
|
176
|
+
# and target directories. The default is to expand relative to
|
177
|
+
# the script called on the command line, i.e. $0.
|
178
|
+
#
|
179
|
+
def rel_path arg=nil
|
180
|
+
if arg
|
181
|
+
@rel_path = File.directory?(arg) ? arg : File.dirname(arg)
|
182
|
+
else
|
183
|
+
rel_path $0
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Turn on or off the progress for checking and acting on each
|
188
|
+
# file.
|
189
|
+
#
|
190
|
+
def quiet bool=true
|
191
|
+
@verbosity = bool ? 0 : 1
|
192
|
+
end
|
193
|
+
|
194
|
+
# Run the rules collected in this instance on every file in the
|
195
|
+
# source, placing the results in the target directory. Takes a
|
196
|
+
# pair of strings either as individual arguments or as a Hash,
|
197
|
+
# (see example below). The source can be a directory or a playlist.
|
198
|
+
#
|
199
|
+
# Normally you would just call Lossfully.generate (which is a
|
200
|
+
# wrapper around this and Generator.new) with a source
|
201
|
+
# library/playlist and a target. But if you want to use the same
|
202
|
+
# rules on several directories or playlists, you can create the
|
203
|
+
# Generator once and then call #generate on it several times with
|
204
|
+
# different arguments
|
205
|
+
#
|
206
|
+
# g = Lossfully::Generator new do
|
207
|
+
# remove_missing false
|
208
|
+
# encode :lossless => '.ogg'
|
209
|
+
# # ...
|
210
|
+
# end
|
211
|
+
#
|
212
|
+
# g.generate 'dir1', 'target'
|
213
|
+
# g.generate 'dir2' => 'target'
|
214
|
+
#
|
215
|
+
def generate *args
|
216
|
+
if args.size == 1 then
|
217
|
+
hash = args[0]
|
218
|
+
raise "Target not specified" unless hash.kind_of? Hash
|
219
|
+
raise "Hash must have only one key-value pair." unless hash.size == 1
|
220
|
+
primary = hash.keys.first
|
221
|
+
target = hash.values.first
|
222
|
+
else
|
223
|
+
raise "Input incorrectly specified." if args.size > 2
|
224
|
+
primary = args[0]
|
225
|
+
target = args[1]
|
226
|
+
end
|
227
|
+
|
228
|
+
raise "Converting from multiple libraries is not supported" if primary.kind_of? Array
|
229
|
+
raise "Writing to multiple directories is not supported" if target.kind_of? Array
|
230
|
+
primary = File.expand_path primary, File.dirname(rel_path())
|
231
|
+
target = File.expand_path target, File.dirname(rel_path())
|
232
|
+
|
233
|
+
raise "Overwriting original library not supported." if primary == target
|
234
|
+
|
235
|
+
[@encode_rules,
|
236
|
+
@path_rules,
|
237
|
+
@option_rules,
|
238
|
+
@effect_option_rules,
|
239
|
+
@clobber_rules,
|
240
|
+
@remove_missing_rules].each do |a|
|
241
|
+
self.class.sort_by_rules! a
|
242
|
+
end
|
243
|
+
|
244
|
+
encode_actions = ThreadPool.new(@threads)
|
245
|
+
copy_actions = ThreadPool.new(1)
|
246
|
+
|
247
|
+
int_level = 0
|
248
|
+
trap("INT") do
|
249
|
+
if int_level == 0
|
250
|
+
int_level += 1
|
251
|
+
message "\nWill stop after current processes are finished; press CTRL-C again to stop immediately."
|
252
|
+
encode_actions.stop
|
253
|
+
copy_actions.stop
|
254
|
+
abort
|
255
|
+
else
|
256
|
+
encode_actions.kill
|
257
|
+
copy_actions.kill
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
files_to_keep = []
|
262
|
+
|
263
|
+
if File.file? primary
|
264
|
+
files = File.readlines(primary).map { |f| f.chomp }.uniq
|
265
|
+
if File.extname(primary) == '.cue'
|
266
|
+
file = files.select { |f| f =~ /^FILE/ }
|
267
|
+
files.map! { |f| f.match(/"(.*[^\\])"/)[1] }
|
268
|
+
end
|
269
|
+
primary = File.dirname(primary)
|
270
|
+
files.map! { |f| File.expand_path(f.strip, primary) }
|
271
|
+
files = files.select { |f| File.file? f }
|
272
|
+
files.uniq!
|
273
|
+
else
|
274
|
+
files = []
|
275
|
+
Find.find(primary) { |f| files << f unless File.directory? f}
|
276
|
+
end
|
277
|
+
|
278
|
+
files.each_with_index do |file, file_index|
|
279
|
+
next if File.directory? file
|
280
|
+
|
281
|
+
file_index += 1
|
282
|
+
file_rel_name = Pathname.new(file).relative_path_from(Pathname.new(primary))
|
283
|
+
file_rel_name = File.basename(primary) + '/' + file_rel_name.to_s
|
284
|
+
message "check [#{file_index}/#{files.size}] " + file_rel_name
|
285
|
+
|
286
|
+
# By making `file' into an AudioFile we gain memoization,
|
287
|
+
# which actually speeds things up quite a bit for some hard
|
288
|
+
# drives.
|
289
|
+
file = AudioFile.new(file)
|
290
|
+
|
291
|
+
encoding = self.class.determine_rule @encode_rules, file
|
292
|
+
encoding = Array(encoding) unless encoding.kind_of? Array
|
293
|
+
|
294
|
+
next if encoding[0] == :skip
|
295
|
+
path = determine_path file, primary, target, encoding
|
296
|
+
files_to_keep << path
|
297
|
+
|
298
|
+
if File.exist? path
|
299
|
+
clobber = self.class.determine_rule @clobber_rules, file
|
300
|
+
next unless clobber # if clobber == false
|
301
|
+
if clobber.kind_of? String
|
302
|
+
path = path.chomp(File.extname(path)) + clobber + File.extname(path)
|
303
|
+
files_to_keep << path
|
304
|
+
elsif clobber == :rename
|
305
|
+
i = '1'
|
306
|
+
begin
|
307
|
+
new_path = path.chomp(File.extname(path)) + " (#{i.succ!})" + File.extname(path)
|
308
|
+
files_to_keep << new_path
|
309
|
+
end while File.exist? new_path
|
310
|
+
path = new_path
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
path_rel_name = Pathname.new(path).relative_path_from(Pathname.new(target))
|
315
|
+
path_rel_name = File.basename(target) + '/' + path_rel_name.to_s
|
316
|
+
# copy rather than reencoding if possible
|
317
|
+
if encoding[0] == :copy ||
|
318
|
+
(encoding[0] != :reencode &&
|
319
|
+
(File.extname(path) == File.extname(file.path) && encoding[1].nil?))
|
320
|
+
n = copy_actions.total + 1
|
321
|
+
copy_actions << lambda do
|
322
|
+
message "copy [#{n}/#{copy_actions.total}] " + path_rel_name
|
323
|
+
FileUtils.mkdir_p File.dirname(path)
|
324
|
+
FileUtils.cp(file.path, path)
|
325
|
+
end
|
326
|
+
else
|
327
|
+
options = self.class.determine_rule @option_rules, file
|
328
|
+
effect_options = self.class.determine_rule @effect_option_rules, file
|
329
|
+
n = encode_actions.total + 1
|
330
|
+
encode_actions << lambda do
|
331
|
+
message "encode [#{n}/#{encode_actions.total}] " + path_rel_name
|
332
|
+
FileUtils.mkdir_p File.dirname(path)
|
333
|
+
options = "-C #{encoding[1]} " + options if encoding[1].kind_of? Numeric
|
334
|
+
file.encode path, options, effect_options
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
encode_actions.join
|
340
|
+
copy_actions.join
|
341
|
+
|
342
|
+
files_to_keep.uniq!
|
343
|
+
Find.find(target) do |f|
|
344
|
+
next if ! File.exist? f
|
345
|
+
next if File.directory? f
|
346
|
+
if self.class.determine_rule @remove_missing_rules, f
|
347
|
+
FileUtils.rm f unless files_to_keep.include? f
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
delete_empty_directories(target)
|
352
|
+
end
|
353
|
+
|
354
|
+
private
|
355
|
+
|
356
|
+
# Print + "\n" is used instead of puts because sometimes two puts
|
357
|
+
# strings are printed together and then the newlines together. I
|
358
|
+
# guess because of threading.
|
359
|
+
#
|
360
|
+
def message str
|
361
|
+
print str + "\n" unless @verbosity == 0
|
362
|
+
end
|
363
|
+
|
364
|
+
def delete_empty_directories directory
|
365
|
+
Dir.entries(directory).delete_if {|x| x =~ /^\.+$/ }.each do |dir|
|
366
|
+
dir = File.join(directory, dir)
|
367
|
+
next unless File.directory?(dir)
|
368
|
+
delete_empty_directories dir unless dir == directory
|
369
|
+
FileUtils.rm_r dir if Dir.entries(dir).delete_if { |x| x=~ /^\.+$/ }.empty?
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
def determine_path audiofile_or_path, primary, target, encoding
|
374
|
+
file = audiofile_or_path.kind_of?(AudioFile) ? audiofile_or_path.path : audiofile_or_path
|
375
|
+
|
376
|
+
path = self.class.determine_rule @path_rules, audiofile_or_path
|
377
|
+
if path == :original
|
378
|
+
relative = Pathname.new(file).relative_path_from(Pathname.new(primary))
|
379
|
+
path = File.expand_path(File.join(target, relative))
|
380
|
+
path = path.chomp(File.extname path)
|
381
|
+
else
|
382
|
+
# Not done yet:
|
383
|
+
artist = artist
|
384
|
+
album = album
|
385
|
+
track = track
|
386
|
+
title = title
|
387
|
+
path = ERB.new(File.join(@target, path)).result(binding)
|
388
|
+
end
|
389
|
+
|
390
|
+
ext = if [:reencode, :copy].include?(encoding[0])
|
391
|
+
File.extname file
|
392
|
+
else
|
393
|
+
encoding[0]
|
394
|
+
end
|
395
|
+
ext = '.' + ext if ext[0..0] != '.'
|
396
|
+
|
397
|
+
return path + ext
|
398
|
+
end
|
399
|
+
|
400
|
+
def separate_input arg, default_input, default_output='', &block
|
401
|
+
if arg.kind_of? Hash
|
402
|
+
raise "Hash must have one key-value pair." unless arg.size == 1
|
403
|
+
input = arg.keys.first
|
404
|
+
output = arg.values.first
|
405
|
+
else
|
406
|
+
if block
|
407
|
+
input = !arg.nil? ? arg : default_input
|
408
|
+
output = default_output
|
409
|
+
else
|
410
|
+
input = default_input
|
411
|
+
output = !arg.nil? ? arg : default_output
|
412
|
+
end
|
413
|
+
end
|
414
|
+
input = Array(input) unless input.kind_of? Array
|
415
|
+
return input, output
|
416
|
+
end
|
417
|
+
|
418
|
+
public
|
419
|
+
|
420
|
+
# Set a rule for how to encode a matching file. See the common
|
421
|
+
# documentation for Generator for input format. The action is a
|
422
|
+
# single object or an array consiting of a string indicating the
|
423
|
+
# new extension of the encoded file, and/or a number which is
|
424
|
+
# passed as the compression/quality level (see the -C,
|
425
|
+
# --compression option in the man pages for sox and soxformat).
|
426
|
+
#
|
427
|
+
# Normally a matched file will not be reencoded if the output is
|
428
|
+
# to have the same file extension, unless a specific compression
|
429
|
+
# level is given as well; then it will be reencoded.
|
430
|
+
#
|
431
|
+
# To force a file to be reencoded at the default compression level
|
432
|
+
# (for example, in order to update changed metadata) use the
|
433
|
+
# symbol :reencode as the rule.
|
434
|
+
#
|
435
|
+
# Alternatively, the rule can be either :copy or :skip, which have
|
436
|
+
# the obvious result.
|
437
|
+
#
|
438
|
+
def encode arg=[], &block
|
439
|
+
input, output = separate_input arg, [:audio], [], &block
|
440
|
+
output = Array(output) unless output.kind_of? Array
|
441
|
+
|
442
|
+
if (output & [:lossless, :lossy, :everything, :nonaudio, :audio]) != []
|
443
|
+
raise "Target specifier symbols are not valid for output."
|
444
|
+
end
|
445
|
+
raise "No output format specified." if !block && output.empty?
|
446
|
+
|
447
|
+
unless block
|
448
|
+
sym = []; int = []; str = []; other = 0
|
449
|
+
output.each do |x|
|
450
|
+
if x.kind_of? Symbol
|
451
|
+
sym << x
|
452
|
+
elsif x.kind_of? Numeric
|
453
|
+
int << x
|
454
|
+
elsif x.kind_of? String
|
455
|
+
str << x
|
456
|
+
else
|
457
|
+
other += 1
|
458
|
+
end
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
if [:everything, :nonaudio] & input != []
|
463
|
+
unless ([:copy, :skip] & output != []) || block
|
464
|
+
raise "only valid targets for :everything and :nonaudio are :copy and :skip"
|
465
|
+
end
|
466
|
+
raise "Bitrate not allowed with :everything and :nonaudio." if int.size > 0
|
467
|
+
end
|
468
|
+
|
469
|
+
unless block ||
|
470
|
+
(sym.size == 1 && int.empty? && str.empty? && other == 0) ||
|
471
|
+
(str.size + int.size >= 1 && str.size < 2 && int.size < 2 && other == 0 && sym.empty?)
|
472
|
+
raise "Output format incorrectly specified."
|
473
|
+
end
|
474
|
+
|
475
|
+
output = if ! sym.empty?
|
476
|
+
[sym[0]]
|
477
|
+
else
|
478
|
+
int.empty? ? [str[0]] : [str[0], int[0]]
|
479
|
+
end
|
480
|
+
|
481
|
+
input = InputRules.new(input, &block)
|
482
|
+
@encode_rules.delete_if { |x| x[0] == input }
|
483
|
+
@encode_rules << [input, output]
|
484
|
+
end
|
485
|
+
|
486
|
+
# An alias to encode using :skip as the action.
|
487
|
+
#
|
488
|
+
# gencode
|
489
|
+
#
|
490
|
+
def skip arg=[]
|
491
|
+
encode(arg=>:skip)
|
492
|
+
end
|
493
|
+
|
494
|
+
# An alias to encode using :copy as the action.
|
495
|
+
#
|
496
|
+
#
|
497
|
+
def copy arg=[]
|
498
|
+
encode(arg=>:copy)
|
499
|
+
end
|
500
|
+
|
501
|
+
# Currently not implemented.
|
502
|
+
#
|
503
|
+
# Set a rule for what path to use for the output of a matched
|
504
|
+
# file. See the common documentation for Generator for input
|
505
|
+
# format.
|
506
|
+
#
|
507
|
+
def path arg=[], &block
|
508
|
+
# TODO: implement configurable paths
|
509
|
+
input, output = separate_input arg, [:audio], '', &block
|
510
|
+
raise "not implemented yet" unless output==:original
|
511
|
+
raise "No output path specified." if !block && output == ''
|
512
|
+
raise "Output path incorrectly specified." unless block || output == :original || output.kind_of?(String)
|
513
|
+
if [:everything, :nonaudio] & input != []
|
514
|
+
raise unless (output == :original) || block
|
515
|
+
end
|
516
|
+
|
517
|
+
input = InputRules.new(input, &block)
|
518
|
+
@path_rules.delete_if { |x| x[0] == input }
|
519
|
+
@path_rules << [input, output]
|
520
|
+
end
|
521
|
+
|
522
|
+
# Set a rule for what option string to pass to sox for matched
|
523
|
+
# files. See the common documentation for Generator for input
|
524
|
+
# format. See man page for sox for available options.
|
525
|
+
#
|
526
|
+
def options arg=[], &block
|
527
|
+
input, output = separate_input arg, [:audio], '', &block
|
528
|
+
raise "Output path incorrectly specified." unless block || output.kind_of?(String)
|
529
|
+
raise if [:everything, :nonaudio] & input != []
|
530
|
+
|
531
|
+
input = InputRules.new(input, &block)
|
532
|
+
@option_rules.delete_if { |x| x[0] == input }
|
533
|
+
@option_rules << [input, output]
|
534
|
+
end
|
535
|
+
|
536
|
+
# Set a rule for what effect option string to pass to sox for
|
537
|
+
# matched files. See the common documentation for Generator for
|
538
|
+
# input format. See man page for sox for available options.
|
539
|
+
#
|
540
|
+
def effect_options arg=[], &block
|
541
|
+
input, output = separate_input arg, [:audio], '', &block
|
542
|
+
raise "Effect incorrectly specified." unless block || output.kind_of?(String)
|
543
|
+
raise if [:everything, :nonaudio] & input != []
|
544
|
+
|
545
|
+
input = InputRules.new(input, &block)
|
546
|
+
@effect_option_rules.delete_if { |x| x[0] == input }
|
547
|
+
@effect_option_rules << [input, output]
|
548
|
+
end
|
549
|
+
|
550
|
+
# Set a rule for whether to write over an existing file. The rule
|
551
|
+
# is matched against the *input* filename, not the output. See the
|
552
|
+
# common documentation for Generator for input format.
|
553
|
+
#
|
554
|
+
# The action part of the input can be True or False, the symbol
|
555
|
+
# :rename, or a string. If the action for a matched file is True,
|
556
|
+
# any existing file will be overwritten. If False, the file will
|
557
|
+
# be silently skipped. If :rename, a numbered suffix will be
|
558
|
+
# appended if necessary to create a unique name. If a string is
|
559
|
+
# given, that string will be appended to the filename (before the
|
560
|
+
# extension) if necessary to avoid writing over a file; however
|
561
|
+
# anything with that new filename will be silently overwritten.
|
562
|
+
#
|
563
|
+
def clobber arg=[], &block
|
564
|
+
input, output = separate_input arg, [:everything], true, &block
|
565
|
+
raise "Effect incorrectly specified." unless block ||
|
566
|
+
[String, NilClass, TrueClass, FalseClass].include?(output.class) || output == :rename
|
567
|
+
|
568
|
+
input = InputRules.new(input, &block)
|
569
|
+
@clobber_rules.delete_if { |x| x[0] == input }
|
570
|
+
@clobber_rules << [input, output]
|
571
|
+
end
|
572
|
+
|
573
|
+
# Set a rule for whether to remove a file from the target
|
574
|
+
# directory if there was no file in the source directory or
|
575
|
+
# playlist to generate it. The rule is matched against the *
|
576
|
+
# filename, not the output. See the common documentation for
|
577
|
+
# Generator for input format. Action values should be True or
|
578
|
+
# False.
|
579
|
+
#
|
580
|
+
def remove_missing arg=[], &block
|
581
|
+
input, output = separate_input arg, [:everything], true, &block
|
582
|
+
raise "Effect incorrectly specified." unless block ||
|
583
|
+
[NilClass, TrueClass, FalseClass].include?(output.class)
|
584
|
+
|
585
|
+
input = InputRules.new(input, &block)
|
586
|
+
@remove_missing_rules.delete_if { |x| x[0] == input }
|
587
|
+
@remove_missing_rules << [input, output]
|
588
|
+
end
|
589
|
+
end
|
590
|
+
|
591
|
+
# Create a new Generator instance, yield it to the block (or call
|
592
|
+
# instance_eval, if arity==0), and then call generate on the instance.
|
593
|
+
#
|
594
|
+
def self.generate *args, &block
|
595
|
+
g = Generator.new(&block)
|
596
|
+
g.generate(*args)
|
597
|
+
end
|
598
|
+
end
|
599
|
+
|
600
|
+
# tp = ThreadPool.new(2)
|
601
|
+
|
602
|
+
# int_level = 0
|
603
|
+
# trap("INT") do
|
604
|
+
# if int_level == 0
|
605
|
+
# int_level += 1
|
606
|
+
# puts "Will stop after current processes are finished; press CTRL-C again to stop immediately."
|
607
|
+
# tp.stop
|
608
|
+
# abort
|
609
|
+
# else
|
610
|
+
# tp.kill
|
611
|
+
# end
|
612
|
+
# end
|
613
|
+
|
614
|
+
# tp.join
|