somadic 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ca4d0b5f1262c05f4c93871065843f3a08b16da5
4
- data.tar.gz: dd8035445c3828512c2acfe0a80faf62e68020d7
3
+ metadata.gz: e781f2967b238c8b0babce28ff7688a48bf01ce9
4
+ data.tar.gz: b35d981810b5b6f87dabaebf1155892d239935dd
5
5
  SHA512:
6
- metadata.gz: 824df614dc6f57756a05a79e1fc3e149e9e7ed78c14c2d852f81d1b517ebaac14e590b51f115a4b7d1715369df7b4316fa29b7bda4e7329e04d077196af75202
7
- data.tar.gz: 3e1fb4eafd3569ed18942bc4c483041593209aa405df7c0da4cb6cae2f60452069515bf75b00a98f7f1bd59d43e44437ec105923b98dab15e3619ff830faf3c7
6
+ metadata.gz: c68b44cbae2e7bff1a956bf78cb43a00dabf092a58ecd6f52acf25a71408a4c1702b47bd566124d5059dc153033860065ad491a965860005a5ddba7589ef017e
7
+ data.tar.gz: 370cce2131766c31daa4b96f8e0f3e09b013500a71b384c0ff01fd0190b032ab20b8cdab87f3c9452cecbe0fba26e7fb164c57bfeaa5d70cf19805f3310e0373
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Somadic
2
2
 
