somadic 0.0.2 → 0.0.3

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