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