cloudruby 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []