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.
@@ -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
@@ -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
+
@@ -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
@@ -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
@@ -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
@@ -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: []