typingpool 0.7.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/LICENSE +20 -0
  2. data/README.markdown +452 -0
  3. data/lib/typingpool/amazon/hit/assignment/empty.rb +19 -0
  4. data/lib/typingpool/amazon/hit/assignment.rb +43 -0
  5. data/lib/typingpool/amazon/hit/full/fromsearchhits.rb +44 -0
  6. data/lib/typingpool/amazon/hit/full.rb +105 -0
  7. data/lib/typingpool/amazon/hit.rb +458 -0
  8. data/lib/typingpool/amazon/question.rb +45 -0
  9. data/lib/typingpool/amazon.rb +3 -677
  10. data/lib/typingpool/app/cli/formatter.rb +16 -0
  11. data/lib/typingpool/app/cli.rb +64 -0
  12. data/lib/typingpool/app/friendlyexceptions.rb +34 -0
  13. data/lib/typingpool/app.rb +2 -97
  14. data/lib/typingpool/config/root.rb +114 -0
  15. data/lib/typingpool/config.rb +13 -119
  16. data/lib/typingpool/filer/audio.rb +84 -0
  17. data/lib/typingpool/filer/csv.rb +57 -0
  18. data/lib/typingpool/filer/dir.rb +76 -0
  19. data/lib/typingpool/filer/files/audio.rb +63 -0
  20. data/lib/typingpool/filer/files.rb +55 -0
  21. data/lib/typingpool/filer.rb +4 -313
  22. data/lib/typingpool/project/local.rb +117 -0
  23. data/lib/typingpool/project/remote/s3.rb +135 -0
  24. data/lib/typingpool/project/remote/sftp.rb +100 -0
  25. data/lib/typingpool/project/remote.rb +65 -0
  26. data/lib/typingpool/project.rb +2 -396
  27. data/lib/typingpool/template/assignment.rb +17 -0
  28. data/lib/typingpool/template/env.rb +77 -0
  29. data/lib/typingpool/template.rb +2 -87
  30. data/lib/typingpool/test/script.rb +310 -0
  31. data/lib/typingpool/test.rb +1 -306
  32. data/lib/typingpool/transcript/chunk.rb +129 -0
  33. data/lib/typingpool/transcript.rb +1 -125
  34. data/lib/typingpool/utility/castable.rb +65 -0
  35. data/lib/typingpool/utility.rb +1 -61
  36. data/test/test_integration_script_6_tp_finish.rb +1 -0
  37. metadata +135 -81
