cloudruby 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/cloudruby +34 -0
- data/lib/cloudruby.rb +101 -0
- data/lib/mpg123player.rb +171 -0
- data/lib/ncurses_ui.rb +485 -0
- data/lib/soundcloud.rb +141 -0
- metadata +83 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c190dbded5261f41bb498514cc555dc242ce2ac6
|
4
|
+
data.tar.gz: e4e64db8837ca1db44c143b92064b4e38fb845b6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 016cb876daaa4694fea7289d10779e86d4ebe1efb0c35338aa4b9bd0f35bc5e687c23fe3968bed5c04e25eb2fd5396ab26059d995c16d3b2d883011e3148d58c
|
7
|
+
data.tar.gz: a53de0174fbfb83dd03971fff46e2e17c8cd185b230bed7085faf7eadcf63e8e1b86e4299d68e47abfc688a172d10200f50617988846baf5024705ca8db33b6c
|
data/bin/cloudruby
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'json/pure'
|
4
|
+
require 'cloudruby'
|
5
|
+
|
6
|
+
@config = {}
|
7
|
+
@query = []
|
8
|
+
cfile = File.join Dir.home, ".#{File.basename($0)}.json"
|
9
|
+
|
10
|
+
ARGV.each do |a|
|
11
|
+
if a[0, 2] == "--"
|
12
|
+
a = a[2..-1]
|
13
|
+
key, value = a.split("=", 2)
|
14
|
+
@config[key.to_sym] = value || true
|
15
|
+
else
|
16
|
+
@query << a
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
if @config[:noconfig].nil? && File.readable?(cfile)
|
21
|
+
File.open cfile, 'r' do |file|
|
22
|
+
jsonconf = JSON.load file, nil, :symbolize_names => true
|
23
|
+
@config = jsonconf.merge(@config)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
@config[:download_dir] ||= Dir.pwd
|
28
|
+
|
29
|
+
@query = @query.join " "
|
30
|
+
|
31
|
+
c = Cloudruby.new
|
32
|
+
c.init @query, @config
|
33
|
+
c.run
|
34
|
+
|
data/lib/cloudruby.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'observer'
|
3
|
+
require 'logger'
|
4
|
+
require 'json/pure'
|
5
|
+
require_relative 'soundcloud.rb'
|
6
|
+
require_relative 'mpg123player.rb'
|
7
|
+
require_relative 'ncurses_ui.rb'
|
8
|
+
|
9
|
+
class Cloudruby
|
10
|
+
def init(q, config)
|
11
|
+
@config = config
|
12
|
+
@cloud = SoundCloud.new "76796f79392f9398288cdac3fe3391c0"
|
13
|
+
@player = MPG123Player.new
|
14
|
+
@ui = NCursesUI.new self, (@config[:ncurses] || @config[:curses])
|
15
|
+
|
16
|
+
# @logger = Logger.new "logfile.log"
|
17
|
+
@logger = Logger.new STDERR
|
18
|
+
# @logger.level = Logger::DEBUG
|
19
|
+
@logger.level = Logger::Severity::UNKNOWN
|
20
|
+
|
21
|
+
@player.logger = @logger
|
22
|
+
@ui.logger = @logger
|
23
|
+
@logger.info {"logger inited"}
|
24
|
+
|
25
|
+
@player.add_observer @ui, :player_update
|
26
|
+
@player.add_observer self
|
27
|
+
@cloud.add_observer @ui, :cloud_update
|
28
|
+
@logger.info {"observer assigned"}
|
29
|
+
|
30
|
+
@cloud.load_playlist q
|
31
|
+
@logger.info {"loaded playlist"}
|
32
|
+
@cloud.shufflePlaylist unless @config[:"no-shuffle"]
|
33
|
+
@logger.info {"playlist shuffled"}
|
34
|
+
nextTrack
|
35
|
+
|
36
|
+
trap("INT") do
|
37
|
+
self.quit
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def nextTrack
|
42
|
+
track = @cloud.nextTrack
|
43
|
+
unless track
|
44
|
+
puts "Nothing found"
|
45
|
+
quit
|
46
|
+
else
|
47
|
+
@player.play track
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def prevTrack
|
52
|
+
track = @cloud.prevTrack
|
53
|
+
unless track
|
54
|
+
puts "Nothing found"
|
55
|
+
quit
|
56
|
+
else
|
57
|
+
@player.play track
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def pause
|
62
|
+
@player.pause
|
63
|
+
end
|
64
|
+
|
65
|
+
def volumeUp
|
66
|
+
@player.volume = 5
|
67
|
+
end
|
68
|
+
|
69
|
+
def volumeDown
|
70
|
+
@player.volume = -5
|
71
|
+
end
|
72
|
+
|
73
|
+
def toggleMute
|
74
|
+
@player.mute
|
75
|
+
end
|
76
|
+
|
77
|
+
def run
|
78
|
+
@ui.run
|
79
|
+
end
|
80
|
+
|
81
|
+
# jump to next track, if current track finishes
|
82
|
+
def update(arg)
|
83
|
+
state = arg[:state]
|
84
|
+
case state
|
85
|
+
when :stop
|
86
|
+
nextTrack
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def download
|
91
|
+
@cloud.download @config[:download_dir]
|
92
|
+
end
|
93
|
+
|
94
|
+
# quit app and free all resources
|
95
|
+
def quit
|
96
|
+
@ui.close
|
97
|
+
@player.close
|
98
|
+
@logger.close
|
99
|
+
exit
|
100
|
+
end
|
101
|
+
end
|
data/lib/mpg123player.rb
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
class MPG123Player
|
4
|
+
include Observable
|
5
|
+
attr_reader :error, :paused
|
6
|
+
attr_accessor :logger
|
7
|
+
|
8
|
+
def initialize()
|
9
|
+
@volume = 100
|
10
|
+
@muted = false
|
11
|
+
@inqueue = []
|
12
|
+
begin
|
13
|
+
@pin, @pout, @perr = Open3.popen3 "mpg123 --keep-open --remote"
|
14
|
+
Thread.new do mpg123read end
|
15
|
+
Thread.new do mpg123send end
|
16
|
+
changed
|
17
|
+
notify_observers :state => :inited
|
18
|
+
rescue => err
|
19
|
+
@error = err
|
20
|
+
changed
|
21
|
+
notify_observers :state => :error, :error => err
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def play(track)
|
26
|
+
@last_track = track
|
27
|
+
unless track.nil? || track.is_a?(Hash) && track[:error]
|
28
|
+
mpg123puts "load #{track["mpg123url"]}"
|
29
|
+
@pstate = 2
|
30
|
+
end
|
31
|
+
changed
|
32
|
+
notify_observers :state => :load, :track => track
|
33
|
+
rescue => err
|
34
|
+
@error = err
|
35
|
+
changed
|
36
|
+
notify_observers :state => :error, :error => err
|
37
|
+
end
|
38
|
+
|
39
|
+
def playing?
|
40
|
+
@pstate == 2 && !@paused
|
41
|
+
end
|
42
|
+
|
43
|
+
def pause()
|
44
|
+
@paused = !@paused
|
45
|
+
mpg123puts "pause"
|
46
|
+
rescue => err
|
47
|
+
@error = err
|
48
|
+
changed
|
49
|
+
notify_observers :state => :error, :error => err
|
50
|
+
end
|
51
|
+
|
52
|
+
def stop()
|
53
|
+
mpg123puts "stop"
|
54
|
+
rescue => err
|
55
|
+
@error = err
|
56
|
+
changed
|
57
|
+
notify_observers :state => :error, :error => err
|
58
|
+
end
|
59
|
+
|
60
|
+
def volume= (val)
|
61
|
+
@volume += val
|
62
|
+
@volume = [@volume, 100].min
|
63
|
+
@volume = [@volume, 0].max
|
64
|
+
mpg123puts "V #{@volume}"
|
65
|
+
rescue => err
|
66
|
+
@error = err
|
67
|
+
changed
|
68
|
+
notify_observers :state => :error, :error => err
|
69
|
+
end
|
70
|
+
|
71
|
+
def volume()
|
72
|
+
@volume
|
73
|
+
end
|
74
|
+
|
75
|
+
def mute
|
76
|
+
@muted = !@muted
|
77
|
+
mpg123puts "V 0" if @muted
|
78
|
+
mpg123puts "V #{@volume}" unless @muted
|
79
|
+
rescue => err
|
80
|
+
@error = err
|
81
|
+
changed
|
82
|
+
notify_observers :state => :error, :error => err
|
83
|
+
end
|
84
|
+
|
85
|
+
def muted?
|
86
|
+
@muted
|
87
|
+
end
|
88
|
+
|
89
|
+
def close()
|
90
|
+
@perr.close
|
91
|
+
@pout.close
|
92
|
+
@pin.close
|
93
|
+
changed
|
94
|
+
notify_observers :state => :closed
|
95
|
+
rescue => err
|
96
|
+
@error = err
|
97
|
+
changed
|
98
|
+
notify_observers :state => :error, :error => err
|
99
|
+
end
|
100
|
+
|
101
|
+
def mpg123puts(out)
|
102
|
+
@inqueue << out
|
103
|
+
@logger.debug {">> #{out}"} #
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def mpg123send
|
109
|
+
while not @pin.closed?
|
110
|
+
begin
|
111
|
+
last_cmd = nil
|
112
|
+
@logger.debug "queue size #{@inqueue.size}"
|
113
|
+
@inqueue.select! do |cmd|
|
114
|
+
if cmd[0,4] == "load"
|
115
|
+
last_cmd = cmd
|
116
|
+
false
|
117
|
+
else
|
118
|
+
true
|
119
|
+
end
|
120
|
+
end
|
121
|
+
@inqueue << last_cmd unless last_cmd.nil?
|
122
|
+
while out = @inqueue.shift
|
123
|
+
@pin.puts out
|
124
|
+
end
|
125
|
+
select(nil, nil, nil, 0.4)
|
126
|
+
rescue
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def mpg123read
|
132
|
+
while not @pout.closed?
|
133
|
+
begin
|
134
|
+
io = IO.select([@pout, @perr])
|
135
|
+
io = io.first.first
|
136
|
+
response = io.read_nonblock 1024
|
137
|
+
lines = response.split "\n"
|
138
|
+
lines.each do |line|
|
139
|
+
@logger.debug {"<< #{line}"} #
|
140
|
+
if line =~ /@F\s(\S*)\s(\S*)\s(\S*)\s(\S*)\s*/
|
141
|
+
changed
|
142
|
+
notify_observers :state => :info,
|
143
|
+
:frame => $1.to_i,
|
144
|
+
:frameleft => $2.to_i,
|
145
|
+
:time => $3,
|
146
|
+
:timeleft => $4
|
147
|
+
elsif line =~ /@P\s(\S*)\s*/
|
148
|
+
@pstate = $1.to_i
|
149
|
+
changed
|
150
|
+
if @paused
|
151
|
+
if @pstate == 2
|
152
|
+
notify_observers :state => :resume
|
153
|
+
@paused = false
|
154
|
+
elsif @pstate == 1
|
155
|
+
notify_observers :state => :pause
|
156
|
+
end
|
157
|
+
else
|
158
|
+
notify_observers :state => :play if @pstate == 2
|
159
|
+
notify_observers :state => :stop if @pstate == 1
|
160
|
+
end
|
161
|
+
elsif line =~ /@E.*Unfinished command:.*/
|
162
|
+
play @last_track
|
163
|
+
else
|
164
|
+
#puts "don't know #{line}"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
rescue
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
data/lib/ncurses_ui.rb
ADDED
@@ -0,0 +1,485 @@
|
|
1
|
+
require 'curses'
|
2
|
+
|
3
|
+
class NCursesUI
|
4
|
+
attr_accessor :logger
|
5
|
+
|
6
|
+
def initialize cloud, options = {}
|
7
|
+
defaults = {
|
8
|
+
:colors => {
|
9
|
+
:default => [:cyan, :blue],
|
10
|
+
:playlist => [:cyan, :blue],
|
11
|
+
:playlist_active => [:white, :blue],
|
12
|
+
:progress => [:cyan, :blue],
|
13
|
+
:progress_bar => [:blue, :cyan],
|
14
|
+
:title => [:cyan, :black],
|
15
|
+
:artist => [:cyan, :black]
|
16
|
+
}
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
@options = defaults.merge(options || {})
|
20
|
+
@cloud = cloud
|
21
|
+
@state = :running
|
22
|
+
@frac = 0
|
23
|
+
@title = "None"
|
24
|
+
@op = " "
|
25
|
+
@time = 0
|
26
|
+
@timeleft = 0
|
27
|
+
@playlist = []
|
28
|
+
end
|
29
|
+
|
30
|
+
def run
|
31
|
+
begin
|
32
|
+
stdscr = Curses.init_screen
|
33
|
+
Curses.start_color
|
34
|
+
Colors.init @options[:colors]
|
35
|
+
stdscr.keypad true
|
36
|
+
Curses.nonl
|
37
|
+
Curses.cbreak
|
38
|
+
Curses.noecho
|
39
|
+
Curses.curs_set 0
|
40
|
+
Curses.timeout = 5
|
41
|
+
@p = NProgress.new stdscr, 0, 0, :progress, :progress_bar
|
42
|
+
@l = NPlaylist.new stdscr, 4, 0, :playlist, :playlist_active, 0, 0, @playlist
|
43
|
+
@i = NInfobox.new stdscr, 4, 0, :playlist, 0, 8
|
44
|
+
@d = NDownloadBox.new stdscr, Curses.lines-1, 0, :default, 0, 1
|
45
|
+
@l.active = 0
|
46
|
+
last_ch = nil
|
47
|
+
while(@state != :close)
|
48
|
+
ch = Curses.getch
|
49
|
+
last_ch = ch if ch
|
50
|
+
Curses.setpos 3, 0
|
51
|
+
Curses.clrtoeol
|
52
|
+
# Nutils.print stdscr, 3, 0, "Test %s" % [last_ch], :red
|
53
|
+
case ch
|
54
|
+
when Curses::KEY_RESIZE
|
55
|
+
@p.resize
|
56
|
+
@l.resize
|
57
|
+
@i.resize
|
58
|
+
@d.resize
|
59
|
+
Curses.refresh
|
60
|
+
when 110, 78, 'n', 'N', Curses::KEY_DOWN
|
61
|
+
@cloud.nextTrack
|
62
|
+
when 112, 80, 'p', 'P', Curses::KEY_UP
|
63
|
+
@cloud.prevTrack
|
64
|
+
when 113, 81, 'q', 'Q', 27, Curses::KEY_EXIT
|
65
|
+
@cloud.quit
|
66
|
+
when 61, 43, '=', '+'
|
67
|
+
@cloud.volumeUp
|
68
|
+
when 45, 95, '-', '_'
|
69
|
+
@cloud.volumeDown
|
70
|
+
when 109, 77, 'm', 'M'
|
71
|
+
@cloud.toggleMute
|
72
|
+
when 68, 100, 'd', 'D'
|
73
|
+
@cloud.download
|
74
|
+
when 118, 86, 'v', 'V'
|
75
|
+
@i.visible = !@i.visible
|
76
|
+
@l.dirty = true
|
77
|
+
when 32, ' '
|
78
|
+
@cloud.pause
|
79
|
+
end
|
80
|
+
|
81
|
+
if @error
|
82
|
+
Nutils.print stdscr, 3, 0, "Error: #{@error}", :red
|
83
|
+
Curses.refresh
|
84
|
+
end
|
85
|
+
tr = " %s " % [Nutils.timestr(@timetotal)]
|
86
|
+
t = " %-#{Curses.cols-tr.size-1}s%s" % [Nutils.timestr(@time), tr]
|
87
|
+
@p.value = @frac
|
88
|
+
@p.text = t
|
89
|
+
@p.refresh
|
90
|
+
Nutils.print stdscr, 1, 0, "#{@op} #{@title}", :title
|
91
|
+
Nutils.print stdscr, 2, 0, " by #{@username}", :artist
|
92
|
+
@l.refresh
|
93
|
+
@i.refresh
|
94
|
+
@d.refresh
|
95
|
+
stdscr.refresh
|
96
|
+
end
|
97
|
+
rescue => ex
|
98
|
+
ensure
|
99
|
+
@l.close if @l
|
100
|
+
@p.close if @p
|
101
|
+
stdscr.close
|
102
|
+
Curses.echo
|
103
|
+
Curses.nocbreak
|
104
|
+
Curses.nl
|
105
|
+
Curses.close_screen
|
106
|
+
puts ex.inspect if ex
|
107
|
+
puts ex.backtrace if ex
|
108
|
+
# Colors.debug
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def cloud_update(arg)
|
113
|
+
case arg[:state]
|
114
|
+
when :load
|
115
|
+
@playlist |= arg[:tracks]
|
116
|
+
@l.list = @playlist if @l
|
117
|
+
when :shuffle
|
118
|
+
@playlist = arg[:tracks]
|
119
|
+
@l.list = @playlist if @l
|
120
|
+
when :next, :previous
|
121
|
+
pos = arg[:position]
|
122
|
+
@l.active = pos if @l
|
123
|
+
when :download
|
124
|
+
if arg[:error]
|
125
|
+
@error = arg[:error]
|
126
|
+
end
|
127
|
+
if arg[:count]
|
128
|
+
count = arg[:count]
|
129
|
+
if(count > 0)
|
130
|
+
@l.height = -1
|
131
|
+
@d.visible = true
|
132
|
+
@d.count = count
|
133
|
+
@d.title = arg[:name] if arg[:name]
|
134
|
+
else
|
135
|
+
@l.height = 0
|
136
|
+
@d.visible = false
|
137
|
+
@d.title = ""
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def player_update(arg)
|
144
|
+
|
145
|
+
case arg[:state]
|
146
|
+
when :load
|
147
|
+
track = arg[:track]
|
148
|
+
if track.nil?
|
149
|
+
@error = "Nothing found!"
|
150
|
+
else
|
151
|
+
@error = nil
|
152
|
+
@title = track["title"]
|
153
|
+
@username = track["user"]["username"]
|
154
|
+
@timetotal = track["duration"]
|
155
|
+
@error = track[:error] if track[:error]
|
156
|
+
end
|
157
|
+
when :info
|
158
|
+
frame = arg[:frame].to_f
|
159
|
+
frames = frame + arg[:frameleft]
|
160
|
+
@frac = frame/frames
|
161
|
+
@time = arg[:time].to_i
|
162
|
+
when :pause
|
163
|
+
@op = "\u2161"
|
164
|
+
when :resume, :play
|
165
|
+
@op = "\u25B6"
|
166
|
+
when :stop
|
167
|
+
@op = "\u25FC"
|
168
|
+
when :error
|
169
|
+
@error = arg[:error]
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def close
|
174
|
+
@state = :close
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
class Nutils
|
179
|
+
def self.print(scr, row, col, text, color, width = (Curses.cols))
|
180
|
+
width = [Curses.cols, col+width].min - col
|
181
|
+
t = "%-#{width}s" % [scroll(text, width)]
|
182
|
+
scr.attron(Colors.map(color)) if color
|
183
|
+
scr.setpos row, col
|
184
|
+
scr.addstr t
|
185
|
+
scr.attroff(Colors.map(color)) if color
|
186
|
+
end
|
187
|
+
|
188
|
+
def self.scroll(text, width, offset=0)
|
189
|
+
return unless text
|
190
|
+
ellipsis = "*"
|
191
|
+
t = text
|
192
|
+
if t.size+offset > width
|
193
|
+
t = t[offset..(width-ellipsis.size-1)] << ellipsis
|
194
|
+
end
|
195
|
+
t
|
196
|
+
end
|
197
|
+
|
198
|
+
def self.timestr(sec)
|
199
|
+
sec = sec.to_i
|
200
|
+
"%02d:%02d" % [sec/60, sec%60]
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
|
205
|
+
class Colors
|
206
|
+
$map = {}
|
207
|
+
$counter = 0
|
208
|
+
def self.init colormap = {}
|
209
|
+
colormap.each do |key, colors|
|
210
|
+
self.add(key, colors[0], colors[1])
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def self.map(key)
|
215
|
+
$map[key] || $map[:default]
|
216
|
+
end
|
217
|
+
|
218
|
+
def self.add(key, fg, bg)
|
219
|
+
Curses.init_pair $counter, ncg(fg), ncg(bg)
|
220
|
+
$map[key] = Curses.color_pair $counter
|
221
|
+
$counter += 1
|
222
|
+
end
|
223
|
+
|
224
|
+
def self.debug
|
225
|
+
puts "colors supported: #{Curses.colors}"
|
226
|
+
puts "map: #{$map}"
|
227
|
+
end
|
228
|
+
# get ncurses color constant
|
229
|
+
def self.ncg(color)
|
230
|
+
color = :black unless color
|
231
|
+
Curses.const_get "COLOR_#{color.upcase}"
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
class NProgress
|
236
|
+
attr_reader :value
|
237
|
+
attr_accessor :text
|
238
|
+
def initialize scr, row, col, color, bar_color, width=0, value = 0, text = ""
|
239
|
+
@width = width
|
240
|
+
@color = color
|
241
|
+
@bar_color = bar_color
|
242
|
+
@row = row
|
243
|
+
@col = col
|
244
|
+
@winfg = Curses::Window.new 1, 1, @row, @col
|
245
|
+
@winbg = Curses::Window.new 1, self.width, @row, @col
|
246
|
+
@value = value
|
247
|
+
@text = text
|
248
|
+
refresh
|
249
|
+
end
|
250
|
+
|
251
|
+
def width
|
252
|
+
[@col + @width, Curses.cols].min - @col
|
253
|
+
end
|
254
|
+
|
255
|
+
def value=(val)
|
256
|
+
@value = val
|
257
|
+
@winfg.resize(1, fgw) if fgw > 0
|
258
|
+
end
|
259
|
+
|
260
|
+
def refresh
|
261
|
+
offset = fgw
|
262
|
+
Nutils.print @winbg, 0, offset, @text[offset..-1], @color
|
263
|
+
Nutils.print @winfg, 0, 0, @text, @bar_color if fgw > 0
|
264
|
+
@winbg.refresh
|
265
|
+
@winfg.refresh if fgw > 0
|
266
|
+
end
|
267
|
+
|
268
|
+
def resize
|
269
|
+
end
|
270
|
+
|
271
|
+
def close
|
272
|
+
@winbg.close
|
273
|
+
@winfg.close
|
274
|
+
end
|
275
|
+
|
276
|
+
private
|
277
|
+
def fgw
|
278
|
+
w = width() == 0 ? Curses.cols - @col : width()
|
279
|
+
(w * @value).floor
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
class NPlaylist
|
284
|
+
attr_writer :list
|
285
|
+
attr_accessor :dirty
|
286
|
+
def initialize scr, row, col, color, active_color, w, h, l
|
287
|
+
@list = l
|
288
|
+
@row = row
|
289
|
+
@col = col
|
290
|
+
@width = w
|
291
|
+
@height = h
|
292
|
+
@color = color
|
293
|
+
@active_color = active_color
|
294
|
+
@apos = -1
|
295
|
+
@win = Curses::Window.new height, width, @row, @col
|
296
|
+
@dirty = true
|
297
|
+
refresh
|
298
|
+
end
|
299
|
+
|
300
|
+
def width
|
301
|
+
w = [@col + @width, Curses.cols].min - @col
|
302
|
+
if w <= 0
|
303
|
+
Curses.cols - @col + w
|
304
|
+
else
|
305
|
+
w
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
def height
|
310
|
+
h = [@row + @height, Curses.lines].min - @row
|
311
|
+
if h <= 0
|
312
|
+
Curses.lines - @row + h
|
313
|
+
else
|
314
|
+
h
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def width=(val)
|
319
|
+
@width = val
|
320
|
+
resize
|
321
|
+
refresh
|
322
|
+
end
|
323
|
+
|
324
|
+
def height=(val)
|
325
|
+
@height = val
|
326
|
+
resize
|
327
|
+
refresh
|
328
|
+
end
|
329
|
+
|
330
|
+
def active=(pos)
|
331
|
+
@apos = pos
|
332
|
+
@dirty = true
|
333
|
+
end
|
334
|
+
|
335
|
+
def resize
|
336
|
+
@win.resize height, width
|
337
|
+
@dirty = true
|
338
|
+
end
|
339
|
+
|
340
|
+
def refresh
|
341
|
+
return unless @dirty
|
342
|
+
if !@list.is_a?(Array) || @list.empty?
|
343
|
+
Nutils.print @win, 1, 2, "Empty playlist", @color, width - 3
|
344
|
+
else
|
345
|
+
r = 1
|
346
|
+
size = height - 2
|
347
|
+
offset = ([[size/2.0, @apos].max, [@list.size, size].max-(size/2.0)].min - size/2.0).ceil
|
348
|
+
|
349
|
+
@list[offset..@list.size].each do |t|
|
350
|
+
tl = t["title"]
|
351
|
+
if @apos == r - 1 + offset
|
352
|
+
tl = ">#{tl}"
|
353
|
+
color = @active_color
|
354
|
+
else
|
355
|
+
tl = " #{tl}"
|
356
|
+
color = @color
|
357
|
+
end
|
358
|
+
tr = "[%6s]" % Nutils.timestr(t["duration"])
|
359
|
+
tr = "[D]#{tr}" if t["downloadable"]
|
360
|
+
wr = tr.size
|
361
|
+
wl = width - 3- wr
|
362
|
+
Nutils.print @win, r, 1, tl, color, wl+1
|
363
|
+
Nutils.print @win, r, 2+wl, tr, color, wr
|
364
|
+
r += 1
|
365
|
+
if(r >= height - 1)
|
366
|
+
# print arrow down
|
367
|
+
break
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
@win.attron(Colors.map(@color)) if @color
|
372
|
+
@win.box 0, 0
|
373
|
+
@win.attroff(Colors.map(@color)) if @color
|
374
|
+
@win.refresh
|
375
|
+
@dirty = false
|
376
|
+
end
|
377
|
+
|
378
|
+
def close
|
379
|
+
@win.close
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
class NInfobox
|
384
|
+
attr_accessor :visible
|
385
|
+
def initialize scr, row, col, color, w, h
|
386
|
+
@scr = scr
|
387
|
+
@row = row
|
388
|
+
@col = col
|
389
|
+
@color = color
|
390
|
+
@width = w
|
391
|
+
@height = h
|
392
|
+
@win = Curses::Window.new height, width, @row, @col
|
393
|
+
@visible = false
|
394
|
+
refresh
|
395
|
+
end
|
396
|
+
|
397
|
+
def width
|
398
|
+
w = [@col + @width, Curses.cols].min - @col
|
399
|
+
if w == 0
|
400
|
+
Curses.cols - @col
|
401
|
+
else
|
402
|
+
w
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
def height
|
407
|
+
h = [@row + @height, Curses.lines].min - @row
|
408
|
+
if h == 0
|
409
|
+
Curses.lines - @row
|
410
|
+
else
|
411
|
+
h
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
def resize
|
416
|
+
@win.resize height, width
|
417
|
+
end
|
418
|
+
|
419
|
+
def refresh
|
420
|
+
return unless @visible
|
421
|
+
Nutils.print @win, 1, 2, "Cloudruby v1", :default
|
422
|
+
Nutils.print @win, 2, 4, "Curses version: #{(Curses.const_defined?"VERSION")?Curses::VERSION : "N/A"}", :default
|
423
|
+
Nutils.print @win, 3, 4, "Ruby version: #{RUBY_VERSION}", :default
|
424
|
+
Nutils.print @win, 4, 4, "Author: kulpae <my.shando@gmail.com>", :artist
|
425
|
+
Nutils.print @win, 5, 4, "Website: uraniumlane.net", :title
|
426
|
+
Nutils.print @win, 6, 4, "License: MIT", :default
|
427
|
+
@win.attron(Colors.map(@color)) if @color
|
428
|
+
@win.box 0, 0
|
429
|
+
@win.attroff(Colors.map(@color)) if @color
|
430
|
+
@win.refresh
|
431
|
+
end
|
432
|
+
|
433
|
+
def close
|
434
|
+
@win.close
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
class NDownloadBox
|
439
|
+
attr_accessor :visible, :count, :title
|
440
|
+
def initialize scr, row, col, color, w, h
|
441
|
+
@scr = scr
|
442
|
+
@row = row
|
443
|
+
@col = col
|
444
|
+
@color = color
|
445
|
+
@width = w
|
446
|
+
@height = h
|
447
|
+
@win = Curses::Window.new height, width, @row, @col
|
448
|
+
@visible = false
|
449
|
+
@count = 0
|
450
|
+
@title = ""
|
451
|
+
refresh
|
452
|
+
end
|
453
|
+
|
454
|
+
def width
|
455
|
+
w = [@col + @width, Curses.cols].min - @col
|
456
|
+
if w == 0
|
457
|
+
Curses.cols - @col
|
458
|
+
else
|
459
|
+
w
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
def height
|
464
|
+
h = [@row + @height, Curses.lines].min - @row
|
465
|
+
if h == 0
|
466
|
+
Curses.lines - @row
|
467
|
+
else
|
468
|
+
h
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
def resize
|
473
|
+
@win.resize height, width
|
474
|
+
end
|
475
|
+
|
476
|
+
def refresh
|
477
|
+
return unless @visible
|
478
|
+
Nutils.print @win, 0, 0, "Downloading #{@count} track#{@count > 1?"s":""} | #{title}", :default
|
479
|
+
@win.refresh
|
480
|
+
end
|
481
|
+
|
482
|
+
def close
|
483
|
+
@win.close
|
484
|
+
end
|
485
|
+
end
|
data/lib/soundcloud.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'open-uri'
|
3
|
+
require 'json/pure'
|
4
|
+
|
5
|
+
class SoundCloud
|
6
|
+
include Observable
|
7
|
+
LIMIT = 100
|
8
|
+
|
9
|
+
def initialize(client_id)
|
10
|
+
@cid = client_id
|
11
|
+
@playlist_pos = -1
|
12
|
+
@download_queue = []
|
13
|
+
@dthread = Thread.new do downloader end
|
14
|
+
end
|
15
|
+
|
16
|
+
def load_playlist(search = nil, offset = 0)
|
17
|
+
search = "" unless search && !search.empty?
|
18
|
+
if search =~ /\s*http(s)?:\/\/(www.)?soundcloud.com.*/
|
19
|
+
url = "http://api.soundcloud.com/resolve.json?url=%s&client_id=%s" % [CGI.escape(search), @cid]
|
20
|
+
else
|
21
|
+
url = "http://api.soundcloud.com/tracks.json?client_id=%s&filter=streamable&limit=%d&offset=%d&q=%s" \
|
22
|
+
% [@cid, LIMIT, offset, CGI.escape(search)]
|
23
|
+
end
|
24
|
+
c = open(url) do |io|
|
25
|
+
io.readlines
|
26
|
+
end.join
|
27
|
+
@tracks = JSON.parse c
|
28
|
+
@tracks = [@tracks] if @tracks.is_a? Hash
|
29
|
+
@tracks.map! do |t|
|
30
|
+
t["mpg123url"] = client_url t['stream_url']
|
31
|
+
t["download"] = client_url t['download_url']
|
32
|
+
t["duration"] = t["duration"].nil? ? 0 : t["duration"].to_i/1000
|
33
|
+
t["bpm"] = t["bpm"].nil? ? 0 : t["bpm"].to_i
|
34
|
+
t[:error] = "Not streamable" if t["stream_url"].nil?
|
35
|
+
t
|
36
|
+
end
|
37
|
+
changed
|
38
|
+
notify_observers :state =>:load, :tracks => @tracks
|
39
|
+
rescue => e
|
40
|
+
@error = {:error => e}
|
41
|
+
end
|
42
|
+
|
43
|
+
def shufflePlaylist
|
44
|
+
return unless @tracks.respond_to? "shuffle!"
|
45
|
+
@tracks.shuffle!
|
46
|
+
changed
|
47
|
+
notify_observers :state => :shuffle, :tracks => @tracks
|
48
|
+
end
|
49
|
+
|
50
|
+
def nextTrack
|
51
|
+
return @error unless @tracks
|
52
|
+
return if @tracks.empty? || @tracks.nil?
|
53
|
+
if @tracks.is_a? Hash
|
54
|
+
t = @tracks
|
55
|
+
else
|
56
|
+
@playlist_pos += 1
|
57
|
+
@playlist_pos -= @tracks.size if @playlist_pos >= @tracks.size
|
58
|
+
t = @tracks[@playlist_pos]
|
59
|
+
changed
|
60
|
+
notify_observers :state => :next, :position => @playlist_pos
|
61
|
+
end
|
62
|
+
t
|
63
|
+
end
|
64
|
+
|
65
|
+
def prevTrack
|
66
|
+
return @error unless @tracks
|
67
|
+
return if @tracks.empty?
|
68
|
+
if @tracks.is_a? Hash
|
69
|
+
t = @tracks
|
70
|
+
else
|
71
|
+
@playlist_pos -= 1
|
72
|
+
@playlist_pos += @tracks.size if @playlist_pos < 0
|
73
|
+
t = @tracks[@playlist_pos]
|
74
|
+
changed
|
75
|
+
notify_observers :state => :previous, :position => @playlist_pos
|
76
|
+
end
|
77
|
+
t
|
78
|
+
end
|
79
|
+
|
80
|
+
def download(target_dir)
|
81
|
+
return @error unless @tracks
|
82
|
+
return if @tracks.empty?
|
83
|
+
if @tracks.is_a? Hash
|
84
|
+
t = @tracks
|
85
|
+
else
|
86
|
+
t = @tracks[@playlist_pos]
|
87
|
+
end
|
88
|
+
unless t["download"].nil? || t["download"].size == 0
|
89
|
+
filename = "#{t["permalink"]}.#{t["original_format"]}"
|
90
|
+
path = File.join(target_dir, filename)
|
91
|
+
pair = [path, t['download']]
|
92
|
+
@download_queue << pair unless @download_queue.include? pair
|
93
|
+
@dthread.run
|
94
|
+
|
95
|
+
changed
|
96
|
+
notify_observers :state => :download, :count => @download_queue.size
|
97
|
+
else
|
98
|
+
changed
|
99
|
+
notify_observers :state => :download, :error => "Not downloadable"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# download thread
|
104
|
+
def downloader
|
105
|
+
loop do
|
106
|
+
size = @download_queue.size
|
107
|
+
while d = @download_queue.shift
|
108
|
+
path = d[0]
|
109
|
+
uri = d[1]
|
110
|
+
changed
|
111
|
+
notify_observers :state => :download, :name => path, :count => size
|
112
|
+
size = @download_queue.size
|
113
|
+
begin
|
114
|
+
path = File.expand_path path
|
115
|
+
file = File.new(path, "wb")
|
116
|
+
File.open(path, "wb") do |file|
|
117
|
+
file.print open(uri).read
|
118
|
+
end
|
119
|
+
changed
|
120
|
+
notify_observers :state => :download, :name => path, :count => size
|
121
|
+
rescue OpenURI::HTTPError => e
|
122
|
+
changed
|
123
|
+
notify_observers :state => :error, :error => e
|
124
|
+
ensure
|
125
|
+
file.close
|
126
|
+
end
|
127
|
+
end
|
128
|
+
sleep 5
|
129
|
+
end
|
130
|
+
rescue => e
|
131
|
+
changed
|
132
|
+
notify_observers :state => "error", :error => e
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def client_url(url)
|
138
|
+
"#{url}?client_id=%s" % [@cid] if url
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cloudruby
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Paul Koch
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-03-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: curses
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: json_pure
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1'
|
41
|
+
description: "== A soundcloud player written in Ruby \nwith Ncurses for user interface
|
42
|
+
and \nmpg123 for playback.\n\nRequires the mpg123 executable to be present in the
|
43
|
+
PATH.\n\n== Usage\n\n* lists recently uploaded tracks from soundcloud\n cloudruby\n\n*
|
44
|
+
lists all tracks that matches the keyword (here 'wearecastor')\n cloudruby wearecastor\n\n*
|
45
|
+
also works with the direct soundcloud URL\n cloudruby http://soundcloud.com/crassmix/feint-clockwork-hearts-crass\n"
|
46
|
+
email: my.shando@gmail.com
|
47
|
+
executables:
|
48
|
+
- cloudruby
|
49
|
+
extensions: []
|
50
|
+
extra_rdoc_files: []
|
51
|
+
files:
|
52
|
+
- bin/cloudruby
|
53
|
+
- lib/cloudruby.rb
|
54
|
+
- lib/mpg123player.rb
|
55
|
+
- lib/ncurses_ui.rb
|
56
|
+
- lib/soundcloud.rb
|
57
|
+
homepage: https://github.com/kulpae/cloudruby
|
58
|
+
licenses:
|
59
|
+
- MIT
|
60
|
+
metadata:
|
61
|
+
issue_tracker: https://github.com/kulpae/cloudruby/issues
|
62
|
+
post_install_message:
|
63
|
+
rdoc_options: []
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: 1.9.2
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
requirements:
|
77
|
+
- mpg123 executable
|
78
|
+
rubyforge_project:
|
79
|
+
rubygems_version: 2.2.2
|
80
|
+
signing_key:
|
81
|
+
specification_version: 4
|
82
|
+
summary: Ncurses player for Soundcloud tracks in Ruby
|
83
|
+
test_files: []
|