budik 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/budik/rng.rb ADDED
@@ -0,0 +1,154 @@
1
+ # = rng.rb
2
+ # This file contains methods for random number generation.
3
+ #
4
+ # == Contact
5
+ #
6
+ # Author:: Petr Schmied (mailto:jblack@paworld.eu)
7
+ # Website:: http://www.paworld.eu
8
+ # Date:: September 20, 2015
9
+
10
+ module Budik
11
+ # 'Rng' class provides various methods for random number generation.
12
+ class Rng
13
+ # Loads RNG options including method.
14
+ def initialize
15
+ @options = Config.instance.options['rng']
16
+ @method = @options['method']
17
+ end
18
+
19
+ # Gets RNG options and method.
20
+ attr_accessor :options, :method
21
+
22
+ # Generates random number.
23
+ #
24
+ # - *Args*:
25
+ # - +items+ -> Total number of items (Fixnum).
26
+ # - *Returns*:
27
+ # - Fixnum (0...items)
28
+ #
29
+ def generate(items)
30
+ case @method
31
+ when 'hwrng'
32
+ hwrng(@options['hwrng'], items)
33
+ when 'random.org'
34
+ random_org(@options['random.org'], items)
35
+ when 'rand-hwrng-seed'
36
+ swrng(items, hwrng(@options['hwrng'], 2**64))
37
+ else
38
+ swrng(items)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ # Reads random number from hwrng (/dev/hwrng, /dev/(u)random).
45
+ # Removes modulo bias.
46
+ # http://funloop.org/post/2015-02-27-removing-modulo-bias-redux.html
47
+ # Falls back to swrng if an exception is encountered.
48
+ #
49
+ # - *Args*:
50
+ # - +options+ -> Hwrng options (Hash).
51
+ # - +items+ -> Total number of items (Fixnum).
52
+ # - *Returns*:
53
+ # - Fixnum (0...items)
54
+ #
55
+ def hwrng(options, items)
56
+ source = File.new(options['source'], 'r')
57
+ max = 2**64
58
+ bound = items - 1
59
+ threshold = (max - bound) % bound
60
+ number = source.read(8).unpack('Q') while number.first < threshold
61
+ number.first % bound
62
+ rescue
63
+ swrng(items)
64
+ end
65
+
66
+ # Queries Random.org API to obtain random number.
67
+ # https://api.random.org/json-rpc/1/
68
+ # Falls back to swrng if incorrect response is received.
69
+ #
70
+ # - *Args*:
71
+ # - +options+ -> Random.org options (Hash).
72
+ # - +items+ -> Total number of items (Fixnum).
73
+ # - *Returns*:
74
+ # - Fixnum (0...items)
75
+ #
76
+ def random_org(options, items)
77
+ response = random_org_request(options, items)
78
+ return response if response.is_a? Fixnum
79
+
80
+ if response.code.to_i == 200
81
+ JSON.parse(response.body)['result']['random']['data'].first
82
+ else
83
+ swrng(items)
84
+ end
85
+ end
86
+
87
+ # Generates Random.org API request data.
88
+ #
89
+ # - *Args*:
90
+ # - +apikey+ -> Random.org API key (String).
91
+ # - +items+ -> Total number of items (Fixnum).
92
+ # - *Returns*:
93
+ # - Random.org API request data (Hash).
94
+ #
95
+ def random_org_request_data(apikey, items)
96
+ { jsonrpc: '2.0',
97
+ method: 'generateIntegers',
98
+ params: {
99
+ apiKey: apikey,
100
+ n: 1,
101
+ min: 0,
102
+ max: items - 1
103
+ },
104
+ id: 29 }
105
+ end
106
+
107
+ # Builds a request and sends it to Random.org API.
108
+ #
109
+ # - *Args*:
110
+ # - +options+ -> Random.org options (Hash).
111
+ # - +items+ -> Total number of items (Fixnum).
112
+ # - *Returns*:
113
+ # - Random.org API response object or Fixnum (0...items).
114
+ #
115
+ def random_org_request(options, items)
116
+ uri = URI.parse('http://api.random.org/json-rpc/1/invoke')
117
+ header = { 'Content-Type' => 'application/json-rpc' }
118
+ data = random_org_request_data(options['apikey'], items)
119
+
120
+ http = Net::HTTP.new(uri.host, uri.port)
121
+ request = Net::HTTP::Post.new(uri.request_uri, header)
122
+ request.body = data.to_json
123
+
124
+ random_org_request_send(http, request)
125
+ end
126
+
127
+ # Sends a request to Random.org API.
128
+ #
129
+ # - *Args*:
130
+ # - +http+ -> Net::HTTP object.
131
+ # - +request+ -> Net::HTTP::Post object.
132
+ # - *Returns*:
133
+ # - HTTPResponse object or Fixnum (0...items).
134
+ #
135
+ def random_org_request_send(http, request)
136
+ http.request(request)
137
+ rescue
138
+ swrng(items)
139
+ end
140
+
141
+ # Generates random number using (s)rand.
142
+ #
143
+ # - *Args*:
144
+ # - +items+ -> Total number of items (Fixnum).
145
+ # - +seed+ -> Custom seed for rand.
146
+ # - *Returns*:
147
+ # - Fixnum (0...items).
148
+ #
149
+ def swrng(items, seed = nil)
150
+ seed.nil? ? srand : srand(seed) # TODO: Test this
151
+ rand(0...items)
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,182 @@
1
+ # = sources.rb
2
+ # This file contains methods for parsing sources and category modifiers.
3
+ #
4
+ # == Contact
5
+ #
6
+ # Author:: Petr Schmied (mailto:jblack@paworld.eu)
7
+ # Website:: http://www.paworld.eu
8
+ # Date:: September 20, 2015
9
+
10
+ module Budik
11
+ # 'Sources' class loads and parses media sources file.
12
+ class Sources
13
+ include Singleton
14
+
15
+ # Initializes sources instance variable and strings in currently set
16
+ # language.
17
+ def initialize
18
+ @sources = []
19
+ @strings = Config.instance.lang.sources
20
+ end
21
+
22
+ # Gets sources
23
+ attr_accessor :sources
24
+
25
+ # Applies category modifiers.
26
+ #
27
+ # - *Args*:
28
+ # - +mods+ -> Category modifiers (Hash).
29
+ #
30
+ def apply_mods(mods)
31
+ @sources.keep_if do |source|
32
+ mods[:adds].any? { |mod| apply_mods_check(source[:category], mod) }
33
+ end
34
+
35
+ @sources.delete_if do |source|
36
+ mods[:rms].any? { |mod| apply_mods_check(source[:category], mod) }
37
+ end
38
+ end
39
+
40
+ # Checks if mod applies to category.
41
+ #
42
+ # - *Args*:
43
+ # - +category+ -> Category to be checked (Array).
44
+ # - +mod+ -> Modifier to be checked (Array).
45
+ # - *Returns*:
46
+ # - true or false
47
+ #
48
+ def apply_mods_check(category, mod)
49
+ mod_len = mod.length - 1
50
+ cat_len = category.length - 1
51
+ len = mod_len <= cat_len ? mod_len : cat_len
52
+
53
+ map = category[0..len].zip(mod[0..len]).map { |c, m| c == m }
54
+ !map.include? false
55
+ end
56
+
57
+ # Returns total count of sources
58
+ def count
59
+ @sources.length
60
+ end
61
+
62
+ # Returns source by number.
63
+ #
64
+ # - *Args*:
65
+ # - +number+ -> Fixnum.
66
+ #
67
+ def get(number)
68
+ @sources[number]
69
+ end
70
+
71
+ # Normalizes item.
72
+ #
73
+ # - *Args*:
74
+ # - +item+ -> Item to normalize (Array, Hash or String).
75
+ # - +category+ -> Item's category (Array).
76
+ # - *Returns*:
77
+ # - Normalized source (Hash).
78
+ # - *Raises*:
79
+ # - +RuntimeError+ -> If item is not Array, Hash or String.
80
+ #
81
+ def normalize(source, category)
82
+ case source
83
+ when Array
84
+ normalize_multiple_items(source, category)
85
+ when Hash
86
+ normalize_named_source(source, category)
87
+ when String
88
+ normalize_unnamed_source(source, category)
89
+ else
90
+ fail @strings.invalid_format
91
+ end
92
+ end
93
+
94
+ # Normalizes unnamed source with multiple items.
95
+ #
96
+ # - *Args*:
97
+ # - +source+ -> Source to normalize (Array).
98
+ # - +category+ -> Source's category (Array).
99
+ # - *Returns*:
100
+ # - Normalized source (Hash).
101
+ #
102
+ def normalize_multiple_items(source, category)
103
+ { name: source.join(' + '), category: category, path: source }
104
+ end
105
+
106
+ # Normalizes named source.
107
+ #
108
+ # - *Args*:
109
+ # - +source+ -> Source to normalize (Hash).
110
+ # - +category+ -> Source's category (Array).
111
+ # - *Returns*:
112
+ # - Normalized source (Hash).
113
+ #
114
+ def normalize_named_source(source, category)
115
+ { name: source.keys[0], category: category, path: source.values[0] }
116
+ end
117
+
118
+ # Normalizes unnamed source with single item.
119
+ #
120
+ # - *Args*:
121
+ # - +source+ -> Source to normalize (String).
122
+ # - +category+ -> Source's category (Array).
123
+ # - *Returns*:
124
+ # - Normalized source (Hash).
125
+ #
126
+ def normalize_unnamed_source(source, category)
127
+ { name: source, category: category, path: [] << source }
128
+ end
129
+
130
+ # Parses sources' categories.
131
+ #
132
+ # - *Args*:
133
+ # - +sources+ -> Sources loaded from YAML (Hash).
134
+ # - +current_category+ -> Source's category (Array).
135
+ # - *Raises*:
136
+ # - +RuntimeError+ -> If category's contents is not Array nor Hash.
137
+ #
138
+ def parse(sources, current_category = [])
139
+ sources.each do |category, contents|
140
+ case contents
141
+ when Hash
142
+ parse(contents, current_category + ([] << category))
143
+ when Array
144
+ parse_category(contents, current_category + ([] << category))
145
+ else
146
+ fail @strings.invalid_format
147
+ end
148
+ end
149
+ end
150
+
151
+ # Parses category contents.
152
+ #
153
+ # - *Args*:
154
+ # - +contents+ -> Category's contents (Array).
155
+ # - +category+ -> Source's category (Array).
156
+ #
157
+ def parse_category(contents, category)
158
+ contents.each { |source| @sources << normalize(source, category) }
159
+ end
160
+
161
+ # Parses string of category modifiers into two arrays (adds, rms).
162
+ #
163
+ # - *Args*:
164
+ # - +mods+ -> Category modifiers (String).
165
+ # - *Returns*:
166
+ # - Parsed modifiers (Hash).
167
+ #
168
+ def parse_mods(mods)
169
+ parsed_mods = { adds: [], rms: [] }
170
+
171
+ mods.split(' ').each do |mod|
172
+ if mod.split('.').first.empty?
173
+ parsed_mods[:rms] << mod.split('.').drop(1)
174
+ else
175
+ parsed_mods[:adds] << mod.split('.')
176
+ end
177
+ end
178
+
179
+ parsed_mods
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,91 @@
1
+ # = storage.rb
2
+ # This file contains methods for managing downloaded sources.
3
+ #
4
+ # == Contact
5
+ #
6
+ # Author:: Petr Schmied (mailto:jblack@paworld.eu)
7
+ # Website:: http://www.paworld.eu
8
+ # Date:: September 20, 2015
9
+
10
+ module Budik
11
+ # 'Storage' class downloads and manages media sources/items.
12
+ class Storage
13
+ include Singleton
14
+
15
+ # Loads sources, download directory and download method.
16
+ def initialize
17
+ @sources = Sources.instance.sources
18
+ dir = Config.instance.options['sources']['download']['dir']
19
+ @dir = File.expand_path(dir) + '/'
20
+ @method = Config.instance.options['sources']['download']['method']
21
+ end
22
+
23
+ # Gets sources, download directory and download method.
24
+ attr_accessor :sources, :dir, :method
25
+
26
+ # Downloads specified source or all sources.
27
+ #
28
+ # - *Args*:
29
+ # - +source+ -> Source to download (Hash).
30
+ #
31
+ def download_sources(source = nil)
32
+ if source
33
+ IO.instance.storage_download_info(source)
34
+ source[:path].each do |path|
35
+ download_youtube(YouTubeAddy.extract_video_id(path), path)
36
+ end
37
+ else
38
+ @sources.each { |src| download_sources(src) }
39
+ end
40
+ end
41
+
42
+ # Downloads video from YouTube by ID with specified options.
43
+ #
44
+ # - *Args*:
45
+ # - +id+ -> YouTube video ID (String).
46
+ # - +address+ -> YouTube video address (String).
47
+ #
48
+ def download_youtube(id, address)
49
+ return unless id && !File.file?(@dir + id + '.mp4')
50
+
51
+ # TODO: Update youtube-dl if fail
52
+ # TODO: username + password
53
+ options = { output: @dir + '%(id)s.%(ext)s',
54
+ format: 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4',
55
+ playlist: false }
56
+ YoutubeDL.download '"' + address + '"', options
57
+ end
58
+
59
+ # Gets downloaded item's location.
60
+ #
61
+ # - *Args*:
62
+ # - +item+ -> Item to locate (String).
63
+ # - *Returns*:
64
+ # - Path to downloaded file.
65
+ # - Unaltered path if streaming or if the item is local.
66
+ #
67
+ def locate_item(item)
68
+ return item if @method == 'stream'
69
+ is_url = (item =~ /\A#{URI.regexp(%w(http https))}\z/)
70
+ is_url ? @dir + YouTubeAddy.extract_video_id(item) + '.mp4' : item
71
+ end
72
+
73
+ # Removes specified source or all sources.
74
+ #
75
+ # - *Args*:
76
+ # - +source+ -> Source to remove (Hash).
77
+ #
78
+ def remove_sources(source = nil)
79
+ return unless @method == 'remove'
80
+
81
+ if source
82
+ source[:path].each do |path|
83
+ next if locate_item(path) == path
84
+ FileUtils.rm File.expand_path(locate_item(path)), force: true
85
+ end
86
+ else
87
+ @sources.each { |src| remove_sources(src) }
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,15 @@
1
+ # = version.rb
2
+ # This file defines application's version.
3
+ #
4
+ # == Contact
5
+ #
6
+ # Author:: Petr Schmied (mailto:jblack@paworld.eu)
7
+ # Website:: http://www.paworld.eu
8
+ # Date:: September 20, 2015
9
+
10
+ # 'Budik' is an alarm clock that randomly plays an item from your media
11
+ # collection (local or YouTube).
12
+ module Budik
13
+ # Application's version
14
+ VERSION = '1.0.0'
15
+ end
metadata ADDED
@@ -0,0 +1,288 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: budik
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Petr Schmied
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-09-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: colorize
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: commander
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: r18n-core
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sys-uname
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: terminal-table
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: ya2yaml
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: youtube_addy
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: youtube-dl.rb
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: bundler
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '1.10'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '1.10'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rake
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '10.0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '10.0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rspec
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: cucumber
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: rubocop
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: coveralls
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: rdoc
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
223
+ description: Alarm clock that randomly plays a song or a video from YouTube or your
224
+ local collection.
225
+ email:
226
+ - jblack@paworld.eu
227
+ executables:
228
+ - budik
229
+ extensions: []
230
+ extra_rdoc_files: []
231
+ files:
232
+ - ".coveralls.yml"
233
+ - ".gitignore"
234
+ - ".rspec"
235
+ - ".travis.yml"
236
+ - CODE_OF_CONDUCT.md
237
+ - Gemfile
238
+ - LICENSE.txt
239
+ - README.md
240
+ - Rakefile
241
+ - bin/budik
242
+ - bin/console
243
+ - bin/setup
244
+ - budik.gemspec
245
+ - config/templates/lang/en.yml
246
+ - config/templates/options/linux.yml
247
+ - config/templates/options/rpi.yml
248
+ - config/templates/options/windows.yml
249
+ - config/templates/sources/sources.yml
250
+ - lib/budik.rb
251
+ - lib/budik/command.rb
252
+ - lib/budik/config.rb
253
+ - lib/budik/devices.rb
254
+ - lib/budik/io.rb
255
+ - lib/budik/player.rb
256
+ - lib/budik/rng.rb
257
+ - lib/budik/sources.rb
258
+ - lib/budik/storage.rb
259
+ - lib/budik/version.rb
260
+ homepage: http://jblack.paworld.eu/apps/budik
261
+ licenses:
262
+ - MIT
263
+ metadata:
264
+ allowed_push_host: https://rubygems.org
265
+ post_install_message: |-
266
+ Please make sure VLC/omxplayer and FFmpeg/Libav are installed.
267
+ Run 'budik(.bat) config' to edit app's options as needed.
268
+ Run 'budik(.bat) sources -e' to edit your media sources.
269
+ rdoc_options: []
270
+ require_paths:
271
+ - lib
272
+ required_ruby_version: !ruby/object:Gem::Requirement
273
+ requirements:
274
+ - - ">="
275
+ - !ruby/object:Gem::Version
276
+ version: '0'
277
+ required_rubygems_version: !ruby/object:Gem::Requirement
278
+ requirements:
279
+ - - ">="
280
+ - !ruby/object:Gem::Version
281
+ version: '0'
282
+ requirements: []
283
+ rubyforge_project:
284
+ rubygems_version: 2.4.8
285
+ signing_key:
286
+ specification_version: 4
287
+ summary: Alarm clock.
288
+ test_files: []