somadic 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cd60324d1c5fe7d3a5c68e875726425dd502bfeb
4
+ data.tar.gz: 01443ef9a424ca6ab52d99672b1b72413b977989
5
+ SHA512:
6
+ metadata.gz: 10dcd6dc6e4ea32ef9b3f21292781c54be5f50918db340b6fd76c5b8c73e920c98dda4ce65f8ad666313c099dacff05e3784f0568290281fd0a8b6b28e354d5d
7
+ data.tar.gz: 6de3b6f5669d335f936dabf10a4c28c7ec9f1ff1536777da20e8d6638a540ff9f08d24bce47e7fcc382807a4d10f33d94ace1827fa69e4e6946181941ac9ca35
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ log/*
19
+ tags
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in somadic.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,6 @@
1
+ guard :rspec, cmd: 'bundle exec rspec' do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
6
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Shane Thomas
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # Somadic
2
+
3
+ Somadic is a bare-bones terminal-based player for somafm.com and di.fm. It uses `mplayer` to
4
+ do the heavy lifting.
5
+
6
+ ```
7
+ $ somadic-curses di:breaks
8
+
9
+ [ breaks ][ Rave Channel - Te Quiero (Amase Breaks Mix) ][ 00:25 / 07:38 ]
10
+ [######..................................................................................]
11
+ : Beware of Pickpockets - Nimbus (Original Mix) 05:27 :
12
+ : Deekline - 01NIGHT MOODS ORIGIONAL MIMAI BASS MIX : +0/-1 : 03:29 :
13
+ : Benny Benassi - Satisfaction (DirTy MaN Mix) 04:40 :
14
+ : Vetoo - Recall (Refracture Remix) 05:40 :
15
+ : Firebeatz feat Schella - Dear New York (Barrera Breaks Mix) 05:07 :
16
+ ```
17
+
18
+ ## Installation
19
+
20
+ Clone the repo:
21
+
22
+ $ git clone https://github.com/devshane/somadic.git
23
+
24
+ Build the gem:
25
+
26
+ $ gem build somadic.gemspec
27
+
28
+ Install the gem:
29
+
30
+ $ gem install somadic-0.0.1.gem
31
+
32
+ ## Usage
33
+
34
+ ```
35
+ Usage: somadic [options] [preset_name | [site1:channel1 ...]]
36
+
37
+ You can specify either a `preset_name` or an arbitrary list of `site:channel` identifiers.
38
+
39
+ site: either `di` or `soma`
40
+ channel: a valid channel on `site`
41
+
42
+ DI premium channels require an environment variable: DI_FM_PREMIUM_ID.
43
+
44
+ -c, --cache CACHE_SIZE Set the cache size (KB)
45
+ -m, --cache-min CACHE_MIN Set the minimum cache threshold (percent)
46
+ -h, --help Display this message
47
+ ```
48
+
49
+ #### Valid keys
50
+
51
+ ```
52
+ n - Next site:channel in list
53
+ N - Pick a random channel from `site`
54
+ q - Quit
55
+ r - Refresh the display
56
+ s - Search Google for the current track
57
+ <space> - Start/stop playing current channel
58
+ / - Goto site:channel
59
+ ```
60
+
61
+ #### Examples
62
+
63
+ **Listen to breaks on DI**
64
+ ```
65
+ $ somadic di:breaks
66
+ ```
67
+
68
+ **Listen to breaks, psychill, and secret agent**
69
+ ```
70
+ $ somadic di:breaks di:psychill soma:secretagent
71
+ ```
72
+
73
+ **Listen to the chill preset (assumes a ~/.somadic/presets/chill.yaml file)**
74
+ ```
75
+ $ somadic chill
76
+ ```
77
+
78
+ ## Contributing
79
+
80
+ 1. Fork it ( http://github.com/devshane/somadic/fork )
81
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
82
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
83
+ 4. Push to the branch (`git push origin my-new-feature`)
84
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/somadic ADDED
@@ -0,0 +1,424 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+ require 'somadic'
4
+ require 'curses'
5
+ require 'progress_bar'
6
+ require 'thread'
7
+ require 'chronic'
8
+ require 'readline'
9
+ require 'yaml'
10
+
11
+ SOMADIC_PATH = ENV['HOME'] + '/.somadic'
12
+
13
+ module OS
14
+ def OS.windows?
15
+ (/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil
16
+ end
17
+
18
+ def OS.mac?
19
+ (/darwin/ =~ RUBY_PLATFORM) != nil
20
+ end
21
+
22
+ def OS.unix?
23
+ !OS.windows?
24
+ end
25
+
26
+ def OS.linux?
27
+ OS.unix? and not OS.mac?
28
+ end
29
+ end
30
+
31
+ # Monkey-patches ProgressBar so that it displays periods instead of blank
32
+ # spaces.
33
+ class ProgressBar
34
+ def render_bar
35
+ return '' if bar_width < 2
36
+ "[" +
37
+ "#" * (ratio * (bar_width - 2)).ceil +
38
+ "." * ((1-ratio) * (bar_width - 2)).floor +
39
+ "]"
40
+ end
41
+ end
42
+
43
+ # A curses display.
44
+ class Display
45
+ include Curses
46
+
47
+ attr_reader :channel
48
+ attr_accessor :kp_queue, :stopped, :search_phrase, :inputting
49
+
50
+ def initialize
51
+ curses_init
52
+ @bar = ProgressBar.new(1, :bar)
53
+
54
+ @kp_queue = Queue.new
55
+ start_keypress_thread
56
+ end
57
+
58
+ # Refreshes the display.
59
+ def refresh
60
+ Somadic::Logger.debug('Display#refresh')
61
+ Curses.clear
62
+ Curses.refresh
63
+ end
64
+
65
+ def search(channel)
66
+ #cpos Curses.lines - 1, 0
67
+ @inputting = true
68
+ Curses.close_screen
69
+ @search_phrase = Readline.readline('Go to channel: ', true)
70
+ @search_phrase = '' unless @search_phrase[':']
71
+ cwrite Curses.lines - 1, 0, ''
72
+ @inputting = false
73
+ end
74
+
75
+ def clear_search
76
+ cwrite Curses.lines - 1, 0, ''
77
+ end
78
+
79
+ # Updates the display.
80
+ def update(channel = nil, songs = nil)
81
+ @channel = channel if channel
82
+ @songs = songs if songs
83
+
84
+ return if @channel.nil? || @songs.nil?
85
+
86
+ cur_song = @songs.first
87
+ return if cur_song.nil?
88
+
89
+ # times
90
+ start_time = Time.at(cur_song[:started]) rescue Time.now
91
+ duration = cur_song[:duration]
92
+ if @stopped
93
+ end_time = nil
94
+ elapsed = (Time.now - start_time).to_i
95
+ remains = '][ Paused ]'
96
+ elsif duration <= 0
97
+ end_time = nil
98
+ elapsed = (Time.now - start_time).to_i
99
+ remains = duration < 0 ?
100
+ '][ Updating ]' :
101
+ "][ #{format_secs(elapsed)} ]"
102
+ else
103
+ end_time = start_time + duration
104
+ remains = "][ #{format_secs((Time.now - start_time).to_i)} " \
105
+ "/ #{format_secs(duration)} ]"
106
+ end
107
+
108
+ # current song
109
+ track = cur_song[:track]
110
+ channel_and_track = "[ #{clean_channel_name(@channel[:name])} > #{track}"
111
+
112
+ up = cur_song[:votes][:up]
113
+ down = cur_song[:votes][:down]
114
+ votes = up + down != 0 ? "+#{up}/-#{down}" : ''
115
+
116
+ space_len = Curses.cols - votes.length - channel_and_track.length - remains.length - 1
117
+ spaces = space_len > 0 ? ' ' * space_len : ' '
118
+
119
+ line = "#{channel_and_track}#{spaces}#{votes} #{remains}"
120
+ over = Curses.cols - line.length
121
+ if over < 0
122
+ channel_and_track = channel_and_track[0..over - 1]
123
+ line = "#{channel_and_track}#{spaces}#{votes} #{remains}"
124
+ end
125
+ cwrite 0, 0, line, curses_reverse
126
+
127
+ # current song progress
128
+ unless @stopped
129
+ if duration <= 0
130
+ @bar.max = @bar.count = 100
131
+ else
132
+ @bar.max = duration
133
+ @bar.count = (Time.now - start_time).to_i
134
+ end
135
+ cwrite 1, 0, @bar.to_s, curses_bold
136
+ end
137
+
138
+ # song history
139
+ row = 2
140
+ @songs[1..6].each do |song|
141
+ up = song[:votes][:up]
142
+ down = song[:votes][:down]
143
+ votes = up + down != 0 ? " +#{up}/-#{down} :" : ''
144
+
145
+ if song[:duration] == 0
146
+ duration = Time.at(song[:started]).strftime('%H:%M:%S')
147
+ else
148
+ duration = format_secs(song[:duration])
149
+ end
150
+
151
+ track = ": #{song[:track]}"
152
+ votes_and_duration = "#{votes} #{duration} :"
153
+
154
+ space_len = Curses.cols - track.length - votes_and_duration.length
155
+ spaces = space_len > 0 ? ' ' * space_len : ''
156
+
157
+ line = "#{track}#{spaces}#{votes_and_duration}"
158
+ if space_len < 0
159
+ spaces = ' '
160
+ track = track[0..space_len - 2]
161
+ line = "#{track}#{spaces}#{votes_and_duration}"
162
+ end
163
+ cwrite row, 0, line, curses_dim
164
+ row += 1
165
+ end
166
+ # TODO: this works around the dupe thing @startup, but it shouldn't be
167
+ # necessary
168
+ cwrite row, 0, ''
169
+ cpos Curses.lines - 1, 0
170
+ end
171
+
172
+ private
173
+
174
+ def start_keypress_thread
175
+ Thread.new do
176
+ loop do
177
+ unless @inputting
178
+ ch = Curses.getch
179
+ @kp_queue << ch if ch
180
+ end
181
+ sleep 0.1
182
+ end
183
+ end
184
+ end
185
+
186
+ # Curses init
187
+ def curses_init
188
+ #Curses.noecho
189
+ #Curses.curs_set(0)
190
+ Curses.timeout = -1
191
+
192
+ Curses.init_screen
193
+ Curses.start_color
194
+
195
+ Curses.init_pair(COLOR_WHITE, COLOR_WHITE, COLOR_BLACK)
196
+ end
197
+
198
+ # Curses write
199
+ def cwrite(row, col, message, color = nil)
200
+ Curses.setpos(row, col)
201
+ Curses.clrtoeol
202
+
203
+ if color
204
+ Curses.attron(color) { Curses.addstr(message) }
205
+ else
206
+ Curses.addstr(message)
207
+ end
208
+
209
+ Curses.refresh
210
+ end
211
+
212
+ # Cursor pos
213
+ def cpos(row, col)
214
+ Curses.setpos(row, col)
215
+ Curses.refresh
216
+ end
217
+
218
+ # Colors/styles.
219
+ def curses_bold
220
+ curses_white|A_BOLD
221
+ end
222
+
223
+ def curses_reverse
224
+ curses_white|A_REVERSE
225
+ end
226
+
227
+ def curses_dim
228
+ curses_white|A_DIM
229
+ end
230
+
231
+ def curses_white
232
+ color_pair(COLOR_WHITE)
233
+ end
234
+
235
+ # Formats `seconds` to hours, mins, secs.
236
+ def format_secs(seconds)
237
+ secs = seconds.abs
238
+ hours = 0
239
+ if secs > 3600
240
+ hours = secs / 3600
241
+ secs -= 3600 * hours
242
+ end
243
+ mins = secs / 60
244
+ secs = secs % 60
245
+ h = hours > 0 ? "#{"%1d" % hours}:" : " "
246
+ "#{h}#{"%02d" % mins}:#{"%02d" % secs}"
247
+ end
248
+
249
+ # Cleans up soma channel names.
250
+ def clean_channel_name(name)
251
+ cname = name.gsub(/130$/, '')
252
+ cname.gsub!(/64$/, '')
253
+ cname
254
+ end
255
+ end
256
+
257
+ Signal.trap("INT") do |sig|
258
+ @channel.stop
259
+ exit
260
+ end
261
+
262
+ @display = Display.new
263
+ @options = { cache: nil,
264
+ cache_min: nil,
265
+ listeners: [@display] }
266
+
267
+ @optparser = OptionParser.new do |o|
268
+ o.banner = 'Usage: somadic [options] site:channel [site:channel]'
269
+ o.separator ''
270
+ o.separator 'The `site` parameter can be di or soma. `channel` should be'
271
+ o.separator 'a valid channel on that site.'
272
+ o.separator ''
273
+ o.separator 'DI premium channels require an environment variable: ' \
274
+ 'DI_FM_PREMIUM_ID.'
275
+ o.separator ''
276
+
277
+ o.on('-c CACHE_SIZE', '--cache CACHE_SIZE', 'Set the cache size (KB)') do |c|
278
+ @options[:cache] = c
279
+ end
280
+ o.on('-m CACHE_MIN', '--cache-min CACHE_MIN',
281
+ 'Set the minimum cache threshold (percent)') do |m|
282
+ @options[:cache_min] = m
283
+ end
284
+ o.on('-h', '--help', 'Display this message') { puts o; exit }
285
+
286
+ o.parse!
287
+ end
288
+
289
+ def usage
290
+ puts @optparser
291
+ puts
292
+ exit
293
+ end
294
+
295
+ def next_channel
296
+ @cur_chan ||= 0
297
+
298
+ rv = @channels[@cur_chan]
299
+ @cur_chan += 1
300
+ @cur_chan = 0 if @cur_chan == @channels.count
301
+ rv
302
+ end
303
+
304
+ def start_playing
305
+ who, what = next_channel.split(':')
306
+ @options[:channel] = what
307
+ @options[:premium_id] = ENV['DI_FM_PREMIUM_ID']
308
+ if who == 'di'
309
+ @channel = Somadic::Channel::DI.new(@options)
310
+ else
311
+ @channel = Somadic::Channel::Soma.new(@options)
312
+ end
313
+ @channel.start
314
+ end
315
+
316
+ def start(channels)
317
+ Somadic::Logger.debug("somadic-curses, started with #{channels}")
318
+
319
+ @channels = []
320
+ channels.each do |channel|
321
+ if channel[':']
322
+ @channels << channel
323
+ else
324
+ # is there a preset file?
325
+ fn = File.join(SOMADIC_PATH, 'presets', "#{channel}.yaml")
326
+ if File.exist?(fn)
327
+ YAML.load_file(fn).each { |c| @channels << c }
328
+ else
329
+ fail ArgumentError, "`#{channel}` is not a valid channel or preset."
330
+ end
331
+ end
332
+ end
333
+
334
+ start_playing
335
+
336
+ # keypresses are handled thru a Queue
337
+ keypresses = []
338
+ quitting = false
339
+ stopped = false
340
+ while !quitting
341
+ begin
342
+ keypresses << @display.kp_queue.pop(non_block: true)
343
+ rescue ThreadError => te
344
+ unless te.to_s == "queue empty"
345
+ Somadic::Logger.error("kp_queue.pop error: #{te}")
346
+ end
347
+ end
348
+ unless keypresses.empty?
349
+ keypresses.each do |kp|
350
+ # Somadic::Logger.debug("kp: #{kp} (a #{kp.class}) (searching=#{searching})")
351
+ case kp
352
+ when ' '
353
+ @channel.send(stopped ? :start : :stop)
354
+ stopped = !stopped
355
+ when 'c'
356
+ dump_channels
357
+ when 'n'
358
+ goto_next_channel
359
+ when 'N'
360
+ goto_next_channel_random
361
+ when 'q'
362
+ @channel.stop
363
+ quitting = true
364
+ when 'r'
365
+ @display.refresh
366
+ when 's'
367
+ search
368
+ when '/'
369
+ @display.search(@channel)
370
+ if @display.search_phrase
371
+ Somadic::Logger.debug("searching: #{@display.search_phrase}")
372
+ goto_channel(@display.search_phrase)
373
+ end
374
+ end
375
+ keypresses.delete(kp)
376
+ end
377
+ end
378
+
379
+ @display.stopped = stopped
380
+ @display.update
381
+ sleep 0.1
382
+ end
383
+ end
384
+
385
+ def dump_channels
386
+ @channel.channels.each do |c|
387
+ Somadic::Logger.debug("channel: #{c}")
388
+ end
389
+ end
390
+
391
+ def goto_channel(channel)
392
+ @channel.stop
393
+ who, what = channel.split(':')
394
+ Somadic::Logger.debug("goto_channel: going to #{who}:#{what}")
395
+ @options[:channel] = what
396
+ if who == 'di'
397
+ @channel = Somadic::Channel::DI.new(@options)
398
+ else
399
+ @channel = Somadic::Channel::Soma.new(@options)
400
+ end
401
+ @channel.start
402
+ end
403
+
404
+ def goto_next_channel
405
+ goto_channel(next_channel)
406
+ end
407
+
408
+ def goto_next_channel_random
409
+ who = @channel.is_a?(Somadic::Channel::DI) ? 'di' : 'soma'
410
+ what = @channel.channels.reject { |c| c[:name] == @display.channel[:name] }.sample[:name]
411
+ goto_channel("#{who}:#{what}")
412
+ end
413
+
414
+ def search
415
+ Somadic::Logger.debug("searching for '#{@channel.song}'")
416
+ if OS.mac?
417
+ `open "https://www.google.com/search?safe=off&q=#{@channel.song}"`
418
+ elsif OS.linux?
419
+ `xdg-open "https://www.google.com/search?safe=off&q=#{@channel.song}"`
420
+ end
421
+ end
422
+
423
+ usage if ARGV[0].nil?
424
+ start(ARGV)