typingpool 0.7.0 → 0.7.1
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/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
|