cloudruby 1.0.4 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/cloudruby +1 -0
- data/lib/cloudruby.rb +10 -5
- data/lib/gstplayer.rb +209 -0
- data/lib/mpg123player.rb +21 -2
- data/lib/ncurses_ui.rb +51 -18
- data/lib/soundcloud.rb +5 -3
- metadata +19 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7d65284ff7faa2d2be8f6b1ef10fb0ed825557fb
|
4
|
+
data.tar.gz: 500dbcb28787f33896a1493abcff9bc96b49c1a1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8f9753df56f183867581be5cd7b601c525a23476684e040ffa593f6f57163d50bf1a6e31acf333712503fa22f4c0c3f86ebb883f182742f9531ee90ae8b9cce8
|
7
|
+
data.tar.gz: 973cde30b03d01ef0922fcf41fad6f76330971522eaf39f512c2d0dac5ab98cf1fc2161944057b147a47ea6c721430ca2e89efae37ce92f11d740fdf76b5ec8d
|
data/bin/cloudruby
CHANGED
data/lib/cloudruby.rb
CHANGED
@@ -4,22 +4,27 @@ require 'logger'
|
|
4
4
|
require 'json/pure'
|
5
5
|
require_relative 'soundcloud.rb'
|
6
6
|
require_relative 'mpg123player.rb'
|
7
|
+
require_relative 'gstplayer.rb'
|
7
8
|
require_relative 'ncurses_ui.rb'
|
8
9
|
|
9
10
|
class Cloudruby
|
10
11
|
def init(q, config)
|
11
12
|
@config = config
|
12
|
-
@cloud = SoundCloud.new "76796f79392f9398288cdac3fe3391c0"
|
13
|
-
@player = MPG123Player.new
|
14
|
-
@ui = NCursesUI.new self, (@config[:ncurses] || @config[:curses])
|
15
13
|
|
16
14
|
# @logger = Logger.new "logfile.log"
|
17
15
|
@logger = Logger.new STDERR
|
18
16
|
# @logger.level = Logger::DEBUG
|
19
17
|
@logger.level = Logger::Severity::UNKNOWN
|
20
18
|
|
21
|
-
@
|
22
|
-
@
|
19
|
+
@cloud = SoundCloud.new "76796f79392f9398288cdac3fe3391c0", logger: @logger
|
20
|
+
case @config[:"audio-backend"]
|
21
|
+
when "gstreamer"
|
22
|
+
@player = GstPlayer.new logger: @logger, audio_params: @config[:"audio-params"]
|
23
|
+
else
|
24
|
+
@player = MPG123Player.new logger: @logger
|
25
|
+
end
|
26
|
+
@ui = NCursesUI.new self, (@config[:ncurses] || @config[:curses]), logger: @logger, audio_backend: @player
|
27
|
+
|
23
28
|
@logger.info {"logger inited"}
|
24
29
|
|
25
30
|
@player.add_observer @ui, :player_update
|
data/lib/gstplayer.rb
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
require 'gst'
|
2
|
+
|
3
|
+
class GstPlayer
|
4
|
+
include Observable
|
5
|
+
attr_reader :error, :paused
|
6
|
+
attr_accessor :logger, :audio_params
|
7
|
+
|
8
|
+
def version
|
9
|
+
as = nil
|
10
|
+
unless @pipeline.nil?
|
11
|
+
as = @pipeline.get_property "audio-sink"
|
12
|
+
as = ", Sink: #{as.name}"
|
13
|
+
end
|
14
|
+
return "Audio backend: Gstreamer #{Gst.version.join('.')}#{as}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize params = {}
|
18
|
+
params.each { |key, value| send "#{key}=", value }
|
19
|
+
|
20
|
+
@logger.info version
|
21
|
+
@inqueue = []
|
22
|
+
begin
|
23
|
+
@pipeline = Gst::ElementFactory.make("playbin", "cloudruby_pipeline")
|
24
|
+
if @pipeline.nil?
|
25
|
+
puts "Cannot initialize playing pipeline. Is gstreamer installed?"
|
26
|
+
exit false
|
27
|
+
end
|
28
|
+
@pipeline.set_property "buffer-size", 16 * 1024 * 1024
|
29
|
+
if @audio_params
|
30
|
+
@audio_params.each do |key, value|
|
31
|
+
case key
|
32
|
+
when :"audio-sink"
|
33
|
+
sink = Gst::ElementFactory.make(value, "#{value}")
|
34
|
+
@pipeline.set_property "audio-sink", sink unless sink.nil?
|
35
|
+
when :"buffer-duration", :"buffer-size", :"mute", :"volume"
|
36
|
+
@pipeline.set_property key, value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
changed
|
42
|
+
notify_observers :state => :inited
|
43
|
+
runMainLoop
|
44
|
+
rescue => err
|
45
|
+
@error = err
|
46
|
+
changed
|
47
|
+
notify_observers :state => :error, :error => err
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def play(track)
|
52
|
+
@last_track = track
|
53
|
+
unless track.nil? || track.is_a?(Hash) && track[:error] && !Gst.valid_uri?(track["mpg123url"])
|
54
|
+
gstPlayUrl track["mpg123url"]
|
55
|
+
end
|
56
|
+
changed
|
57
|
+
notify_observers :state => :load, :track => track
|
58
|
+
rescue => err
|
59
|
+
@error = err
|
60
|
+
changed
|
61
|
+
notify_observers :state => :error, :error => err
|
62
|
+
end
|
63
|
+
|
64
|
+
def playing?
|
65
|
+
@pipeline.playing?
|
66
|
+
end
|
67
|
+
|
68
|
+
def pause()
|
69
|
+
@paused = !@paused
|
70
|
+
if @paused
|
71
|
+
@pipeline.pause
|
72
|
+
else
|
73
|
+
@pipeline.play
|
74
|
+
end
|
75
|
+
rescue => err
|
76
|
+
@error = err
|
77
|
+
changed
|
78
|
+
notify_observers :state => :error, :error => err
|
79
|
+
end
|
80
|
+
|
81
|
+
def stop()
|
82
|
+
@playbin.stop
|
83
|
+
rescue => err
|
84
|
+
@error = err
|
85
|
+
changed
|
86
|
+
notify_observers :state => :error, :error => err
|
87
|
+
end
|
88
|
+
|
89
|
+
def volume= (val)
|
90
|
+
@volume = volume
|
91
|
+
@volume += val
|
92
|
+
@volume = [@volume, 100].min
|
93
|
+
@volume = [@volume, 0].max
|
94
|
+
@pipeline.volume = @volume/100.0
|
95
|
+
changed
|
96
|
+
notify_observers :state => :status, :type => "Volume", :value => "#{@volume}%"
|
97
|
+
rescue => err
|
98
|
+
@error = err
|
99
|
+
changed
|
100
|
+
notify_observers :state => :error, :error => err
|
101
|
+
end
|
102
|
+
|
103
|
+
def volume
|
104
|
+
(@pipeline.get_property("volume") * 100).to_int
|
105
|
+
end
|
106
|
+
|
107
|
+
def mute
|
108
|
+
@pipeline.set_property "mute", !@pipeline.get_property("mute")
|
109
|
+
changed
|
110
|
+
notify_observers :state => :status, :type => "Volume", :value => "#{muted? ? 0 : @volume}%"
|
111
|
+
rescue => err
|
112
|
+
@error = err
|
113
|
+
changed
|
114
|
+
notify_observers :state => :error, :error => err
|
115
|
+
end
|
116
|
+
|
117
|
+
def muted?
|
118
|
+
@pipeline.get_property "mute"
|
119
|
+
end
|
120
|
+
|
121
|
+
def close()
|
122
|
+
@pipeline.stop
|
123
|
+
@mainloop.quit
|
124
|
+
changed
|
125
|
+
notify_observers :state => :closed
|
126
|
+
rescue => err
|
127
|
+
@error = err
|
128
|
+
changed
|
129
|
+
notify_observers :state => :error, :error => err
|
130
|
+
end
|
131
|
+
|
132
|
+
def gstPlayUrl(url)
|
133
|
+
@pipeline.stop
|
134
|
+
@pipeline.uri = url
|
135
|
+
@pipeline.play
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def runMainLoop
|
141
|
+
@logger.debug {"listen to gst pipeline"}
|
142
|
+
@mainloop = GLib::MainLoop.new nil, true
|
143
|
+
|
144
|
+
# Add custom tasks
|
145
|
+
GLib::Timeout.add_seconds(1) do
|
146
|
+
pos_res, position = @pipeline.query_position(Gst::Format::TIME)
|
147
|
+
dur_res, duration = @pipeline.query_duration(Gst::Format::TIME)
|
148
|
+
# @pipeline.query
|
149
|
+
duration = (duration / 1000000.0 / 1000).to_int
|
150
|
+
position = (position / 1000000.0 / 1000).to_int
|
151
|
+
if position > 0 && duration > 0 && duration > position
|
152
|
+
changed
|
153
|
+
notify_observers :state => :info,
|
154
|
+
:frame => position,
|
155
|
+
:frameleft => duration-position,
|
156
|
+
:time => position,
|
157
|
+
:timeleft => position-duration
|
158
|
+
end
|
159
|
+
true
|
160
|
+
end
|
161
|
+
|
162
|
+
bus = @pipeline.bus
|
163
|
+
bus.add_watch do |bus, message|
|
164
|
+
raise "message nil" if message.nil?
|
165
|
+
|
166
|
+
case message.type
|
167
|
+
when Gst::MessageType::EOS
|
168
|
+
changed
|
169
|
+
notify_observers :state => :stop
|
170
|
+
when Gst::MessageType::WARNING
|
171
|
+
warning, debug = message.parse_warning
|
172
|
+
changed
|
173
|
+
notify_observers :state => :error, :error => warning.message
|
174
|
+
when Gst::MessageType::ERROR
|
175
|
+
error, debug = message.parse_error
|
176
|
+
changed
|
177
|
+
notify_observers :state => :error, :error => error.message
|
178
|
+
when Gst::MessageType::BUFFERING
|
179
|
+
percent = message.parse_buffering
|
180
|
+
changed
|
181
|
+
notify_observers :state => :status, :type => "Buffering", :value => "#{percent}%"
|
182
|
+
when Gst::MessageType::STATE_CHANGED
|
183
|
+
error, state = message.parse_state_changed
|
184
|
+
case state
|
185
|
+
when Gst::State::PAUSED
|
186
|
+
changed
|
187
|
+
notify_observers :state => :pause
|
188
|
+
when Gst::State::PLAYING
|
189
|
+
changed
|
190
|
+
notify_observers :state => :play
|
191
|
+
end
|
192
|
+
else
|
193
|
+
@logger.info {"bus event: #{message.type.name} #{message.type} #{message.structure}"}
|
194
|
+
end
|
195
|
+
true
|
196
|
+
end
|
197
|
+
|
198
|
+
# @pipeline.play
|
199
|
+
context = @mainloop.context
|
200
|
+
thread = Thread.new do
|
201
|
+
while @mainloop.running? do
|
202
|
+
context.iteration false
|
203
|
+
sleep 0.05
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
data/lib/mpg123player.rb
CHANGED
@@ -1,13 +1,28 @@
|
|
1
1
|
require 'open3'
|
2
|
+
begin
|
3
|
+
require 'httpclient'
|
4
|
+
rescue LoadError
|
5
|
+
end
|
2
6
|
|
3
7
|
class MPG123Player
|
4
8
|
include Observable
|
5
9
|
attr_reader :error, :paused
|
6
10
|
attr_accessor :logger
|
7
11
|
|
8
|
-
def
|
12
|
+
def version
|
13
|
+
return "Audio backend: #{`mpg123 --version`.strip}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize params = {}
|
17
|
+
params.each { |key, value| send "#{key}=", value }
|
18
|
+
unless defined? HTTPClient
|
19
|
+
puts "Mpg123 requires HTTPClient to work"
|
20
|
+
exit false
|
21
|
+
end
|
22
|
+
|
9
23
|
@volume = 100
|
10
24
|
@muted = false
|
25
|
+
@logger.info version
|
11
26
|
@inqueue = []
|
12
27
|
begin
|
13
28
|
@pin, @pout, @perr = Open3.popen3 "mpg123 --keep-open --remote"
|
@@ -25,7 +40,11 @@ class MPG123Player
|
|
25
40
|
def play(track)
|
26
41
|
@last_track = track
|
27
42
|
unless track.nil? || track.is_a?(Hash) && track[:error]
|
28
|
-
mpg123puts "load #{track["mpg123url"]}"
|
43
|
+
# mpg123puts "load #{track["mpg123url"]}"
|
44
|
+
strhead = HTTPClient.new.head(track['mpg123url'])
|
45
|
+
streamuri = strhead.header['Location'][0]
|
46
|
+
streamuri.sub! 'https', 'http'
|
47
|
+
mpg123puts "load #{streamuri}"
|
29
48
|
@pstate = 2
|
30
49
|
end
|
31
50
|
changed
|
data/lib/ncurses_ui.rb
CHANGED
@@ -1,9 +1,25 @@
|
|
1
1
|
require 'curses'
|
2
2
|
|
3
|
+
class ::Hash
|
4
|
+
def deep_merge(second)
|
5
|
+
merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
|
6
|
+
self.merge(second, &merger)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
3
10
|
class NCursesUI
|
4
|
-
attr_accessor :logger
|
11
|
+
attr_accessor :logger, :audio_backend
|
12
|
+
|
13
|
+
def initialize cloud, options = {}, params = {}
|
14
|
+
params.each { |key, value| send "#{key}=", value }
|
15
|
+
|
16
|
+
if not @audio_backend
|
17
|
+
@audio_backend = Object.new
|
18
|
+
def @audio_backend.version
|
19
|
+
"Audio backend: None"
|
20
|
+
end
|
21
|
+
end
|
5
22
|
|
6
|
-
def initialize cloud, options = {}
|
7
23
|
defaults = {
|
8
24
|
:colors => {
|
9
25
|
:default => [:cyan, :blue],
|
@@ -12,11 +28,12 @@ class NCursesUI
|
|
12
28
|
:progress => [:cyan, :blue],
|
13
29
|
:progress_bar => [:blue, :cyan],
|
14
30
|
:title => [:cyan, :black],
|
15
|
-
:artist => [:cyan, :black]
|
31
|
+
:artist => [:cyan, :black],
|
32
|
+
:status => [:magenta, :black]
|
16
33
|
}
|
17
34
|
}.freeze
|
18
35
|
|
19
|
-
@options = defaults.
|
36
|
+
@options = defaults.deep_merge(options || {})
|
20
37
|
@cloud = cloud
|
21
38
|
@state = :running
|
22
39
|
@frac = 0
|
@@ -40,7 +57,7 @@ class NCursesUI
|
|
40
57
|
Curses.timeout = 5
|
41
58
|
@p = NProgress.new stdscr, 0, 0, :progress, :progress_bar
|
42
59
|
@l = NPlaylist.new stdscr, 4, 0, :playlist, :playlist_active, 0, 0, @playlist
|
43
|
-
@i = NInfobox.new stdscr, 4, 0, :playlist, 0,
|
60
|
+
@i = NInfobox.new self, stdscr, 4, 0, :playlist, 0, 9
|
44
61
|
@d = NDownloadBox.new stdscr, Curses.lines-1, 0, :default, 0, 1
|
45
62
|
@l.active = 0
|
46
63
|
last_ch = nil
|
@@ -78,8 +95,10 @@ class NCursesUI
|
|
78
95
|
@cloud.pause
|
79
96
|
end
|
80
97
|
|
81
|
-
|
82
|
-
|
98
|
+
statusLine = @status || @error
|
99
|
+
|
100
|
+
if statusLine
|
101
|
+
Nutils.print stdscr, 3, 0, "#{statusLine}", :status
|
83
102
|
Curses.refresh
|
84
103
|
end
|
85
104
|
tr = " %s " % [Nutils.timestr(@timetotal)]
|
@@ -122,7 +141,7 @@ class NCursesUI
|
|
122
141
|
@l.active = pos if @l
|
123
142
|
when :download
|
124
143
|
if arg[:error]
|
125
|
-
@error = arg[:error]
|
144
|
+
@error = "Error: #{arg[:error]}"
|
126
145
|
end
|
127
146
|
if arg[:count]
|
128
147
|
count = arg[:count]
|
@@ -146,13 +165,13 @@ class NCursesUI
|
|
146
165
|
when :load
|
147
166
|
track = arg[:track]
|
148
167
|
if track.nil?
|
149
|
-
@error = "Nothing found!"
|
168
|
+
@error = "Error: Nothing found!"
|
150
169
|
else
|
151
170
|
@error = nil
|
152
171
|
@title = track["title"]
|
153
172
|
@username = track["user"]["username"]
|
154
173
|
@timetotal = track["duration"]
|
155
|
-
@error = track[:error] if track[:error]
|
174
|
+
@error = "Error: #{track[:error]}" if track[:error]
|
156
175
|
end
|
157
176
|
when :info
|
158
177
|
frame = arg[:frame].to_f
|
@@ -166,7 +185,19 @@ class NCursesUI
|
|
166
185
|
when :stop
|
167
186
|
@op = "\u25FC"
|
168
187
|
when :error
|
169
|
-
@error = arg[:error]
|
188
|
+
@error = "Error: #{arg[:error]}"
|
189
|
+
when :status
|
190
|
+
if arg[:type]
|
191
|
+
@status = "#{arg[:type]}: #{arg[:value]}"
|
192
|
+
if @statusTimeout
|
193
|
+
@statusTimeout.exit
|
194
|
+
end
|
195
|
+
@statusTimeout = Thread.new do
|
196
|
+
sleep 5
|
197
|
+
@status = nil
|
198
|
+
@statusTimeout = nil
|
199
|
+
end
|
200
|
+
end
|
170
201
|
end
|
171
202
|
end
|
172
203
|
|
@@ -382,7 +413,8 @@ end
|
|
382
413
|
|
383
414
|
class NInfobox
|
384
415
|
attr_accessor :visible
|
385
|
-
def initialize scr, row, col, color, w, h
|
416
|
+
def initialize parent, scr, row, col, color, w, h
|
417
|
+
@parent = parent
|
386
418
|
@scr = scr
|
387
419
|
@row = row
|
388
420
|
@col = col
|
@@ -418,12 +450,13 @@ class NInfobox
|
|
418
450
|
|
419
451
|
def refresh
|
420
452
|
return unless @visible
|
421
|
-
Nutils.print @win, 1, 2, "Cloudruby v1", :default
|
422
|
-
Nutils.print @win, 2, 4, "
|
423
|
-
Nutils.print @win, 3, 4, "
|
424
|
-
Nutils.print @win, 4, 4, "
|
425
|
-
Nutils.print @win, 5, 4, "
|
426
|
-
Nutils.print @win, 6, 4, "
|
453
|
+
Nutils.print @win, 1, 2, "Cloudruby v1.1", :default
|
454
|
+
Nutils.print @win, 2, 4, "UI Toolkit: #{(Curses.const_defined?"VERSION")?Curses::VERSION : "N/A"}", :default
|
455
|
+
Nutils.print @win, 3, 4, "#{@parent.audio_backend.version}", :default
|
456
|
+
Nutils.print @win, 4, 4, "Ruby version: #{RUBY_VERSION}", :default
|
457
|
+
Nutils.print @win, 5, 4, "Author: kulpae <my.shando@gmail.com>", :artist
|
458
|
+
Nutils.print @win, 6, 4, "Website: uraniumlane.net", :title
|
459
|
+
Nutils.print @win, 7, 4, "License: MIT", :default
|
427
460
|
@win.attron(Colors.map(@color)) if @color
|
428
461
|
@win.box 0, 0
|
429
462
|
@win.attroff(Colors.map(@color)) if @color
|
data/lib/soundcloud.rb
CHANGED
@@ -5,8 +5,10 @@ require 'json/pure'
|
|
5
5
|
class SoundCloud
|
6
6
|
include Observable
|
7
7
|
LIMIT = 100
|
8
|
+
attr_accessor :logger
|
8
9
|
|
9
|
-
def initialize
|
10
|
+
def initialize client_id, params = {}
|
11
|
+
params.each { |key, value| send "#{key}=", value }
|
10
12
|
@cid = client_id
|
11
13
|
@playlist_pos = -1
|
12
14
|
@download_queue = []
|
@@ -16,9 +18,9 @@ class SoundCloud
|
|
16
18
|
def load_playlist(search = nil, offset = 0)
|
17
19
|
search = "" unless search && !search.empty?
|
18
20
|
if search =~ /\s*http(s)?:\/\/(www.)?soundcloud.com.*/
|
19
|
-
url = "
|
21
|
+
url = "https://api.soundcloud.com/resolve.json?url=%s&client_id=%s" % [CGI.escape(search), @cid]
|
20
22
|
else
|
21
|
-
url = "
|
23
|
+
url = "https://api.soundcloud.com/tracks.json?client_id=%s&filter=streamable&limit=%d&offset=%d&q=%s" \
|
22
24
|
% [@cid, LIMIT, offset, CGI.escape(search)]
|
23
25
|
end
|
24
26
|
c = open(url) do |io|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cloudruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paul Koch
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-11-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: curses
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: httpclient
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2'
|
41
55
|
description: "== A soundcloud player written in Ruby \nwith Ncurses for user interface
|
42
56
|
and \nmpg123 for playback.\n\nRequires the mpg123 executable to be present in the
|
43
57
|
PATH.\n\n== Usage\n\n* lists recently uploaded tracks from soundcloud\n cloudruby\n\n*
|
@@ -51,6 +65,7 @@ extra_rdoc_files: []
|
|
51
65
|
files:
|
52
66
|
- bin/cloudruby
|
53
67
|
- lib/cloudruby.rb
|
68
|
+
- lib/gstplayer.rb
|
54
69
|
- lib/mpg123player.rb
|
55
70
|
- lib/ncurses_ui.rb
|
56
71
|
- lib/soundcloud.rb
|
@@ -74,9 +89,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
89
|
- !ruby/object:Gem::Version
|
75
90
|
version: '0'
|
76
91
|
requirements:
|
92
|
+
- gem gstreamer (optional)
|
77
93
|
- mpg123 executable
|
78
94
|
rubyforge_project:
|
79
|
-
rubygems_version: 2.
|
95
|
+
rubygems_version: 2.4.8
|
80
96
|
signing_key:
|
81
97
|
specification_version: 4
|
82
98
|
summary: Ncurses player for Soundcloud tracks in Ruby
|