typingpool 0.7.0 → 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.markdown +452 -0
- data/lib/typingpool/amazon/hit/assignment/empty.rb +19 -0
- data/lib/typingpool/amazon/hit/assignment.rb +43 -0
- data/lib/typingpool/amazon/hit/full/fromsearchhits.rb +44 -0
- data/lib/typingpool/amazon/hit/full.rb +105 -0
- data/lib/typingpool/amazon/hit.rb +458 -0
- data/lib/typingpool/amazon/question.rb +45 -0
- data/lib/typingpool/amazon.rb +3 -677
- data/lib/typingpool/app/cli/formatter.rb +16 -0
- data/lib/typingpool/app/cli.rb +64 -0
- data/lib/typingpool/app/friendlyexceptions.rb +34 -0
- data/lib/typingpool/app.rb +2 -97
- data/lib/typingpool/config/root.rb +114 -0
- data/lib/typingpool/config.rb +13 -119
- data/lib/typingpool/filer/audio.rb +84 -0
- data/lib/typingpool/filer/csv.rb +57 -0
- data/lib/typingpool/filer/dir.rb +76 -0
- data/lib/typingpool/filer/files/audio.rb +63 -0
- data/lib/typingpool/filer/files.rb +55 -0
- data/lib/typingpool/filer.rb +4 -313
- data/lib/typingpool/project/local.rb +117 -0
- data/lib/typingpool/project/remote/s3.rb +135 -0
- data/lib/typingpool/project/remote/sftp.rb +100 -0
- data/lib/typingpool/project/remote.rb +65 -0
- data/lib/typingpool/project.rb +2 -396
- data/lib/typingpool/template/assignment.rb +17 -0
- data/lib/typingpool/template/env.rb +77 -0
- data/lib/typingpool/template.rb +2 -87
- data/lib/typingpool/test/script.rb +310 -0
- data/lib/typingpool/test.rb +1 -306
- data/lib/typingpool/transcript/chunk.rb +129 -0
- data/lib/typingpool/transcript.rb +1 -125
- data/lib/typingpool/utility/castable.rb +65 -0
- data/lib/typingpool/utility.rb +1 -61
- data/test/test_integration_script_6_tp_finish.rb +1 -0
- metadata +135 -81
@@ -0,0 +1,16 @@
|
|
1
|
+
module Typingpool
|
2
|
+
module App
|
3
|
+
module CLI
|
4
|
+
module Formatter
|
5
|
+
require 'highline/import'
|
6
|
+
def cli_bold(text)
|
7
|
+
HighLine.color(text, :bold)
|
8
|
+
end
|
9
|
+
|
10
|
+
def cli_reverse(text)
|
11
|
+
HighLine.color(text, :reverse)
|
12
|
+
end
|
13
|
+
end #Formatter
|
14
|
+
end #CLI
|
15
|
+
end #App
|
16
|
+
end #Typingpool
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Typingpool
|
2
|
+
module App
|
3
|
+
module CLI
|
4
|
+
class << self
|
5
|
+
include App::FriendlyExceptions
|
6
|
+
|
7
|
+
#Optionally takes an ostensible path to a config file, as passed
|
8
|
+
#as a command-line option. Checks to make sure the file exists;
|
9
|
+
#returns nil if does not, returns a Config instance if it
|
10
|
+
#does. If no path is passed, the default config file is returned
|
11
|
+
#(as retrieved by Config.file with no args).
|
12
|
+
def config_from_arg(arg=nil)
|
13
|
+
if arg
|
14
|
+
path = File.expand_path(arg)
|
15
|
+
return unless File.exists?(path) && File.file?(path)
|
16
|
+
Config.file(path)
|
17
|
+
else
|
18
|
+
Config.file
|
19
|
+
end #if option
|
20
|
+
end
|
21
|
+
|
22
|
+
#Outputs a friendly explanation of the --help option for
|
23
|
+
#appending to script usage banners.
|
24
|
+
def help_arg_explanation
|
25
|
+
"`#{File.basename($PROGRAM_NAME)} --help` for more information."
|
26
|
+
end
|
27
|
+
|
28
|
+
#Converts a user arg into a Project instance, setting up or
|
29
|
+
#consulting a Config along the way.
|
30
|
+
# ==== Params
|
31
|
+
# [arg] A user-supplied argument specifying either an absolute
|
32
|
+
# path to a Project folder (Project#local) or the
|
33
|
+
# name of a project folder within
|
34
|
+
# [config]#transcripts.
|
35
|
+
# [config] A Config instance. If [arg] is an absolute path,
|
36
|
+
# will be modified -- Config#itranscripts will be
|
37
|
+
# changed to match the implied transcripts dir.
|
38
|
+
# ==== Errors
|
39
|
+
# Will abort with a friendly message on any errors.
|
40
|
+
# ==== Returns
|
41
|
+
# A Project instance.
|
42
|
+
def project_from_arg_and_config(arg, config)
|
43
|
+
path = if (File.exists?(arg) && File.directory?(arg))
|
44
|
+
config.transcripts = File.dirname(arg)
|
45
|
+
arg
|
46
|
+
else
|
47
|
+
abort "No 'transcripts' dir specified in your config file and '#{arg}' is not a valid path" unless config.transcripts
|
48
|
+
path = File.join(config.transcripts, arg)
|
49
|
+
abort "No such project '#{arg}' in dir '#{config.transcripts}'" unless File.exists? path
|
50
|
+
abort "'#{arg}' is not a directory at '#{path}'" unless File.directory? path
|
51
|
+
path
|
52
|
+
end
|
53
|
+
project = with_friendly_exceptions('project name', File.basename(path)) do
|
54
|
+
Typingpool::Project.new(File.basename(path), config)
|
55
|
+
end
|
56
|
+
abort "Not a project directory at '#{path}'" unless project.local
|
57
|
+
project
|
58
|
+
end
|
59
|
+
|
60
|
+
end #class << self
|
61
|
+
require 'typingpool/app/cli/formatter'
|
62
|
+
end #CLI
|
63
|
+
end #App
|
64
|
+
end #Typingpool
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Typingpool
|
2
|
+
module App
|
3
|
+
module FriendlyExceptions
|
4
|
+
|
5
|
+
#Massages terse exceptions from our model layer into a
|
6
|
+
#human-friendly message suitable for an abort message from a
|
7
|
+
#command-line script.
|
8
|
+
# ==== Params
|
9
|
+
# [name] A string used to refer to the input. For example
|
10
|
+
# 'project title' or '--config argument'. Used in the
|
11
|
+
# goodbye message.
|
12
|
+
# [*input] One or more values. The user input that will cause
|
13
|
+
# any exceptions. Used in the goodbye message.
|
14
|
+
# [&block] The block to execute and monitor for
|
15
|
+
# exceptions. Will be passed [*input].
|
16
|
+
# ==== Errors
|
17
|
+
# Will abort with a friendly message on any exception of the
|
18
|
+
# type Typingpool::Error::Argument.
|
19
|
+
# ==== Returns
|
20
|
+
# The return value of &block.
|
21
|
+
def with_friendly_exceptions(name, *input)
|
22
|
+
begin
|
23
|
+
yield(*input)
|
24
|
+
rescue Typingpool::Error::Argument => exception
|
25
|
+
goodbye = "Could not make sense of #{name.to_s} "
|
26
|
+
goodbye += input.map{|input| "'#{input}'" }.join(', ')
|
27
|
+
goodbye += ". #{exception.message}"
|
28
|
+
goodbye += '.' unless goodbye.match(/\.$/)
|
29
|
+
abort goodbye
|
30
|
+
end #begin
|
31
|
+
end
|
32
|
+
end #FriendlyExceptions
|
33
|
+
end #App
|
34
|
+
end #Typingpool
|
data/lib/typingpool/app.rb
CHANGED
@@ -533,102 +533,7 @@ module Typingpool
|
|
533
533
|
end #assignments.each!...
|
534
534
|
end
|
535
535
|
end #class << self
|
536
|
-
|
537
|
-
|
538
|
-
#human-friendly message suitable for an abort message from a
|
539
|
-
#command-line script.
|
540
|
-
# ==== Params
|
541
|
-
# [name] A string used to refer to the input. For example
|
542
|
-
# 'project title' or '--config argument'. Used in the
|
543
|
-
# goodbye message.
|
544
|
-
# [*input] One or more values. The user input that will cause
|
545
|
-
# any exceptions. Used in the goodbye message.
|
546
|
-
# [&block] The block to execute and monitor for
|
547
|
-
# exceptions. Will be passed [*input].
|
548
|
-
# ==== Errors
|
549
|
-
# Will abort with a friendly message on any exception of the
|
550
|
-
# type Typingpool::Error::Argument.
|
551
|
-
# ==== Returns
|
552
|
-
# The return value of &block.
|
553
|
-
def with_friendly_exceptions(name, *input)
|
554
|
-
begin
|
555
|
-
yield(*input)
|
556
|
-
rescue Typingpool::Error::Argument => exception
|
557
|
-
goodbye = "Could not make sense of #{name.to_s} "
|
558
|
-
goodbye += input.map{|input| "'#{input}'" }.join(', ')
|
559
|
-
goodbye += ". #{exception.message}"
|
560
|
-
goodbye += '.' unless goodbye.match(/\.$/)
|
561
|
-
abort goodbye
|
562
|
-
end #begin
|
563
|
-
end
|
564
|
-
end #FriendlyExceptions
|
565
|
-
module CLI
|
566
|
-
class << self
|
567
|
-
include App::FriendlyExceptions
|
568
|
-
#Optionally takes an ostensible path to a config file, as passed
|
569
|
-
#as a command-line option. Checks to make sure the file exists;
|
570
|
-
#returns nil if does not, returns a Config instance if it
|
571
|
-
#does. If no path is passed, the default config file is returned
|
572
|
-
#(as retrieved by Config.file with no args).
|
573
|
-
def config_from_arg(arg=nil)
|
574
|
-
if arg
|
575
|
-
path = File.expand_path(arg)
|
576
|
-
return unless File.exists?(path) && File.file?(path)
|
577
|
-
Config.file(path)
|
578
|
-
else
|
579
|
-
Config.file
|
580
|
-
end #if option
|
581
|
-
end
|
582
|
-
|
583
|
-
#Outputs a friendly explanation of the --help option for
|
584
|
-
#appending to script usage banners.
|
585
|
-
def help_arg_explanation
|
586
|
-
"`#{File.basename($PROGRAM_NAME)} --help` for more information."
|
587
|
-
end
|
588
|
-
|
589
|
-
#Converts a user arg into a Project instance, setting up or
|
590
|
-
#consulting a Config along the way.
|
591
|
-
# ==== Params
|
592
|
-
# [arg] A user-supplied argument specifying either an absolute
|
593
|
-
# path to a Project folder (Project#local) or the
|
594
|
-
# name of a project folder within
|
595
|
-
# [config]#transcripts.
|
596
|
-
# [config] A Config instance. If [arg] is an absolute path,
|
597
|
-
# will be modified -- Config#itranscripts will be
|
598
|
-
# changed to match the implied transcripts dir.
|
599
|
-
# ==== Errors
|
600
|
-
# Will abort with a friendly message on any errors.
|
601
|
-
# ==== Returns
|
602
|
-
# A Project instance.
|
603
|
-
def project_from_arg_and_config(arg, config)
|
604
|
-
path = if (File.exists?(arg) && File.directory?(arg))
|
605
|
-
config.transcripts = File.dirname(arg)
|
606
|
-
arg
|
607
|
-
else
|
608
|
-
abort "No 'transcripts' dir specified in your config file and '#{arg}' is not a valid path" unless config.transcripts
|
609
|
-
path = File.join(config.transcripts, arg)
|
610
|
-
abort "No such project '#{arg}' in dir '#{config.transcripts}'" unless File.exists? path
|
611
|
-
abort "'#{arg}' is not a directory at '#{path}'" unless File.directory? path
|
612
|
-
path
|
613
|
-
end
|
614
|
-
project = with_friendly_exceptions('project name', File.basename(path)) do
|
615
|
-
Typingpool::Project.new(File.basename(path), config)
|
616
|
-
end
|
617
|
-
abort "Not a project directory at '#{path}'" unless project.local
|
618
|
-
project
|
619
|
-
end
|
620
|
-
|
621
|
-
end #class << self
|
622
|
-
module Formatter
|
623
|
-
require 'highline/import'
|
624
|
-
def cli_bold(text)
|
625
|
-
HighLine.color(text, :bold)
|
626
|
-
end
|
627
|
-
|
628
|
-
def cli_reverse(text)
|
629
|
-
HighLine.color(text, :reverse)
|
630
|
-
end
|
631
|
-
end #Formatter
|
632
|
-
end #CLI
|
536
|
+
require 'typingpool/app/friendlyexceptions'
|
537
|
+
require 'typingpool/app/cli'
|
633
538
|
end #App
|
634
539
|
end #Typingpool
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module Typingpool
|
2
|
+
class Config
|
3
|
+
|
4
|
+
#The root level of the config file and all full config
|
5
|
+
#objects. Kept distinct from Config because other subclasses need
|
6
|
+
#to inherit from Config, and we don't want them inheriting the
|
7
|
+
#root level fields.
|
8
|
+
class Root < Config
|
9
|
+
local_path_reader :transcripts, :cache, :templates
|
10
|
+
|
11
|
+
class SFTP < Config
|
12
|
+
never_ends_in_slash_reader :path, :url
|
13
|
+
end
|
14
|
+
|
15
|
+
class Amazon < Config
|
16
|
+
never_ends_in_slash_reader :url
|
17
|
+
end
|
18
|
+
|
19
|
+
class Assign < Config
|
20
|
+
local_path_reader :templates
|
21
|
+
time_accessor :deadline, :approval, :lifetime
|
22
|
+
|
23
|
+
define_accessor(:reward) do |value|
|
24
|
+
value.to_s.match(/(\d+(\.\d+)?)|(\d*\.\d+)/) or raise Error::Argument::Format, "Format should be N.NN"
|
25
|
+
value
|
26
|
+
end
|
27
|
+
|
28
|
+
define_reader(:confirm) do |value|
|
29
|
+
next false if value.to_s.match(/(^n)|(^0)|(^false)/i)
|
30
|
+
next true if value.to_s.match(/(^y)|(^1)|(^true)/i)
|
31
|
+
next if value.to_s.empty?
|
32
|
+
raise Error::Argument::Format, "Format should be 'yes' or 'no'"
|
33
|
+
end
|
34
|
+
|
35
|
+
def qualify
|
36
|
+
self.qualify = (@param['qualify'] || []) unless @qualify
|
37
|
+
@qualify
|
38
|
+
end
|
39
|
+
|
40
|
+
def qualify=(specs)
|
41
|
+
@qualify = specs.map{|spec| Qualification.new(spec) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_qualification(spec)
|
45
|
+
self.qualify.push(Qualification.new(spec))
|
46
|
+
end
|
47
|
+
|
48
|
+
def keywords
|
49
|
+
@param['keywords'] ||= []
|
50
|
+
end
|
51
|
+
|
52
|
+
def keywords=(array)
|
53
|
+
@param['keywords'] = array
|
54
|
+
end
|
55
|
+
|
56
|
+
class Qualification < Config
|
57
|
+
def initialize(spec)
|
58
|
+
@raw = spec
|
59
|
+
to_arg #make sure value parses
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_s
|
63
|
+
@raw
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_arg
|
67
|
+
[type, opts]
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
72
|
+
def type
|
73
|
+
type = @raw.split(/\s+/)[0]
|
74
|
+
if RTurk::Qualification::TYPES[type.to_sym]
|
75
|
+
return type.to_sym
|
76
|
+
elsif (type.match(/\d/) || type.size >= 25)
|
77
|
+
return type
|
78
|
+
else
|
79
|
+
#Seems likely to be qualification typo: Not a known
|
80
|
+
#system qualification, all letters and less than 25
|
81
|
+
#chars
|
82
|
+
raise Error::Argument, "Unknown qualification type and does not appear to be a raw qualification type ID: '#{type.to_s}'"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def opts
|
87
|
+
args = @raw.split(/\s+/)
|
88
|
+
if (args.count > 3) || (args.count < 2)
|
89
|
+
raise Error::Argument, "Unexpected number of qualification tokens: #{@raw}"
|
90
|
+
end
|
91
|
+
args.shift
|
92
|
+
comparator(args[0]) or raise Error::Argument, "Unknown comparator '#{args[0]}'"
|
93
|
+
value = 1
|
94
|
+
value = args[1] if args.count == 2
|
95
|
+
return {comparator(args[0]) => value}
|
96
|
+
end
|
97
|
+
|
98
|
+
def comparator(value)
|
99
|
+
Hash[
|
100
|
+
'>' => :gt,
|
101
|
+
'>=' => :gte,
|
102
|
+
'<' => :lt,
|
103
|
+
'<=' => :lte,
|
104
|
+
'==' => :eql,
|
105
|
+
'!=' => :not,
|
106
|
+
'true' => :eql,
|
107
|
+
'exists' => :exists
|
108
|
+
][value]
|
109
|
+
end
|
110
|
+
end #Qualification
|
111
|
+
end #Assign
|
112
|
+
end #Root
|
113
|
+
end #Config
|
114
|
+
end #Typingpool
|
data/lib/typingpool/config.rb
CHANGED
@@ -99,9 +99,9 @@ module Typingpool
|
|
99
99
|
# conf.sftp.url = 'http://luvrecording.s3.amazonaws.com/'
|
100
100
|
# puts conf.sftp.url #'http://luvrecording.s3.amazonaws.com'
|
101
101
|
#
|
102
|
-
class Config
|
103
|
-
|
102
|
+
class Config
|
104
103
|
require 'yaml'
|
104
|
+
|
105
105
|
@@default_file = "~/.typingpool"
|
106
106
|
|
107
107
|
def initialize(params)
|
@@ -158,29 +158,32 @@ module Typingpool
|
|
158
158
|
end
|
159
159
|
end
|
160
160
|
|
161
|
-
def define_reader(*syms)
|
161
|
+
def define_reader(*syms, &block)
|
162
|
+
#explicit block passing to avoid sefault in 1.9.3-p362
|
162
163
|
syms.each do |sym|
|
163
164
|
define_method(sym) do
|
164
165
|
value = @param[sym.to_s]
|
165
|
-
|
166
|
+
block.call(value)
|
166
167
|
end
|
167
168
|
end
|
168
169
|
end
|
169
170
|
|
170
|
-
def define_writer(*syms)
|
171
|
+
def define_writer(*syms, &block)
|
172
|
+
#explicit block passing to avoid sefault in 1.9.3-p362
|
171
173
|
syms.each do |sym|
|
172
174
|
define_method("#{sym.to_s}=".to_sym) do |value|
|
173
|
-
@param[sym.to_s] =
|
175
|
+
@param[sym.to_s] = block.call(value)
|
174
176
|
end
|
175
177
|
end
|
176
178
|
end
|
177
179
|
|
178
|
-
def define_accessor(*syms)
|
180
|
+
def define_accessor(*syms, &block)
|
181
|
+
#explicit block passing to avoid sefault in 1.9.3-p362
|
179
182
|
define_reader(*syms) do |value|
|
180
|
-
|
183
|
+
block.call(value) if value
|
181
184
|
end
|
182
185
|
define_writer(*syms) do |value|
|
183
|
-
|
186
|
+
block.call(value)
|
184
187
|
end
|
185
188
|
end
|
186
189
|
|
@@ -230,115 +233,6 @@ module Typingpool
|
|
230
233
|
match = meth.to_s.match(/([^=]+)=$/) or return
|
231
234
|
return match[1]
|
232
235
|
end
|
233
|
-
|
234
|
-
#The root level of the config file and all full config
|
235
|
-
#objects. Kept distinct from Config because other subclasses need
|
236
|
-
#to inherit from Config, and we don't want them inheriting the
|
237
|
-
#root level fields.
|
238
|
-
class Root < Config
|
239
|
-
local_path_reader :transcripts, :cache, :templates
|
240
|
-
|
241
|
-
class SFTP < Config
|
242
|
-
never_ends_in_slash_reader :path, :url
|
243
|
-
end
|
244
|
-
|
245
|
-
class Amazon < Config
|
246
|
-
never_ends_in_slash_reader :url
|
247
|
-
end
|
248
|
-
|
249
|
-
class Assign < Config
|
250
|
-
local_path_reader :templates
|
251
|
-
time_accessor :deadline, :approval, :lifetime
|
252
|
-
|
253
|
-
define_accessor(:reward) do |value|
|
254
|
-
value.to_s.match(/(\d+(\.\d+)?)|(\d*\.\d+)/) or raise Error::Argument::Format, "Format should be N.NN"
|
255
|
-
value
|
256
|
-
end
|
257
|
-
|
258
|
-
define_reader(:confirm) do |value|
|
259
|
-
next false if value.to_s.match(/(^n)|(^0)|(^false)/i)
|
260
|
-
next true if value.to_s.match(/(^y)|(^1)|(^true)/i)
|
261
|
-
next if value.to_s.empty?
|
262
|
-
raise Error::Argument::Format, "Format should be 'yes' or 'no'"
|
263
|
-
end
|
264
|
-
|
265
|
-
def qualify
|
266
|
-
self.qualify = (@param['qualify'] || []) unless @qualify
|
267
|
-
@qualify
|
268
|
-
end
|
269
|
-
|
270
|
-
def qualify=(specs)
|
271
|
-
@qualify = specs.map{|spec| Qualification.new(spec) }
|
272
|
-
end
|
273
|
-
|
274
|
-
def add_qualification(spec)
|
275
|
-
self.qualify.push(Qualification.new(spec))
|
276
|
-
end
|
277
|
-
|
278
|
-
def keywords
|
279
|
-
@param['keywords'] ||= []
|
280
|
-
end
|
281
|
-
|
282
|
-
def keywords=(array)
|
283
|
-
@param['keywords'] = array
|
284
|
-
end
|
285
|
-
|
286
|
-
class Qualification < Config
|
287
|
-
def initialize(spec)
|
288
|
-
@raw = spec
|
289
|
-
to_arg #make sure value parses
|
290
|
-
end
|
291
|
-
|
292
|
-
def to_s
|
293
|
-
@raw
|
294
|
-
end
|
295
|
-
|
296
|
-
def to_arg
|
297
|
-
[type, opts]
|
298
|
-
end
|
299
|
-
|
300
|
-
protected
|
301
|
-
|
302
|
-
def type
|
303
|
-
type = @raw.split(/\s+/)[0]
|
304
|
-
if RTurk::Qualification::TYPES[type.to_sym]
|
305
|
-
return type.to_sym
|
306
|
-
elsif (type.match(/\d/) || type.size >= 25)
|
307
|
-
return type
|
308
|
-
else
|
309
|
-
#Seems likely to be qualification typo: Not a known
|
310
|
-
#system qualification, all letters and less than 25
|
311
|
-
#chars
|
312
|
-
raise Error::Argument, "Unknown qualification type and does not appear to be a raw qualification type ID: '#{type.to_s}'"
|
313
|
-
end
|
314
|
-
end
|
315
|
-
|
316
|
-
def opts
|
317
|
-
args = @raw.split(/\s+/)
|
318
|
-
if (args.count > 3) || (args.count < 2)
|
319
|
-
raise Error::Argument, "Unexpected number of qualification tokens: #{@raw}"
|
320
|
-
end
|
321
|
-
args.shift
|
322
|
-
comparator(args[0]) or raise Error::Argument, "Unknown comparator '#{args[0]}'"
|
323
|
-
value = 1
|
324
|
-
value = args[1] if args.count == 2
|
325
|
-
return {comparator(args[0]) => value}
|
326
|
-
end
|
327
|
-
|
328
|
-
def comparator(value)
|
329
|
-
Hash[
|
330
|
-
'>' => :gt,
|
331
|
-
'>=' => :gte,
|
332
|
-
'<' => :lt,
|
333
|
-
'<=' => :lte,
|
334
|
-
'==' => :eql,
|
335
|
-
'!=' => :not,
|
336
|
-
'true' => :eql,
|
337
|
-
'exists' => :exists
|
338
|
-
][value]
|
339
|
-
end
|
340
|
-
end #Qualification
|
341
|
-
end #Assign
|
342
|
-
end #Root
|
343
236
|
end #Config
|
237
|
+
require 'typingpool/config/root'
|
344
238
|
end #Typingpool
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Typingpool
|
2
|
+
class Filer
|
3
|
+
|
4
|
+
#Convenience wrapper for audio files.You can convert to mp3s,
|
5
|
+
#split into multiple files, and dynamically read the bitrate.
|
6
|
+
class Audio < Filer
|
7
|
+
require 'open3'
|
8
|
+
|
9
|
+
#Does the file have a '.mp3' extension?
|
10
|
+
def mp3?
|
11
|
+
File.extname(@path).downcase.eql?('.mp3')
|
12
|
+
end
|
13
|
+
|
14
|
+
#Convert to mp3 via ffmpeg.
|
15
|
+
# ==== Params
|
16
|
+
# [dest] Filer object corresponding to the path the mp3 version
|
17
|
+
# should end up at.
|
18
|
+
# [bitrate] If passed, bitrate should be an integer
|
19
|
+
# corresponding to kb/s. If not, we use the bitrate
|
20
|
+
# from the current file or, if that can't be read,
|
21
|
+
# default to 192kbps. Does not check if the file is
|
22
|
+
# already an mp3. Returns a new Filer::Audio
|
23
|
+
# representing the new mp3 file.
|
24
|
+
# ==== Returns
|
25
|
+
# Filer::Audio containing the new mp3.
|
26
|
+
def to_mp3(dest=self.dir.file("#{File.basename(@path, '.*') }.mp3"), bitrate=nil)
|
27
|
+
bitrate ||= self.bitrate || 192
|
28
|
+
Utility.system_quietly('ffmpeg', '-i', @path, '-acodec', 'libmp3lame', '-ab', "#{bitrate}k", '-ac', '2', dest)
|
29
|
+
File.exists?(dest) or raise Error::Shell, "Could not found output from `ffmpeg` on #{path}"
|
30
|
+
self.class.new(dest.path)
|
31
|
+
end
|
32
|
+
|
33
|
+
#Reads the bitrate of the audio file via ffmpeg. Returns an
|
34
|
+
#integer corresponding to kb/s, or nil if the bitrate could not
|
35
|
+
#be determined.
|
36
|
+
def bitrate
|
37
|
+
out, err, status = Open3.capture3('ffmpeg', '-i', @path)
|
38
|
+
bitrate = err.match(/(\d+) kb\/s/)
|
39
|
+
return bitrate ? bitrate[1].to_i : nil
|
40
|
+
end
|
41
|
+
|
42
|
+
#Splits an mp3 into smaller files.
|
43
|
+
# ==== Params
|
44
|
+
# [interval_in_min_dot_seconds] Split the file into chunks this
|
45
|
+
# large. The interval should be of the format
|
46
|
+
# minute.seconds, for example 2 minutes 15 seconds
|
47
|
+
# would be written as "2.15". For further details on
|
48
|
+
# interval format, consult the documentation for
|
49
|
+
# mp3split, a command-line unix utility.
|
50
|
+
# [basename] Name the new chunks using this base. Default is the
|
51
|
+
# basename of the original file.
|
52
|
+
# [dest] Destination directory for the new chunks as a
|
53
|
+
# Filer::Dir. Default is the same directory as the
|
54
|
+
# original file.
|
55
|
+
# ==== Returns
|
56
|
+
# Filer::Files containing the new files.
|
57
|
+
def split(interval_in_min_dot_seconds, basename=File.basename(path, '.*'), dest=dir)
|
58
|
+
#We have to cd into the wrapfile directory and do everything
|
59
|
+
#there because old/packaged versions of mp3splt were
|
60
|
+
#retarded at handling absolute directory paths
|
61
|
+
::Dir.chdir(dir.path) do
|
62
|
+
Utility.system_quietly('mp3splt', '-t', interval_in_min_dot_seconds, '-o', "#{basename}.@m.@s", File.basename(path))
|
63
|
+
end
|
64
|
+
files = Filer::Files::Audio.new(dir.select{|file| File.basename(file.path).match(/^#{Regexp.escape(basename) }\.\d+\.\d+\.mp3$/) })
|
65
|
+
if files.to_a.empty?
|
66
|
+
raise Error::Shell, "Could not find output from `mp3splt` on #{path}"
|
67
|
+
end
|
68
|
+
if dest.path != dir.path
|
69
|
+
files.mv!(dest)
|
70
|
+
end
|
71
|
+
files.sort
|
72
|
+
end
|
73
|
+
|
74
|
+
#Extracts from the filename the offset time of the chunk
|
75
|
+
#relative to the original from which it was split. Format is
|
76
|
+
#minute.seconds. Suitable for use on files created by 'split'
|
77
|
+
#method.
|
78
|
+
def offset
|
79
|
+
match = File.basename(@path).match(/\d+\.\d\d\b/)
|
80
|
+
return match[0] if match
|
81
|
+
end
|
82
|
+
end #Audio
|
83
|
+
end #Filer
|
84
|
+
end #Typingpool
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Typingpool
|
2
|
+
class Filer
|
3
|
+
|
4
|
+
#Convenience wrapper for CSV files. Makes them Enumerable, so you
|
5
|
+
#can iterate through rows with each, map, select, etc. You can
|
6
|
+
#also modify in place with each!. See Filer base class for other
|
7
|
+
#methods.
|
8
|
+
class CSV < Filer
|
9
|
+
include Enumerable
|
10
|
+
require 'csv'
|
11
|
+
|
12
|
+
#Reads into an array of hashes, with hash keys determined by the
|
13
|
+
#first row of the CSV file. Parsing rules are the default for
|
14
|
+
#CSV.parse.
|
15
|
+
def read
|
16
|
+
raw = super or return []
|
17
|
+
rows = ::CSV.parse(raw.to_s)
|
18
|
+
headers = rows.shift or raise Error::File, "No CSV at #{@path}"
|
19
|
+
rows.map{|row| Utility.array_to_hash(row, headers) }
|
20
|
+
end
|
21
|
+
|
22
|
+
#Takes array of hashes followed by optional list of keys (by
|
23
|
+
#default keys are determined by looking at all the
|
24
|
+
#hashes). Lines are written per the defaults of
|
25
|
+
#CSV.generate_line.
|
26
|
+
def write(hashes, headers=hashes.map{|h| h.keys}.flatten.uniq)
|
27
|
+
super(
|
28
|
+
::CSV.generate_line(headers, :encoding => @encoding) +
|
29
|
+
hashes.map do |hash|
|
30
|
+
::CSV.generate_line(headers.map{|header| hash[header] }, :encoding => @encoding)
|
31
|
+
end.join
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
#Takes an array of arrays, corresponding to the rows, and a list
|
36
|
+
#of headers/keys to write at the top.
|
37
|
+
def write_arrays(arrays, headers)
|
38
|
+
write(arrays.map{|array| Utility.array_to_hash(array, headers) }, headers)
|
39
|
+
end
|
40
|
+
|
41
|
+
#Enumerate through the rows, with each row represented by a
|
42
|
+
#hash.
|
43
|
+
def each
|
44
|
+
read.each do |row|
|
45
|
+
yield row
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
#Same as each, but any changes to the rows will be written back
|
50
|
+
#out to the underlying CSV file.
|
51
|
+
def each!
|
52
|
+
#each_with_index doesn't return the array, so we have to use each
|
53
|
+
write(each{|hash| yield(hash) })
|
54
|
+
end
|
55
|
+
end #CSV
|
56
|
+
end #Filer
|
57
|
+
end #Typingpool
|