@@ -0,0 +1,76 @@
1
+ module Typingpool
2
+ class Filer
3
+
4
+ #Convenience wrapper for basic directory operations and for
5
+ #casting files to specific filer types (CSV, Audio).
6
+ class Dir < Files
7
+
8
+ #Full expanded path to the dir
9
+ attr_reader :path
10
+
11
+ #Constructor. Takes full expanded path to the dir. Does NOT
12
+ #create dir in the filesystem.
13
+ def initialize(path)
14
+ @path = path
15
+ end
16
+
17
+ class << self
18
+
19
+ #Constructor. Takes full expanded path to the dir and creates
20
+ #the dir in the filesystem. Returns new Filer::Dir.
21
+ def create(path)
22
+ FileUtils.mkdir(path)
23
+ new(path)
24
+ end
25
+
26
+ #Constructor. Takes directory name and full expanded path of
27
+ #the parent directory. If the so-named directory exists within
28
+ #the parent directory, returns it. If not, returns nil.
29
+ def named(name, in_dir)
30
+ path = File.join(in_dir, name)
31
+ if File.exists?(path) && File.directory?(path)
32
+ new(path)
33
+ end
34
+ end
35
+ end #class << self
36
+
37
+ #Filer::Dir isntances stringify to their path.
38
+ def to_s
39
+ @path
40
+ end
41
+ alias :to_str :to_s
42
+
43
+ #Takes an aribtrary number of path elements relative to the
44
+ #Filer::Dir instance. So a file in the subdir path/to/file.txt
45
+ #would be referenced via file('path', 'to', 'file.txt'). Returns
46
+ #a new Filer instance wrapping the referenced file. Does not
47
+ #guarantee that the referenced file exists.
48
+ def file(*relative_path)
49
+ Filer.new(file_path(*relative_path))
50
+ end
51
+
52
+ #Returns the files in the Filer::Dir directory as Filer
53
+ #instances. Excludes files whose names start with a dot.
54
+ def files
55
+ ::Dir.entries(@path).select{|entry| File.file? file_path(entry) }.reject{|entry| entry.match(/^\./) }.map{|entry| self.file(entry) }
56
+ end
57
+
58
+ #Takes relative path elements as params just like the file
59
+ #method. Returns a new Filer::Dir instance wrapping the
60
+ #referenced subdir.
61
+ def subdir(*relative_path)
62
+ Dir.new(file_path(*relative_path))
63
+ end
64
+
65
+ #OS X specific. Opens the dir in the Finder via the 'open' command.
66
+ def finder_open
67
+ system('open', @path)
68
+ end
69
+
70
+ def file_path(*relative_path)
71
+ File.join(@path, *relative_path)
72
+ end
73
+
74
+ end #Dir
75
+ end #Filer
76
+ end #Typingpool
@@ -0,0 +1,63 @@
1
+ module Typingpool
2
+ class Filer
3
+ class Files
4
+
5
+ #Handler for collection of Filer::Audio instances. Does
6
+ #everything Filer::Files does, plus can batch convert to mp3 an
7
+ #can merge the Filer::Audio instances into a single audio file,
8
+ #provided they are in mp3 format.
9
+ class Audio < Files
10
+
11
+ #Constructor. Takes an array of Filer or Filer subclass instances.
12
+ def initialize(files)
13
+ @files = files.map{|file| self.file(file.path) }
14
+ end
15
+
16
+ def file(path)
17
+ Filer::Audio.new(path)
18
+ end
19
+
20
+ #Batch convert Filer::Audio instances to mp3 format.
21
+ # ==== Params
22
+ # [dest_dir] Filer::Dir instance corresponding to directory
23
+ # into which mp3 file versions will be created.
24
+ # [bitrate] See documentation for Filer::Audio#bitrate.
25
+ # ==== Returns
26
+ # Filer::Files::Audio instance corresponding to new mp3
27
+ # versions of the original files or, in the case where the
28
+ # original file was already in mp3 format, corresponding to
29
+ # the original files themselves.
30
+ def to_mp3(dest_dir, bitrate=nil)
31
+ mp3s = self.map do |file|
32
+ if file.mp3?
33
+ file
34
+ else
35
+ yield(file) if block_given?
36
+ file.to_mp3(dest_dir.file("#{File.basename(file.path, '.*') }.mp3"), bitrate)
37
+ end
38
+ end
39
+ self.class.new(mp3s)
40
+ end
41
+
42
+ #Merge Filer::Audio instances into a single new file, provided
43
+ #they are all in mp3 format.
44
+ # ==== Params
45
+ #[into_file] Filer or Filer subclass instance corresponding to
46
+ #the location of the new, merged file that should be created.
47
+ # ==== Returns
48
+ # Filer::Audio instance corresponding to the new, merged file.
49
+ def merge(into_file)
50
+ raise Error::Argument, "No files to merge" if self.to_a.empty?
51
+ if self.count > 1
52
+ Utility.system_quietly('mp3wrap', into_file, *self.to_a)
53
+ written = File.join(into_file.dir, "#{File.basename(into_file.path, '.*') }_MP3WRAP.mp3")
54
+ FileUtils.mv(written, into_file)
55
+ else
56
+ FileUtils.cp(self.first, into_file)
57
+ end
58
+ self.file(into_file.path)
59
+ end
60
+ end #Audio
61
+ end #Files
62
+ end #Filer
63
+ end #Typingpool
@@ -0,0 +1,55 @@
1
+ module Typingpool
2
+ class Filer
3
+
4
+ #Handler for collection of Filer instances. Makes them enumerable,
5
+ #Allows easy re-casting to Filer::Files subclasses,
6
+ #and provides various other convenience methods.
7
+ class Files
8
+ include Enumerable
9
+ include Utility::Castable
10
+ require 'fileutils'
11
+ require 'typingpool/filer/files/audio'
12
+
13
+ #Array of Filer instances included in the collection
14
+ attr_reader :files
15
+
16
+ #Constructor. Takes array of Filer instances.
17
+ def initialize(files)
18
+ @files = files
19
+ end
20
+
21
+ #Enumerate through Filer instances.
22
+ def each
23
+ files.each do |file|
24
+ yield file
25
+ end
26
+ end
27
+
28
+ #Cast this collection into a new Filer::Files subtype,
29
+ #e.g. Filer::Files::Audio.
30
+ # ==== Params
31
+ # [sym] Symbol corresponding to Filer::Files subclass to cast
32
+ # into. For example, passing :audio will cast into a
33
+ # Filer::Files::Audio.
34
+ # ==== Returns
35
+ # Instance of new Filer::Files subclass
36
+ def as(sym)
37
+ #super calls into Utility::Castable mixin
38
+ super(sym, files)
39
+ end
40
+
41
+ #Returns array of IO streams created by calling to_stream on
42
+ #each Filer instance in the collection.
43
+ def to_streams
44
+ self.map{|file| file.to_stream }
45
+ end
46
+
47
+ #Calls mv! on each Filer instance in the collection. See
48
+ #documentation for Filer#mv! for definition of "to" param and
49
+ #for return value.
50
+ def mv!(to)
51
+ files.map{|file| file.mv! to }
52
+ end
53
+ end #Files
54
+ end #Filer
55
+ end #Typingpool
@@ -79,318 +79,9 @@ module Typingpool
79
79
  #super calls into Utility::Castable mixin
