cloudruby 1.0.4 → 1.1.0
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 +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
|