budik 1.0.0

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/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: []