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.
@@ -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