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 +4 -4
- data/README.md +3 -2
- data/bin/curses/lib/display.rb +259 -0
- data/bin/curses/lib/os.rb +17 -0
- data/bin/curses/lib/progress_bar_patch.rb +11 -0
- data/bin/curses/somadic +180 -0
- data/bin/somadic +6 -250
- data/lib/somadic/audio_addict.rb +0 -2
- data/lib/somadic/base_channel.rb +4 -0
- data/lib/somadic/channel/di.rb +92 -3
- data/lib/somadic/channel/soma.rb +11 -8
- data/lib/somadic/version.rb +1 -1
- data/somadic.gemspec +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e781f2967b238c8b0babce28ff7688a48bf01ce9
|
4
|
+
data.tar.gz: b35d981810b5b6f87dabaebf1155892d239935dd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
data/bin/curses/somadic
ADDED
@@ -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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
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(':')
|
data/lib/somadic/audio_addict.rb
CHANGED
@@ -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] }
|
data/lib/somadic/base_channel.rb
CHANGED
data/lib/somadic/channel/di.rb
CHANGED
@@ -1,5 +1,91 @@
|
|
1
|
-
|
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
|
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
|
data/lib/somadic/channel/soma.rb
CHANGED
@@ -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
|
-
|
34
|
-
|
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 (Now)
|
67
|
+
song[0] = song[0][0..song[0].index('&')-1] if song[0]['&'] # clean hh:mm:ss (Now)
|
66
68
|
|
69
|
+
# TODO: ugh
|
67
70
|
pt = Time.parse(song[0])
|
68
|
-
local = Chronic.parse(pt.to_s.gsub(/-\d+$/, '-
|
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}
|
data/lib/somadic/version.rb
CHANGED
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.
|
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-
|
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
|