cloudruby 1.0.4
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 +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: []
|