80
80
  super(sym, @path)
81
81
  end
82
-
83
-
84
- #Convenience wrapper for CSV files. Makes them Enumerable, so you
85
- #can iterate through rows with each, map, select, etc. You can
86
- #also modify in place with each!. See Filer base class for other
87
- #methods.
88
- class CSV < Filer
89
- include Enumerable
90
- require 'csv'
91
-
92
- #Reads into an array of hashes, with hash keys determined by the
93
- #first row of the CSV file. Parsing rules are the default for
94
- #CSV.parse.
95
- def read
96
- raw = super or return []
97
- rows = ::CSV.parse(raw.to_s)
98
- headers = rows.shift or raise Error::File, "No CSV at #{@path}"
99
- rows.map{|row| Utility.array_to_hash(row, headers) }
100
- end
101
-
102
- #Takes array of hashes followed by optional list of keys (by
103
- #default keys are determined by looking at all the
104
- #hashes). Lines are written per the defaults of
105
- #CSV.generate_line.
106
- def write(hashes, headers=hashes.map{|h| h.keys}.flatten.uniq)
107
- super(
108
- ::CSV.generate_line(headers, :encoding => @encoding) +
109
- hashes.map do |hash|
110
- ::CSV.generate_line(headers.map{|header| hash[header] }, :encoding => @encoding)
111
- end.join
112
- )
113
- end
114
-
115
- #Takes an array of arrays, corresponding to the rows, and a list
116
- #of headers/keys to write at the top.
117
- def write_arrays(arrays, headers)
118
- write(arrays.map{|array| Utility.array_to_hash(array, headers) }, headers)
119
- end
120
-
121
- #Enumerate through the rows, with each row represented by a
122
- #hash.
123
- def each
124
- read.each do |row|
125
- yield row
126
- end
127
- end
128
-
129
- #Same as each, but any changes to the rows will be written back
130
- #out to the underlying CSV file.
131
- def each!
132
- #each_with_index doesn't return the array, so we have to use each
133
- write(each{|hash| yield(hash) })
134
- end
135
- end #CSV
136
-
137
- #Convenience wrapper for audio files.You can convert to mp3s,
138
- #split into multiple files, and dynamically read the bitrate.
139
- class Audio < Filer
140
- require 'open3'
141
-
142
- #Does the file have a '.mp3' extension?
143
- def mp3?
144
- File.extname(@path).downcase.eql?('.mp3')
145
- end
146
-
147
- #Convert to mp3 via ffmpeg.
148
- # ==== Params
149
- # [dest] Filer object corresponding to the path the mp3 version
150
- # should end up at.
151
- # [bitrate] If passed, bitrate should be an integer
152
- # corresponding to kb/s. If not, we use the bitrate
153
- # from the current file or, if that can't be read,
154
- # default to 192kbps. Does not check if the file is
155
- # already an mp3. Returns a new Filer::Audio
156
- # representing the new mp3 file.
157
- # ==== Returns
158
- # Filer::Audio containing the new mp3.
159
- def to_mp3(dest=self.dir.file("#{File.basename(@path, '.*') }.mp3"), bitrate=nil)
160
- bitrate ||= self.bitrate || 192
161
- Utility.system_quietly('ffmpeg', '-i', @path, '-acodec', 'libmp3lame', '-ab', "#{bitrate}k", '-ac', '2', dest)
162
- File.exists?(dest) or raise Error::Shell, "Could not found output from `ffmpeg` on #{path}"
163
- self.class.new(dest.path)
164
- end
165
-
166
- #Reads the bitrate of the audio file via ffmpeg. Returns an
167
- #integer corresponding to kb/s, or nil if the bitrate could not
168
- #be determined.
169
- def bitrate
170
- out, err, status = Open3.capture3('ffmpeg', '-i', @path)
171
- bitrate = err.match(/(\d+) kb\/s/)
172
- return bitrate ? bitrate[1].to_i : nil
173
- end
174
-
175
- #Splits an mp3 into smaller files.
176
- # ==== Params
177
- # [interval_in_min_dot_seconds] Split the file into chunks this
178
- # large. The interval should be of the format
179
- # minute.seconds, for example 2 minutes 15 seconds
180
- # would be written as "2.15". For further details on
181
- # interval format, consult the documentation for
182
- # mp3split, a command-line unix utility.
183
- # [basename] Name the new chunks using this base. Default is the
184
- # basename of the original file.
185
- # [dest] Destination directory for the new chunks as a
186
- # Filer::Dir. Default is the same directory as the
187
- # original file.
188
- # ==== Returns
189
- # Filer::Files containing the new files.
190
- def split(interval_in_min_dot_seconds, basename=File.basename(path, '.*'), dest=dir)
191
- #We have to cd into the wrapfile directory and do everything
192
- #there because old/packaged versions of mp3splt were
193
- #retarded at handling absolute directory paths
194
- ::Dir.chdir(dir.path) do
195
- Utility.system_quietly('mp3splt', '-t', interval_in_min_dot_seconds, '-o', "#{basename}.@m.@s", File.basename(path))
196
- end
197
- files = Filer::Files::Audio.new(dir.select{|file| File.basename(file.path).match(/^#{Regexp.escape(basename) }\.\d+\.\d+\.mp3$/) })
198
- if files.to_a.empty?
199
- raise Error::Shell, "Could not find output from `mp3splt` on #{path}"
200
- end
201
- if dest.path != dir.path
202
- files.mv!(dest)
203
- end
204
- files.sort
205
- end
206
-
207
- #Extracts from the filename the offset time of the chunk
208
- #relative to the original from which it was split. Format is
209
- #minute.seconds. Suitable for use on files created by 'split'
210
- #method.
211
- def offset
212
- match = File.basename(@path).match(/\d+\.\d\d\b/)
213
- return match[0] if match
214
- end
215
- end #Audio
216
-
217
- #Handler for collection of Filer instances. Makes them enumerable,
218
- #Allows easy re-casting to Filer::Files subclasses,
219
- #and provides various other convenience methods.
220
- class Files
221
- include Enumerable
222
- include Utility::Castable
223
- require 'fileutils'
224
-
225
- #Array of Filer instances included in the collection
226
- attr_reader :files
227
-
228
- #Constructor. Takes array of Filer instances.
229
- def initialize(files)
230
- @files = files
231
- end
232
-
233
- #Enumerate through Filer instances.
234
- def each
235
- files.each do |file|
236
- yield file
237
- end
238
- end
239
-
240
- #Cast this collection into a new Filer::Files subtype,
241
- #e.g. Filer::Files::Audio.
242
- # ==== Params
243
- # [sym] Symbol corresponding to Filer::Files subclass to cast
244
- # into. For example, passing :audio will cast into a
245
- # Filer::Files::Audio.
246
- # ==== Returns
247
- # Instance of new Filer::Files subclass
248
- def as(sym)
249
- #super calls into Utility::Castable mixin
250
- super(sym, files)
251
- end
252
-
253
- #Returns array of IO streams created by calling to_stream on
254
- #each Filer instance in the collection.
255
- def to_streams
256
- self.map{|file| file.to_stream }
257
- end
258
-
259
- #Calls mv! on each Filer instance in the collection. See
260
- #documentation for Filer#mv! for definition of "to" param and
261
- #for return value.
262
- def mv!(to)
263
- files.map{|file| file.mv! to }
264
- end
265
-
266
- #Handler for collection of Filer::Audio instances. Does
267
- #everything Filer::Files does, plus can batch convert to mp3 an
268
- #can merge the Filer::Audio instances into a single audio file,
269
- #provided they are in mp3 format.
270
- class Audio < Files
271
-
272
- #Constructor. Takes an array of Filer or Filer subclass instances.
273
- def initialize(files)
274
- @files = files.map{|file| self.file(file.path) }
275
- end
276
-
277
- def file(path)
278
- Filer::Audio.new(path)
279
- end
280
-
281
- #Batch convert Filer::Audio instances to mp3 format.
282
- # ==== Params
283
- # [dest_dir] Filer::Dir instance corresponding to directory
284
- # into which mp3 file versions will be created.
285
- # [bitrate] See documentation for Filer::Audio#bitrate.
286
- # ==== Returns
287
- # Filer::Files::Audio instance corresponding to new mp3
288
- # versions of the original files or, in the case where the
289
- # original file was already in mp3 format, corresponding to
290
- # the original files themselves.
291
- def to_mp3(dest_dir, bitrate=nil)
292
- mp3s = self.map do |file|
293
- if file.mp3?
294
- file
295
- else
296
- yield(file) if block_given?
297
- file.to_mp3(dest_dir.file("#{File.basename(file.path, '.*') }.mp3"), bitrate)
298
- end
299
- end
300
- self.class.new(mp3s)
301
- end
302
-
303
- #Merge Filer::Audio instances into a single new file, provided
304
- #they are all in mp3 format.
305
- # ==== Params
306
- #[into_file] Filer or Filer subclass instance corresponding to
307
- #the location of the new, merged file that should be created.
308
- # ==== Returns
309
- # Filer::Audio instance corresponding to the new, merged file.
310
- def merge(into_file)
311
- raise Error::Argument, "No files to merge" if self.to_a.empty?
312
- if self.count > 1
313
- Utility.system_quietly('mp3wrap', into_file, *self.to_a)
314
- written = File.join(into_file.dir, "#{File.basename(into_file.path, '.*') }_MP3WRAP.mp3")
315
- FileUtils.mv(written, into_file)
316
- else
317
- FileUtils.cp(self.first, into_file)
318
- end
319
- self.file(into_file.path)
320
- end
321
- end #Audio
322
- end #Files
323
-
324
- #Convenience wrapper for basic directory operations and for
325
- #casting files to specific filer types (CSV, Audio).
326
- class Dir < Files
327
-
328
- #Full expanded path to the dir
329
- attr_reader :path
330
-
331
- #Constructor. Takes full expanded path to the dir. Does NOT
332
- #create dir in the filesystem.
333
- def initialize(path)
334
- @path = path
335
- end
336
-
337
- class << self
338
-
339
- #Constructor. Takes full expanded path to the dir and creates
340
- #the dir in the filesystem. Returns new Filer::Dir.
341
- def create(path)
342
- FileUtils.mkdir(path)
343
- new(path)
344
- end
345
-
346
- #Constructor. Takes directory name and full expanded path of
347
- #the parent directory. If the so-named directory exists within
348
- #the parent directory, returns it. If not, returns nil.
349
- def named(name, in_dir)
350
- path = File.join(in_dir, name)
351
- if File.exists?(path) && File.directory?(path)
352
- new(path)
353
- end
354
- end
355
- end #class << self
356
-
357
- #Filer::Dir isntances stringify to their path.
358
- def to_s
359
- @path
360
- end
361
- alias :to_str :to_s
362
-
363
- #Takes an aribtrary number of path elements relative to the
364
- #Filer::Dir instance. So a file in the subdir path/to/file.txt
365
- #would be referenced via file('path', 'to', 'file.txt'). Returns
366
- #a new Filer instance wrapping the referenced file. Does not
367
- #guarantee that the referenced file exists.
368
- def file(*relative_path)
369
- Filer.new(file_path(*relative_path))
370
- end
371
-
372
- #Returns the files in the Filer::Dir directory as Filer
373
- #instances. Excludes files whose names start with a dot.
374
- def files
375
- ::Dir.entries(@path).select{|entry| File.file? file_path(entry) }.reject{|entry| entry.match(/^\./) }.map{|entry| self.file(entry) }
376
- end
377
-
378
- #Takes relative path elements as params just like the file
379
- #method. Returns a new Filer::Dir instance wrapping the
380
- #referenced subdir.
381
- def subdir(*relative_path)
382
- Dir.new(file_path(*relative_path))
383
- end
384
-
385
- #OS X specific. Opens the dir in the Finder via the 'open' command.
386
- def finder_open
387
- system('open', @path)
388
- end
389
-
390
- def file_path(*relative_path)
391
- File.join(@path, *relative_path)
392
- end
393
-
394
- end #Dir
395
82
  end #Filer
83
+ require 'typingpool/filer/csv'
84
+ require 'typingpool/filer/audio'
85
+ require 'typingpool/filer/files'
86
+ require 'typingpool/filer/dir'
396
87
  end #Typingpool
@@ -0,0 +1,117 @@
1
+ module Typingpool
2
+ class Project
3
+ #Representation of the Project instance in the local
4
+ #filesystem. Subclass of Filer::Dir; see Filer::Dir docs for
5
+ #additional details.
6
+ #
7
+ #This is basically a local dir with various subdirs and files
8
+ #containing the canonical representation of the project, including
9
+ #data on remote resources, the project ID and subtitle, the audio files
10
+ #themselves, and, when complete, an HTML transcript of that audio,
11
+ #along with supporting CSS and Javascript files.
12
+ class Local < Filer::Dir
13
+ require 'fileutils'
14
+ require 'securerandom'
15
+
16
+ #Returns the dir path.
17
+ attr_reader :path
18
+
19
+ class << self
20
+ #Constructor. Creates a directory in the filesystem for the
21
+ #project.
22
+ #
23
+ # ==== Params
24
+ # [name] Name of the associated project.
25
+ # [base_dir] Path to the local directory into which the project
26
+ # dir should be placed.
27
+ # [template_dir] Path to the dir which will be used as a base
28
+ # template for new projects.
29
+ # ==== Returns
30
+ # Project::Local instance.
31
+ def create(name, base_dir, template_dir)
32
+ local = super(File.join(base_dir, name))
33
+ FileUtils.cp_r(File.join(template_dir, '.'), local)
34
+ local.create_id
35
+ local
36
+ end
37
+
38
+ #Takes the name of a project and a path. If there's a
39
+ #directory with a matching name in the given path whose file
40
+ #layout indicates it is a Project::Local instance (see 'ours?'
41
+ #docs), returns a corresponding Project::Local instance.
42
+ def named(string, path)
43
+ match = super
44
+ if match && ours?(match)
45
+ return match
46
+ end
47
+ return
48
+ end
49
+
50
+ #Takes a Filer::Dir instance. Returns true or false depending on whether
51
+ #the file layout inside the dir indicates it is a
52
+ #Project::Local instance.
53
+ def ours?(dir)
54
+ File.exists?(dir.subdir('audio')) && File.exists?(dir.subdir('audio', 'originals'))
55
+ end
56
+
57
+ #Takes the name of a project and returns true if it is a valid
58
+ #name for a directory in the local filesystem, false if not.
59
+ def valid_name?(name)
60
+ Utility.in_temp_dir do |dir|
61
+ begin
62
+ FileUtils.mkdir(File.join(dir, name))
63
+ rescue Errno::ENOENT
64
+ return false
65
+ end #begin
66
+ return File.exists?(File.join(dir, name))
67
+ end #Utility.in_temp_dir do...
68
+ end
69
+
70
+ #Takes one or more symbols. Adds corresponding getter/setter
71
+ #and delete method(s) to Project::Local, which read (getter)
72
+ #and write (setter) and delete corresponding text files in the
73
+ #data directory.
74
+ #
75
+ #So, for example, 'data_file_accessor :name' would allow you
76
+ #to later create the file 'data/foo.txt' in the project dir by
77
+ #calling 'project.local.name = "Foo"', read that same file via
78
+ #'project.local.name', and delete the file via
79
+ #'project.local.delete_name'
80
+ def data_file_accessor(*syms)
81
+ syms.each do |sym|
82
+ define_method(sym) do
83
+ file('data',"#{sym.to_s}.txt").read
84
+ end
85
+ define_method("#{sym.to_s}=".to_sym) do |value|
86
+ file('data',"#{sym.to_s}.txt").write(value)
87
+ end
88
+ define_method("delete_#{sym.to_s}".to_sym) do
89
+ if File.exists? file('data',"#{sym.to_s}.txt")
90
+ File.delete(file('data',"#{sym.to_s}.txt"))
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end #class << self
96
+
97
+ #Calling 'subtitle' will read 'data/subtitle.txt'; calling
98
+ #'subtitle=' will write 'data/subtitle.txt'; calling
99
+ #'delete_subtitle' will delete 'data/subtitle.txt'.
100
+ data_file_accessor :subtitle
101
+
102
+ #Returns the ID of the project, as stored in 'data/id.txt'.
103
+ def id
104
+ file('data','id.txt').read
105
+ end
106
+
107
+ #Creates a file storing the canonical ID of the project in
108
+ #'data/id.txt'. Raises an exception if the file already exists.
109
+ def create_id
110
+ if id
111
+ raise Error, "id already exists"
112
+ end
113
+ file('data','id.txt').write(SecureRandom.hex(16))
114
+ end
115
+ end #Local
116
+ end #Project
117
+ end #Typingpool