lossfully 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,176 @@
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
+ module Lossfully
22
+ LOSSLESS_TYPES = %w(wav flac wv sox).map(&:to_sym)
23
+
24
+ # The InputRules class wraps up conditions and provides a way to
25
+ # test if a file meets those conditions. It also allows the
26
+ # conditions to be sorted so that more restrictive conditions are
27
+ # tried before less restrictive. The sorting hopefully does what
28
+ # seems natural; it looks at first at the regexp, then the file type
29
+ # (as returned by soxi -t), the file extension, the bitrate
30
+ # threshold, and finally if a block is given. For example, the
31
+ # following encode rules are shown in the order that they would be
32
+ # tested against every file (even though the rules would be checked
33
+ # in this order even if the below encode statements were in a
34
+ # different order):
35
+ #
36
+ # encode [:mp3, 128, /bach/] do
37
+ # ...
38
+ # end
39
+ # encode [:mp3, 128, /bach/] => ...
40
+ # encode [:mp3, /bach/] => ...
41
+ # encode [:mp3, 128] => ...
42
+ # encode :mp3 => ...
43
+ # encode :lossy => ...
44
+ # encode :audio => ...
45
+ # encode :everything => ...
46
+ #
47
+ # It's obviously only a partial order; see the code for
48
+ # compare_strictness if you need to know exactly what it's doing.
49
+ #
50
+ class InputRules
51
+ include Comparable
52
+
53
+ def initialize array=[], &block
54
+ raise unless array.kind_of? Array
55
+
56
+ @block = block
57
+
58
+ array.each do |x|
59
+ @type = x if x.kind_of? Symbol
60
+ @max_bitrate = x if x.kind_of? Numeric
61
+ if x.kind_of? String
62
+ @extension = (x[0..0] == '.') || x== '' ? x : '.' + x
63
+ end
64
+ @regexp = x if x.kind_of? Regexp
65
+ end
66
+ @type ||= :everything
67
+ @max_bitrate ||= 0
68
+ @extension ||= ''
69
+ @regexp ||= //
70
+ end
71
+
72
+ attr_reader :block, :extension, :regexp, :type, :max_bitrate
73
+
74
+ def test file_or_path
75
+ file = if file_or_path.kind_of? AudioFile
76
+ file_or_path
77
+ else
78
+ AudioFile.new(file_or_path)
79
+ end
80
+ # unless file.is_audio?
81
+ # return false unless [:everything, :nonaudio].include?(@type)
82
+ # return false unless file.path =~ @regexp
83
+ # (return block.call(file.path)) if @block
84
+ # return true
85
+ # end
86
+
87
+ if @type != :everything
88
+ if [:audio, :lossy, :lossless].include? @type
89
+ return false unless file.is_audio?
90
+ end
91
+
92
+ if @type == :lossy
93
+ return false if LOSSLESS_TYPES.include? file.type
94
+ elsif @type == :lossless
95
+ return false unless LOSSLESS_TYPES.include? file.type
96
+ elsif @type == :nonaudio
97
+ return false if file.is_audio?
98
+ elsif @type != :audio
99
+ v = [:vorbis, :ogg]
100
+ return false unless (file.type == @type) ||
101
+ (v.include?(file.type) && v.include?(@type))
102
+ end
103
+ end
104
+
105
+ if @max_bitrate > 0
106
+ return false unless file.bitrate_kbps > @max_bitrate
107
+ end
108
+
109
+ if @extension != ''
110
+ return false unless File.extname(file.path) == @extension
111
+ end
112
+
113
+ if @regexp != //
114
+ return false unless file.path =~ @regexp
115
+ end
116
+
117
+ if @block
118
+ # return block.call(file.path)
119
+ # TODO: decide if this should be file or file.path
120
+ return block.call(file)
121
+ end
122
+
123
+ return true
124
+ end
125
+
126
+ # Order by strictness, which is the proper order to test things in
127
+ def <=> x
128
+ -1 * compare_strictness(x)
129
+ end
130
+
131
+ # return -1 if self is less strict, 1 if self is more strict
132
+ def compare_strictness x
133
+ return nil unless x.class == self.class
134
+
135
+ if @regexp != x.regexp
136
+ return -1 if @regexp == //
137
+ return 1 if x.regexp == //
138
+ return nil
139
+ end
140
+
141
+ if @type != x.type
142
+ return -1 if @type == :everything
143
+ return 1 if x.type == :everything
144
+ return -1 if @type == :audio
145
+ return 1 if x.type == :audio
146
+ # these don't have to be comparable since they're mutual exclusive
147
+ return -1 if @type == :nonaudio
148
+ return 1 if x.type == :nonaudio
149
+ return -1 if @type == :lossless
150
+ return 1 if x.type == :lossless
151
+ return -1 if @type == :lossy
152
+ return 1 if x.type == :lossy
153
+ return -1 * (@type.to_s <=> x.type.to_s)
154
+ end
155
+
156
+ if @extension != x.extension
157
+ return -1 if @extension == ''
158
+ return 1 if x.extension == ''
159
+ return -1 * (@extension <=> x.extension)
160
+ end
161
+
162
+ b = @max_bitrate <=> x.max_bitrate
163
+ return b unless b == 0
164
+
165
+ if @block || x.block
166
+ return nil if @block && x.block
167
+ return -1 if ! @block
168
+ return 1 if ! x.block
169
+ end
170
+
171
+ return 0
172
+ end
173
+ end
174
+
175
+ end
176
+
@@ -0,0 +1,162 @@
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 'thread'
22
+ require 'timeout'
23
+
24
+ module Lossfully
25
+
26
+ # There's (at least) two ways to do this: 1) the Ruby Recipes way,
27
+ # which is to make a thread for every incoming task, but put it to
28
+ # sleep until there's room in the pool for another task, or 2) have a
29
+ # pool of threads that eat tasks from a queue. This implements (2),
30
+ # mainly because it seemed more fun to me. But also because it
31
+ # doesn't require the explicit use of Mutexes at all; it uses them,
32
+ # for the sake of sending signals with ConditionVaribles, but if those
33
+ # signals aren't received there would be a delay of at most 1 second.
34
+ #
35
+ # Another useful thing about this implementation is for the case when
36
+ # every task you anticipate adding to the ThreadPool of the same
37
+ # general form. Then then ThreadPool can be initialized with a block
38
+ # and you can just add objects to the task queue.
39
+ #
40
+ class ThreadPool
41
+
42
+ DEFAULT_BLOCK = lambda {|block, &blk| block = blk if block_given? ; block.call}
43
+
44
+ def initialize(max_size = 1, block=nil, &blk)
45
+ @running = true
46
+ @joining = false
47
+
48
+ @mutex = Mutex.new
49
+ @cv = ConditionVariable.new
50
+ @max_size = max_size
51
+ block = blk if block_given?
52
+ @block = block.nil? ? DEFAULT_BLOCK : block
53
+ @queue = Queue.new
54
+ @workers = []
55
+ @master = master_thread
56
+ @completed = 0
57
+ @total = 0
58
+ end
59
+
60
+ def process (block_or_item=nil, &blk)
61
+ block_or_item = blk if block_given?
62
+ if block_or_item.respond_to?(:call)
63
+ @queue << block_or_item
64
+ else
65
+ @queue << lambda { @block.call(block_or_item) }
66
+ end
67
+ # @mutex.synchronize { @total +=1 }
68
+ @total += 1
69
+ signal_master
70
+ end
71
+
72
+ def current
73
+ @total - @queue.size
74
+ end
75
+
76
+ attr_reader :max_size, :mutex, :completed, :total
77
+ alias :enq :process
78
+ alias :dispatch :process
79
+
80
+ def max_size=(size)
81
+ @max_size = size
82
+ signal_master
83
+ end
84
+
85
+ def << block_or_item
86
+ process block_or_item
87
+ end
88
+
89
+ def join
90
+ @running = false
91
+ @joining = true
92
+ signal_master
93
+ # A weird bug happens on this next line if you don't test
94
+ # @master.alive?, but only sometimes. I don't care enough to
95
+ # figure it out right now.
96
+ @master.join if @master.alive?
97
+ end
98
+
99
+ def size
100
+ @workers.size
101
+ end
102
+
103
+ def queue_size
104
+ @queue.size
105
+ end
106
+
107
+ def stop
108
+ @queue.clear
109
+ join
110
+ end
111
+
112
+ def kill
113
+ @queue.clear
114
+ @workers.each(&:kill)
115
+ join
116
+ end
117
+
118
+ private
119
+
120
+ def signal_master
121
+ @mutex.synchronize { @cv.signal }
122
+ end
123
+
124
+ def master_thread
125
+ Thread.new do
126
+ while @running || ! @queue.empty?
127
+
128
+ @workers ||= []
129
+ @workers.delete_if { |w| ! w.alive? }
130
+
131
+ while @workers.size < @max_size && @queue.size > 0
132
+ @workers << Thread.new do
133
+ begin
134
+ if task = @queue.pop(true) rescue nil
135
+ task.call
136
+ @mutex.synchronize { @completed +=1 }
137
+ end
138
+ ensure
139
+ signal_master
140
+ end
141
+ end
142
+ end
143
+
144
+ @mutex.synchronize do
145
+ # @cv.wait(@mutex, 1) # can't do this in 1.8.7
146
+ begin
147
+ Timeout::timeout(2) { @cv.wait(@mutex) }
148
+ rescue Timeout::Error
149
+ end
150
+ end
151
+ # This needs to come after the critical section above,
152
+ # otherwise the main thread will have to wait for the timeout
153
+ # before continuing when the ThreadPool is joined. The rescue
154
+ # below handles exceptions that might have happened in the
155
+ # threads, which will stop the main thread now that they're
156
+ # being joined.
157
+ @workers.each(&:join) if @joining rescue nil
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
data/lib/lossfully.rb ADDED
@@ -0,0 +1,79 @@
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
+ module Lossfully
22
+
23
+ # :stopdoc:
24
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
25
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
26
+ # :startdoc:
27
+ VERSION = ::File.read(PATH + 'version.txt').strip
28
+
29
+ # Returns the library path for the module. If any arguments are given,
30
+ # they will be joined to the end of the libray path using
31
+ # <tt>File.join</tt>.
32
+ #
33
+ def self.libpath( *args )
34
+ rv = args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
35
+ if block_given?
36
+ begin
37
+ $LOAD_PATH.unshift LIBPATH
38
+ rv = yield
39
+ ensure
40
+ $LOAD_PATH.shift
41
+ end
42
+ end
43
+ return rv
44
+ end
45
+
46
+ # Returns the lpath for the module. If any arguments are given,
47
+ # they will be joined to the end of the path using
48
+ # <tt>File.join</tt>.
49
+ #
50
+ def self.path( *args )
51
+ rv = args.empty? ? PATH : ::File.join(PATH, args.flatten)
52
+ if block_given?
53
+ begin
54
+ $LOAD_PATH.unshift PATH
55
+ rv = yield
56
+ ensure
57
+ $LOAD_PATH.shift
58
+ end
59
+ end
60
+ return rv
61
+ end
62
+
63
+ # Utility method used to require all files ending in .rb that lie in the
64
+ # directory below this file that has the same name as the filename passed
65
+ # in. Optionally, a specific _directory_ name can be passed in such that
66
+ # the _filename_ does not have to be equivalent to the directory.
67
+ #
68
+ def self.require_all_libs_relative_to( fname, dir = nil )
69
+ dir ||= ::File.basename(fname, '.*')
70
+ search_me = ::File.expand_path(
71
+ ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
72
+
73
+ Dir.glob(search_me).sort.each {|rb| require rb}
74
+ end
75
+
76
+ end
77
+
78
+ Lossfully.require_all_libs_relative_to(__FILE__)
79
+
@@ -0,0 +1,47 @@
1
+ require 'test/unit'
2
+
3
+ $:.unshift File.dirname(__FILE__) + '/../lib'
4
+ require 'lossfully'
5
+
6
+ module TestLossfully
7
+ class TestAudioFile < Test::Unit::TestCase
8
+
9
+ def test_encoding
10
+ assert_raise RuntimeError do Lossfully::AudioFile.encoding('test/data/this_file_does_not_exist') end
11
+ assert ! Lossfully::AudioFile.is_audio?('test/data/text.txt')
12
+ assert Lossfully::AudioFile.encoding('test/data/so_sad.ogg')
13
+ end
14
+
15
+ def test_bitrate
16
+ assert_equal '114k', Lossfully::AudioFile.bitrate('test/data/so_sad.ogg')
17
+ end
18
+
19
+ def test_bitrate_kbps
20
+ assert_equal 114, Lossfully::AudioFile.bitrate_kbps('test/data/so_sad.ogg')
21
+ assert_equal 2820, Lossfully::AudioFile.bitrate_kbps('test/data/so_sad.sox')
22
+ end
23
+
24
+ def test_duration
25
+ f = Lossfully::AudioFile.new('test/data/so_sad.ogg')
26
+ assert_equal 6.047959, f.duration
27
+ end
28
+
29
+ def test_class_encode
30
+ input = 'test/data/so_sad.flac'
31
+ output = 'test/data/so_sad.wav'
32
+ FileUtils.rm output if File.exist? output
33
+ Lossfully::AudioFile.encode input, output
34
+ assert_equal :wav, Lossfully::AudioFile.type(output)
35
+ end
36
+
37
+ def test_encode
38
+ input = 'test/data/so_sad.flac'
39
+ f = Lossfully::AudioFile.new input
40
+ output = 'test/data/so_sad.wav'
41
+ FileUtils.rm output if File.exist? output
42
+ f.encode output
43
+ assert_equal :wav, Lossfully::AudioFile.type(output)
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,125 @@
1
+ require 'test/unit'
2
+
3
+ $:.unshift File.dirname(__FILE__) + '/../lib'
4
+ require 'lossfully'
5
+
6
+ module TestLossfully
7
+ class TestInputRules < Test::Unit::TestCase
8
+
9
+ def test_test
10
+ r1 = Lossfully::InputRules.new [:flac]
11
+ r2 = Lossfully::InputRules.new [:everything]
12
+ r2 = Lossfully::InputRules.new [:nonaudio]
13
+ f1 = 'test/data/text.txt'
14
+ assert !(r1.test f1)
15
+ assert r2.test f1
16
+
17
+ r1 = Lossfully::InputRules.new [:flac]
18
+ r2 = Lossfully::InputRules.new [:ogg]
19
+ r3 = Lossfully::InputRules.new [:everything]
20
+ r4 = Lossfully::InputRules.new [:lossy]
21
+ r5 = Lossfully::InputRules.new [:lossless]
22
+ r6 = Lossfully::InputRules.new [:audio]
23
+ r7 = Lossfully::InputRules.new [:nonaudio]
24
+ f1 = 'test/data/so_sad.flac'
25
+ f2 = 'test/data/so_sad.ogg'
26
+ assert r1.test f1
27
+ assert r2.test f2
28
+ assert r3.test f1
29
+ assert r3.test f2
30
+ assert r4.test f2
31
+ assert r5.test f1
32
+ assert r6.test f1
33
+ assert r6.test f2
34
+ assert ! (r4.test f1)
35
+ assert ! (r5.test f2)
36
+
37
+ r1 = Lossfully::InputRules.new [100]
38
+ r2 = Lossfully::InputRules.new [128]
39
+ f1 = 'test/data/so_sad.ogg'
40
+ assert r1.test f1
41
+ assert !(r2.test f1)
42
+
43
+ r1 = Lossfully::InputRules.new ['.ogg']
44
+ r2 = Lossfully::InputRules.new ['.flac']
45
+ f1 = 'test/data/so_sad.ogg'
46
+ f2 = 'test/data/so_sad.flac'
47
+ assert r1.test f1
48
+ assert r2.test f2
49
+ assert !(r1.test f2)
50
+ assert !(r2.test f1)
51
+
52
+ r1 = Lossfully::InputRules.new [/sad/]
53
+ r2 = Lossfully::InputRules.new [/happy/]
54
+ f1 = Lossfully::AudioFile.new 'test/data/so_sad.ogg'
55
+ assert r1.test f1
56
+ assert !(r2.test f1)
57
+
58
+ r1 = Lossfully::InputRules.new do |f|
59
+ [:mp3] if File.dirname(f.path) == 'test/data'
60
+ end
61
+ f1 = Lossfully::AudioFile.new 'test/data/so_sad.ogg'
62
+ assert r1.test f1
63
+
64
+ r1 = Lossfully::InputRules.new [:ogg, 100] do |f|
65
+ [:mp3] if File.dirname(f.path) == 'test/data'
66
+ end
67
+ assert r1.test f1
68
+
69
+ r1 = Lossfully::InputRules.new [:ogg, 128] do |f|
70
+ [:mp3] if File.dirname(f.path) == 'test/data'
71
+ end
72
+ assert !(r1.test f1)
73
+ end
74
+
75
+ def test_comparison
76
+ r1 = Lossfully::InputRules.new [:mp3, /mp3/]
77
+ r2 = Lossfully::InputRules.new [:mp3, /mp3/]
78
+ r3 = Lossfully::InputRules.new [:mp3]
79
+ r4 = Lossfully::InputRules.new { false }
80
+ a = [r1]
81
+ assert a.include? r2
82
+ assert !(a.include? r3)
83
+ assert !(a.include? r4)
84
+
85
+ r1 = Lossfully::InputRules.new { false }
86
+ r2 = Lossfully::InputRules.new
87
+ r3 = Lossfully::InputRules.new [:mp3, /regexp/, 192]
88
+ r4 = Lossfully::InputRules.new [:mp3, /regexp/, 192] do false end
89
+ assert r1 < r2
90
+ assert r3 < r1
91
+ assert r4 < r3
92
+
93
+ r1 = Lossfully::InputRules.new [:everything]
94
+ r2 = Lossfully::InputRules.new [:lossy]
95
+ r3 = Lossfully::InputRules.new [:lossless]
96
+ r4 = Lossfully::InputRules.new [:audio]
97
+ r5 = Lossfully::InputRules.new [:nonaudio]
98
+ assert r2 < r1
99
+ assert r3 < r1
100
+ assert r4 < r1
101
+ assert r5 < r1
102
+ assert r2 < r4
103
+ assert r3 < r4
104
+
105
+ r1 = Lossfully::InputRules.new ['']
106
+ r2 = Lossfully::InputRules.new ['ogg']
107
+ assert r2 < r1
108
+
109
+ r1 = Lossfully::InputRules.new [192]
110
+ r2 = Lossfully::InputRules.new [128]
111
+ assert r1 < r2
112
+
113
+ r1 = Lossfully::InputRules.new [//]
114
+ r2 = Lossfully::InputRules.new [/a/]
115
+ r3 = Lossfully::InputRules.new [/b/]
116
+ assert r2 < r1
117
+ assert_not_equal r2, r3
118
+
119
+ r1 = Lossfully::InputRules.new [:ogg, 192]
120
+ r2 = Lossfully::InputRules.new [:ogg, /a/]
121
+ assert r2 < r1
122
+ end
123
+
124
+ end
125
+ end
@@ -0,0 +1,77 @@
1
+ require 'test/unit'
2
+
3
+ $:.unshift File.dirname(__FILE__) + '/../lib'
4
+ require 'lossfully'
5
+
6
+ module TestLossfully
7
+ class TestThreadPool < Test::Unit::TestCase
8
+
9
+ def test_everything
10
+ a = []
11
+ tp = Lossfully::ThreadPool.new(2)
12
+ tp.max_size = 3
13
+ assert_equal 3, tp.max_size
14
+ tp.process { a << 1}
15
+ tp.process { a << 2 }
16
+ tp.process { a << 3 }
17
+ tp.join
18
+ assert a.include? 1
19
+ assert a.include? 2
20
+ assert a.include? 3
21
+ end
22
+
23
+ def test_everything_with_auto_blocks
24
+ a = []
25
+ tp = Lossfully::ThreadPool.new(2) do |x|
26
+ a << x
27
+ end
28
+ tp << 1
29
+ tp << 2
30
+ tp << 3
31
+ tp.join
32
+ assert a.include? 1
33
+ assert a.include? 2
34
+ assert a.include? 3
35
+ end
36
+
37
+ def test_stop
38
+ tp = Lossfully::ThreadPool.new(2)
39
+ r1 = false
40
+ r2 = false
41
+ r3 = false
42
+
43
+ tp.process { sleep 0.3; r1 = true}
44
+ tp.process { sleep 0.3; r2 = true}
45
+ tp.process { sleep 0.3; r3 = true}
46
+ 2.times { Thread.pass } and sleep 0.1
47
+ assert_equal 1, tp.queue_size
48
+ assert_equal 2, tp.size
49
+ tp.stop
50
+ sleep 0.4
51
+
52
+ assert r1, 'first task finished'
53
+ assert r2, 'second task finished'
54
+ assert ! r3, 'third task did not finish'
55
+ end
56
+
57
+ def test_kill
58
+ tp = Lossfully::ThreadPool.new(2)
59
+ r1 = false
60
+ r2 = false
61
+ r3 = false
62
+
63
+ tp.process { sleep 0.3; r1 = true}
64
+ tp.process { sleep 0.3; r2 = true}
65
+ tp.process { sleep 0.3; r3 = true}
66
+ 2.times { Thread.pass } and sleep 0.1
67
+
68
+ tp.kill
69
+ sleep 0.4
70
+
71
+ assert ! r1, 'first task did not finished'
72
+ assert ! r2, 'second task did not finished'
73
+ assert ! r3, 'third task did not finish'
74
+ end
75
+
76
+ end
77
+ end
data/version.txt ADDED
@@ -0,0 +1 @@
1
+ 0.0.0