3
3
  Somadic is a bare-bones terminal-based player for [somafm.com](http://somafm.com) and [di.fm](http://di.fm).
4
- It uses `mplayer` to do the heavy lifting.
4
+ It uses `mplayer` to do the heavy lifting. It's a hot Curses mess, but it works on my machine.
5
5
 
6
6
  ```
7
- $ somadic-curses di:breaks
7
+ $ somadic di:breaks
8
8
 
9
9
  [ breaks ][ Rave Channel - Te Quiero (Amase Breaks Mix) ][ 00:25 / 07:38 ]
10
10
  [######..................................................................................]
@@ -41,6 +41,7 @@ DI premium channels require an environment variable: DI_FM_PREMIUM_ID.
41
41
  #### Valid keys
42
42
 
43
43
  ```
44
+ c - List channels for `site`
44
45
  n - Next site:channel in list
45
46
  N - Pick a random channel from `site`
46
47
  q - Quit
@@ -0,0 +1,259 @@
1
+ # A curses display.
2
+ class Display
3
+ include Curses
4
+
5
+ attr_reader :channel
6
+ attr_accessor :kp_queue, :stopped, :search_phrase, :inputting
7
+
8
+ def initialize
9
+ curses_init
10
+ @bar = ProgressBar.new(1, :bar)
11
+
12
+ @kp_queue = Queue.new
13
+ start_keypress_thread
14
+ end
15
+
16
+ # Refreshes the display.
17
+ def refresh
18
+ Somadic::Logger.debug('Display#refresh')
19
+ Curses.clear
20
+ Curses.refresh
21
+ end
22
+
23
+ def search(channel)
24
+ #cpos Curses.lines - 1, 0
25
+ @inputting = true
26
+ Curses.close_screen
27
+ @search_phrase = Readline.readline('Go to channel: ', true)
28
+ @search_phrase = '' unless @search_phrase[':']
29
+ cwrite Curses.lines - 1, 0, ''
30
+ @inputting = false
31
+ end
32
+
33
+ def clear_search
34
+ cwrite Curses.lines - 1, 0, ''
35
+ end
36
+
37
+ # Updates the display.
38
+ def update(channel = nil, songs = nil)
39
+ @channel = channel if channel
40
+ @songs = songs if songs
41
+ return if @channel.nil? || @songs.nil?
42
+
43
+ cur_song = @songs.first
44
+ return if cur_song.nil?
45
+
46
+ # times
47
+ start_time = Time.at(cur_song[:started]) rescue Time.now
48
+ duration = cur_song[:duration]
49
+ if @stopped
50
+ end_time = nil
51
+ elapsed = (Time.now - start_time).to_i
52
+ remains = '][ Paused ]'
53
+ elsif duration <= 0
54
+ end_time = nil
55
+ elapsed = (Time.now - start_time).to_i
56
+ remains = duration < 0 ?
57
+ '][ Updating ]' :
58
+ "][ #{format_secs(elapsed)} ]"
59
+ else
60
+ end_time = start_time + duration
61
+ remains = "][ #{format_secs((Time.now - start_time).to_i)} " \
62
+ "/ #{format_secs(duration)} ]"
63
+ end
64
+
65
+ # current song
66
+ track = cur_song[:track]
67
+ channel_and_track = "[ #{clean_channel_name(@channel[:display_name])} > #{track}"
68
+
69
+ up = cur_song[:votes][:up]
70
+ down = cur_song[:votes][:down]
71
+ votes = up + down != 0 ? "+#{up}/-#{down}" : ''
72
+
73
+ space_len = Curses.cols - votes.length - channel_and_track.length - remains.length - 1
74
+ spaces = space_len > 0 ? ' ' * space_len : ' '
75
+
76
+ line = "#{channel_and_track}#{spaces}#{votes} #{remains}"
77
+ over = Curses.cols - line.length
78
+ if over < 0
79
+ channel_and_track = channel_and_track[0..over - 1]
80
+ line = "#{channel_and_track}#{spaces}#{votes} #{remains}"
81
+ end
82
+ cwrite 0, 0, line, curses_reverse
83
+
84
+ # current song progress
85
+ unless @stopped
86
+ if duration <= 0
87
+ @bar.max = @bar.count = 100
88
+ else
89
+ @bar.max = duration
90
+ @bar.count = (Time.now - start_time).to_i
91
+ end
92
+ cwrite 1, 0, @bar.to_s, curses_bold
93
+ end
94
+
95
+ # song history
96
+ row = 2
97
+ @songs[1..-1].each do |song|
98
+ up = song[:votes][:up]
99
+ down = song[:votes][:down]
100
+ votes = up + down != 0 ? " +#{up}/-#{down} :" : ''
101
+
102
+ if song[:duration] == 0
103
+ duration = Time.at(song[:started]).strftime('%H:%M:%S')
104
+ else
105
+ duration = format_secs(song[:duration])
106
+ end
107
+
108
+ track = ": #{song[:track]}"
109
+ votes_and_duration = "#{votes} #{duration} :"
110
+
111
+ space_len = Curses.cols - track.length - votes_and_duration.length
112
+ spaces = space_len > 0 ? ' ' * space_len : ''
113
+
114
+ line = "#{track}#{spaces}#{votes_and_duration}"
115
+ if space_len < 0
116
+ spaces = ' '
117
+ track = track[0..space_len - 2]
118
+ line = "#{track}#{spaces}#{votes_and_duration}"
119
+ end
120
+ cwrite row, 0, line, curses_dim
121
+ row += 1
122
+ end
123
+
124
+ while row < Curses.lines - 1
125
+ clear_line row
126
+ row += 1
127
+ end
128
+
129
+ # TODO: this works around the dupe thing @startup, but it shouldn't be
130
+ # necessary
131
+ cwrite row, 0, ''
132
+ cpos Curses.lines - 1, 0
133
+ end
134
+
135
+ def show_channels(list)
136
+ container = Curses::Window.new(0, 0, 0, 0)
137
+ w = container.subwin(Curses.lines, Curses.cols, 0, 0)
138
+ w.addstr("[ Channels ]\n\n")
139
+ #w.setscrreg(0, Curses.lines)
140
+ #w.scrollok(true)
141
+ w.addstr("#{in_columns(list)}\n")
142
+ w.addstr("Press q to close channel list.")
143
+ w.getch
144
+ w.close
145
+
146
+ refresh
147
+ end
148
+
149
+ private
150
+
151
+ # Breaks a channel list up into columns.
152
+ def in_columns(list)
153
+ longest = 0
154
+ list.each { |l| longest = l[:name].length if l[:name].length > longest }
155
+ chan_list = list.map do |l|
156
+ ll = longest - l[:name].length
157
+ pad = if ll > 0
158
+ ' ' * ll
159
+ else
160
+ ''
161
+ end
162
+ "#{l[:name]}#{pad}"
163
+ end.sort
164
+
165
+ cols = Curses.cols / (longest + 1)
166
+ groups = chan_list.each_slice(cols).to_a
167
+ chans = ''
168
+ groups.each { |g| chans << "#{g.join(' ')}\n" }
169
+
170
+ chans
171
+ end
172
+
173
+ def start_keypress_thread
174
+ Thread.new do
175
+ loop do
176
+ unless @inputting
177
+ ch = Curses.getch
178
+ @kp_queue << ch if ch
179
+ end
180
+ sleep 0.1
181
+ end
182
+ end
183
+ end
184
+
185
+ # Curses init
186
+ def curses_init
187
+ #Curses.noecho
188
+ #Curses.curs_set(0)
189
+ Curses.timeout = -1
190
+
191
+ Curses.init_screen
192
+ Curses.start_color
193
+
194
+ Curses.init_pair(COLOR_WHITE, COLOR_WHITE, COLOR_BLACK)
195
+ end
196
+
197
+ # Curses write
198
+ def cwrite(row, col, message, color = nil)
199
+ Curses.setpos(row, col)
200
+ Curses.clrtoeol
201
+
202
+ if color
203
+ Curses.attron(color) { Curses.addstr(message) }
204
+ else
205
+ Curses.addstr(message)
206
+ end
207
+
208
+ Curses.refresh
209
+ end
210
+
211
+ # Cursor pos
212
+ def cpos(row, col)
213
+ Curses.setpos(row, col)
214
+ Curses.refresh
215
+ end
216
+
217
+ # Colors/styles.
218
+ def curses_bold
219
+ curses_white|A_BOLD
220
+ end
221
+
222
+ def curses_reverse
223
+ curses_white|A_REVERSE
224
+ end
225
+
226
+ def curses_dim
227
+ curses_white|A_DIM
228
+ end
229
+
230
+ def curses_white
231
+ color_pair(COLOR_WHITE)
232
+ end
233
+
234
+ def clear_line(row)
235
+ Curses.setpos(row, 0)
236
+ Curses.clrtoeol
237
+ end
238
+
239
+ # Formats `seconds` to hours, mins, secs.
240
+ def format_secs(seconds)
241
+ secs = seconds.abs
242
+ hours = 0
243
+ if secs > 3600
244
+ hours = secs / 3600
245
+ secs -= 3600 * hours
246
+ end
247
+ mins = secs / 60
248
+ secs = secs % 60
249
+ h = hours > 0 ? "#{"%1d" % hours}:" : " "
250
+ "#{h}#{"%02d" % mins}:#{"%02d" % secs}"
251
+ end
252
+
253
+ # Cleans up soma channel names.
254
+ def clean_channel_name(name)
255
+ cname = name.gsub(/130$/, '')
256
+ cname.gsub!(/64$/, '')
257
+ cname
258
+ end
259
+ end
@@ -0,0 +1,17 @@
1
+ module OS
2
+ def OS.windows?
3
+ (/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil
4
+ end
5
+
6
+ def OS.mac?
7
+ (/darwin/ =~ RUBY_PLATFORM) != nil
8
+ end
9
+
10
+ def OS.unix?
11
+ !OS.windows?
12
+ end
13
+
14
+ def OS.linux?
15
+ OS.unix? and not OS.mac?
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ # Monkey-patches ProgressBar so that it displays periods instead of blank
2
+ # spaces.
3
+ class ProgressBar
4
+ def render_bar
5
+ return '' if bar_width < 2
6
+ "[" +
7
+ "#" * (ratio * (bar_width - 2)).ceil +
8
+ "." * ((1-ratio) * (bar_width - 2)).floor +
9
+ "]"
10
+ end
11
+ end
@@ -0,0 +1,180 @@
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
+ base_path = File.expand_path(File.dirname(__FILE__))
14
+ Dir["#{base_path}/curses/lib/*.rb"].each do |file|
15
+ Somadic::Logger.debug("require #{file}")
16
+ require file
17
+ end
18
+
19
+ Signal.trap("INT") do |sig|
20
+ @channel.stop
21
+ exit
22
+ end
23
+
24
+ @display = Display.new
25
+ @options = { cache: nil,
26
+ cache_min: nil,
27
+ listeners: [@display] }
28
+
29
+ @optparser = OptionParser.new do |o|
30
+ o.banner = 'Usage: somadic [options] site:channel [site:channel]'
31
+ o.separator ''
32
+ o.separator 'The `site` parameter can be di or soma. `channel` should be'
33
+ o.separator 'a valid channel on that site.'
34
+ o.separator ''
35
+ o.separator 'DI premium channels require an environment variable: ' \
36
+ 'DI_FM_PREMIUM_ID.'
37
+ o.separator ''
38
+
39
+ o.on('-c CACHE_SIZE', '--cache CACHE_SIZE', 'Set the cache size (KB)') do |c|
40
+ @options[:cache] = c
41
+ end
42
+ o.on('-m CACHE_MIN', '--cache-min CACHE_MIN',
43
+ 'Set the minimum cache threshold (percent)') do |m|
44
+ @options[:cache_min] = m
45
+ end
46
+ o.on('-h', '--help', 'Display this message') { puts o; exit }
47
+
48
+ o.parse!
49
+ end
50
+
51
+ def usage
52
+ puts @optparser
53
+ puts
54
+ exit
55
+ end
56
+
57
+ def next_channel
58
+ @cur_chan ||= 0
59
+
60
+ rv = @channels[@cur_chan]
61
+ @cur_chan += 1
62
+ @cur_chan = 0 if @cur_chan == @channels.count
63
+ rv
64
+ end
65
+
66
+ def start_playing
67
+ who, what = next_channel.split(':')
68
+ @options[:channel] = what
69
+ @options[:premium_id] = ENV['DI_FM_PREMIUM_ID']
70
+ if who == 'di'
71
+ @channel = Somadic::Channel::DI.new(@options)
72
+ else
73
+ @channel = Somadic::Channel::Soma.new(@options)
74
+ end
75
+ @channel.start
76
+ end
77
+
78
+ def start(channels)
79
+ Somadic::Logger.debug("somadic-curses, started with #{channels}")
80
+
81
+ @channels = []
82
+ channels.each do |channel|
83
+ if channel[':']
84
+ @channels << channel
85
+ else
86
+ # is there a preset file?
87
+ fn = File.join(SOMADIC_PATH, 'presets', "#{channel}.yaml")
88
+ if File.exist?(fn)
89
+ YAML.load_file(fn).each { |c| @channels << c }
90
+ else
91
+ fail ArgumentError, "`#{channel}` is not a valid channel or preset."
92
+ end
93
+ end
94
+ end
95
+
96
+ start_playing
97
+
98
+ # keypresses are handled thru a Queue
99
+ keypresses = []
100
+ quitting = false
101
+ stopped = false
102
+ while !quitting
103
+ begin
104
+ keypresses << @display.kp_queue.pop(non_block: true)
105
+ rescue ThreadError => te
106
+ unless te.to_s == "queue empty"
107
+ Somadic::Logger.error("kp_queue.pop error: #{te}")
108
+ end
109
+ end
110
+ unless keypresses.empty?
111
+ keypresses.each do |kp|
112
+ case kp
113
+ when ' '
114
+ @channel.send(stopped ? :start : :stop)
115
+ stopped = !stopped
116
+ when 'c'
117
+ chanlist = @channel.channel_list
118
+ @display.show_channels(chanlist)
119
+ when 'n'
120
+ goto_next_channel
121
+ when 'N'
122
+ goto_next_channel_random
123
+ when 'q'
124
+ @channel.stop
125
+ quitting = true
126
+ when 'r'
127
+ @display.refresh
128
+ when 's'
129
+ search
130
+ when '/'
131
+ @display.search(@channel)
132
+ if @display.search_phrase
133
+ Somadic::Logger.debug("searching: #{@display.search_phrase}")
134
+ goto_channel(@display.search_phrase)
135
+ end
136
+ end
137
+ keypresses.delete(kp)
138
+ end
139
+ end
140
+
141
+ @display.stopped = stopped
142
+ @display.update
143
+ sleep 0.1
144
+ end
145
+ end
146
+
147
+ def goto_channel(channel)
148
+ @channel.stop
149
+ who, what = channel.split(':')
150
+ Somadic::Logger.debug("goto_channel: going to #{who}:#{what}")
151
+ @options[:channel] = what
152
+ if who == 'di'
153
+ @channel = Somadic::Channel::DI.new(@options)
154
+ else
155
+ @channel = Somadic::Channel::Soma.new(@options)
156
+ end
157
+ @channel.start
158
+ end
159
+
160
+ def goto_next_channel
161
+ goto_channel(next_channel)
162
+ end
163
+
164
+ def goto_next_channel_random
165
+ who = @channel.is_a?(Somadic::Channel::DI) ? 'di' : 'soma'
166
+ what = @channel.channels.reject { |c| c[:name] == @display.channel[:name] }.sample[:name]
167
+ goto_channel("#{who}:#{what}")
168
+ end
169
+
170
+ def search
171
+ Somadic::Logger.debug("searching for '#{@channel.song}'")
172
+ if OS.mac?
173
+ `open "https://www.google.com/search?safe=off&q=#{@channel.song}"`
174
+ elsif OS.linux?
175
+ `xdg-open "https://www.google.com/search?safe=off&q=#{@channel.song}"`
176
+ end
177
+ end
178
+
179
+ usage if ARGV[0].nil?
180
+ start(ARGV)
data/bin/somadic CHANGED
@@ -10,248 +10,10 @@ require 'yaml'
10
10
 
11
11
  SOMADIC_PATH = ENV['HOME'] + '/.somadic'
12
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
13
+ base_path = File.expand_path(File.dirname(__FILE__))
14
+ Dir["#{base_path}/curses/lib/*.rb"].each do |file|
15
+ Somadic::Logger.debug("require #{file}")
16
+ require file
255
17
  end
256
18
 
257
19
  Signal.trap("INT") do |sig|
@@ -347,13 +109,13 @@ def start(channels)
347
109
  end
348
110
  unless keypresses.empty?
349
111
  keypresses.each do |kp|
350
- # Somadic::Logger.debug("kp: #{kp} (a #{kp.class}) (searching=#{searching})")
351
112
  case kp
352
113
  when ' '
353
114
  @channel.send(stopped ? :start : :stop)
354
115
  stopped = !stopped
355
116
  when 'c'
356
- dump_channels
117
+ chanlist = @channel.channel_list
118
+ @display.show_channels(chanlist)
357
119
  when 'n'
358
120
  goto_next_channel
359
121
  when 'N'
@@ -382,12 +144,6 @@ def start(channels)
382
144
  end
383
145
  end
384
146
 
385
- def dump_channels
386
- @channel.channels.each do |c|
387
- Somadic::Logger.debug("channel: #{c}")
388
- end
389
- end
390
-
391
147
  def goto_channel(channel)
392
148
  @channel.stop
393
149
  who, what = channel.split(':')
@@ -7,9 +7,7 @@ module Somadic
7
7
 
8
8
  def refresh_playlist
9
9
  page = open(@url).read
10
- Somadic::Logger.debug("page=#{page}")
11
10
  data = JSON.parse(page[page.index("(") + 1..-3])
12
-
13
11
  symbolized_data = []
14
12
  data.each { |d| symbolized_data << symbolize_keys(d) }
15
13
  @songs = symbolized_data.keep_if { |d| d[:title] }
@@ -49,5 +49,9 @@ module Somadic
49
49
  def find_channel(name)
50
50
  raise NotImplementedError
51
51
  end
52
+
53
+ def channel_list
54
+ @channels
55
+ end
52
56
  end
53
57
  end
@@ -1,5 +1,91 @@
1
- require 'pp'
2
-
1
+ # A wrapper around a DI.fm channel.
2
+ #
3
+ # Channel filters (not currently used, analogous to presets):
4
+ #
5
+ # "channel_filters": [
6
+ # {
7
+ # "channels": [
8
+ # 348,
9
+ # 346,
10
+ # 291,
11
+ # 347
12
+ # ],
13
+ # "display": true,
14
+ # "id": 20,
15
+ # "key": "new",
16
+ # "meta": false,
17
+ # "name": "New",
18
+ # "network_id": 1,
19
+ # "position": 1,
20
+ # "sprite": "//api.audioaddict.com/v1/assets/channel_sprite/di/new{/digest}{.format}{?width,height,quality}"
21
+ # },
22
+ # {
23
+ # "channels": [
24
+ # 1,
25
+ # 2,
26
+ # 175,
27
+ # 7,
28
+ # 8,
29
+ # 346,
30
+ # 90,
31
+ # 125,
32
+ # 178,
33
+ # 176,
34
+ # 10
35
+ # ],
36
+ # "display": true,
37
+ # "id": 5,
38
+ # "key": "trance",
39
+ # "meta": false,
40
+ # "name": "Trance",
41
+ # "network_id": 1,
42
+ # "position": 2,
43
+ # "sprite": "//api.audioaddict.com/v1/assets/channel_sprite/di/trance{/digest}{.format}{?width,height,quality}"
44
+ # },
45
+ # ...
46
+ #
47
+ # Each channel:
48
+ #
49
+ # "channels": [
50
+ # {
51
+ # "ad_channels": "",
52
+ # "asset_id": 54679,
53
+ # "asset_url": "//static.audioaddict.com/e/4/b/3/4/6/e4b346b193c1adec01f8489b98a2bf3f.png",
54
+ # "banner_url": null,
55
+ # "channel_director": "takito jockey",
56
+ # "created_at": "2014-12-02T10:03:54-05:00",
57
+ # "description": "An emphasis on the bass and drums, delayed effects, sampled vocals and smokey Reggae inspired vibes.",
58
+ # "description_long": "",
59
+ # "description_short": "An emphasis on the bass and drums, delayed effects, sampled vocals and smokey Reggae inspired vibes.",
60
+ # "favorite": false,
61
+ # "forum_id": null,
62
+ # "id": 348,
63
+ # "images": {
64
+ # "default": "//api.audioaddict.com/v1/assets/image/e4b346b193c1adec01f8489b98a2bf3f.png{?size,height,width,quality}"
65
+ # },
66
+ # "key": "dub",
67
+ # "name": "Dub",
68
+ # "network_id": 1,
69
+ # "old_id": 729,
70
+ # "premium_id": null,
71
+ # "similar_channels": [
72
+ # {
73
+ # "id": 666,
74
+ # "similar_channel_id": 91
75
+ # },
76
+ # {
77
+ # "id": 667,
78
+ # "similar_channel_id": 13
79
+ # },
80
+ # {
81
+ # "id": 668,
82
+ # "similar_channel_id": 15
83
+ # }
84
+ # ],
85
+ # "tracklist_server_id": 25235,
86
+ # "tunein_url": "http://www.di.fm/dub",
87
+ # "updated_at": "2015-02-17T13:04:14-05:00"
88
+ # },
3
89
  module Somadic
4
90
  module Channel
5
91
  class DI < Somadic::BaseChannel
@@ -59,6 +145,7 @@ module Somadic
59
145
  sleep one_minute_from_now - Time.now < 15 ? 2 : 5
60
146
  songs = aa.refresh_playlist
61
147
  end
148
+
62
149
  songs
63
150
  end
64
151
 
@@ -71,7 +158,9 @@ module Somadic
71
158
  page = open('http://www.di.fm').read
72
159
  app_start = page.scan(/di\.app\.start\((.*?)\);/).flatten[0]
73
160
  json = JSON.parse(app_start)
74
- json['channels'].each { |c| channels << {id: c['id'], name: c['key']} }
161
+ json['channels'].each do |c|
162
+ channels << {id: c['id'], name: c['key'], display_name: c['name']}
163
+ end
75
164
 
76
165
  channels
77
166
  end
@@ -1,3 +1,4 @@
1
+ # A wrapper around a soma.fm channel.
1
2
  module Somadic
2
3
  module Channel
3
4
  class Soma < Somadic::BaseChannel
@@ -10,14 +11,14 @@ module Somadic
10
11
  # Overrides BaseChannel
11
12
  def find_channel(name)
12
13
  Somadic::Logger.debug("Soma#find_channel(#{name})")
13
- { id: 0, name: name }
14
+ { id: 0, name: name, display_name: name }
14
15
  end
15
16
 
16
17
  # Observer callback.
17
18
  def update(time, song)
18
19
  @song = song if song
19
20
  songs = refresh_playlist
20
- channel = { id: 0, name: @options[:channel] }
21
+ channel = { id: 0, name: @options[:channel], display_name: @options[:channel] }
21
22
  @listeners.each do |l|
22
23
  l.update(channel, songs) if l.respond_to?(:update)
23
24
  end
@@ -30,16 +31,17 @@ module Somadic
30
31
  APICache.get('soma_fm_chanel_list', cache: ONE_DAY, timeout: API_TIMEOUT) do
31
32
  Somadic::Logger.debug('Soma#load_channels')
32
33
  channels = []
33
- f = open('http://somafm.com/listen')
34
- page = f.read
35
- chans = page.scan(/\/play\/(.*?)"/).flatten
34
+ page = open('http://somafm.com/listen').read
35
+ chans = page.scan(/href="http:\/\/somafm.com\/(.*?)\.pls/).flatten
36
36
  chans.each do |c|
37
37
  unless c.start_with?('fw/') || c.gsub(/\d+$/, '') != c
38
- channels << {id: 0, name: c}
38
+ channels << {id: 0, name: c, display_name: c}
39
39
  end
40
40
  end
41
41
  channels.sort_by! {|k, _| k[:name]}
42
42
  channels.uniq! {|k, _| k[:name]}
43
+
44
+ channels
43
45
  end
44
46
  end
45
47
 
@@ -62,10 +64,11 @@ module Somadic
62
64
  next if song[3].scan(/<a.*?>(.*?)<\/a>/).empty?
63
65
 
64
66
  d = {}
65
- song[0] = song[0][0..song[0].index('&')-1]if song[0]['&'] # clean hh:mm:ss&nbsp; (Now)
67
+ song[0] = song[0][0..song[0].index('&')-1] if song[0]['&'] # clean hh:mm:ss&nbsp; (Now)
66
68
 
69
+ # TODO: ugh
67
70
  pt = Time.parse(song[0])
68
- local = Chronic.parse(pt.to_s.gsub(/-\d+$/, '-0700'))
71
+ local = Chronic.parse(pt.to_s.gsub(/-\d+$/, '-0800'))
69
72
  d[:started] = local.to_i
70
73
 
71
74
  d[:votes] = {up: 0, down: 0}
@@ -1,3 +1,3 @@
1
1
  module Somadic
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
data/somadic.gemspec CHANGED
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
14
14
  spec.license = 'MIT'
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0")
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.executables = spec.files.grep(%r{^bin/curses/somadic}) { |f| File.basename(f) }
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ['lib']
20
20
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: somadic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shane Thomas
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-02-28 00:00:00.000000000 Z
11
+ date: 2015-03-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mono_logger
@@ -165,6 +165,10 @@ files:
165
165
  - LICENSE.txt
166
166
  - README.md
167
167
  - Rakefile
168
+ - bin/curses/lib/display.rb
169
+ - bin/curses/lib/os.rb
170
+ - bin/curses/lib/progress_bar_patch.rb
171
+ - bin/curses/somadic
168
172
  - bin/somadic
169
173
  - lib/somadic.rb
170
174
  - lib/somadic/audio_addict.rb