mpg321 0.4.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +20 -16
- data/history.rdoc +7 -0
- data/lib/mpg321.rb +6 -101
- data/lib/mpg321/client.rb +16 -0
- data/lib/mpg321/control/playback.rb +34 -0
- data/lib/mpg321/control/volume.rb +22 -0
- data/lib/mpg321/playlist.rb +41 -0
- data/lib/mpg321/process_wrapper.rb +89 -0
- data/lib/mpg321/version.rb +3 -0
- data/spec/client_spec.rb +12 -0
- data/spec/playback_control_spec.rb +103 -0
- data/spec/playlist_spec.rb +90 -0
- data/spec/process_wrapper_spec.rb +114 -0
- data/spec/spec_helper.rb +3 -2
- data/spec/support/context.rb +13 -0
- data/spec/support/matcher.rb +51 -0
- data/spec/support/mocks.rb +76 -0
- data/spec/volume_control_spec.rb +68 -0
- metadata +18 -12
- data/.gitignore +0 -4
- data/.semver +0 -6
- data/.travis.yml +0 -19
- data/Gemfile +0 -9
- data/Rakefile +0 -10
- data/lib/version.rb +0 -3
- data/mpg321.gemspec +0 -17
- data/spec/mpg321_spec.rb +0 -157
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 945d500d8a3ca04c58284f2456ba90ace17c8f60
|
4
|
+
data.tar.gz: b4d8b06747ad4c9457902093fe76ff50a27ead0b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f5742b2ae1a0d8201dc512fde5567f01332733095c8eaec29cdfc8ef9f7608ba4d1ef6a7a2ce2a0cbd3b31551e983454309f3e42bd4813b2ced3709355e0cb1a
|
7
|
+
data.tar.gz: 04900df45afc40372adced49111df5563e3f6dbf2a185e7404469c44b0c1cdd695e8918e4db3c08d0b603b60bcdd302dc696c8f5641a06161dba97134e5d5f5b
|
data/README.md
CHANGED
@@ -25,27 +25,16 @@ Here's how you can easily play an mp3:
|
|
25
25
|
|
26
26
|
```ruby
|
27
27
|
require 'mpg321'
|
28
|
-
|
28
|
+
mpg321 = Mpg321::Client.new
|
29
29
|
|
30
30
|
mpg321.play('/some_path/song.mp3') #=> to play a song from a file
|
31
31
|
|
32
32
|
mpg321.play('http://example.com/a_hosted_song.mp3') #=> to play a song from the web
|
33
33
|
```
|
34
|
-
To play a list of songs:
|
35
|
-
|
36
|
-
```ruby
|
37
|
-
require 'mpg321'
|
38
|
-
|
39
|
-
mog321 = Mpg321.new
|
40
|
-
mpg321.play(['/some_path/song.mp3', '/another_path/another_song'])
|
41
|
-
```
|
42
34
|
|
43
35
|
Volume controls:
|
44
36
|
|
45
37
|
```ruby
|
46
|
-
require 'mpg321'
|
47
|
-
mog321 = Mpg321.new
|
48
|
-
|
49
38
|
mpg321.volume #=> initialized to 50
|
50
39
|
|
51
40
|
mpg321.volume = 10 #=> Set volume to a number between 0 and 100
|
@@ -53,15 +42,30 @@ mpg321.volume = 10 #=> Set volume to a number between 0 and 100
|
|
53
42
|
mpg321.volume_up(10) #=> Increase volume by 10
|
54
43
|
mpg321.volume_down(10) #=> Decrease volume by 10
|
55
44
|
```
|
45
|
+
|
56
46
|
Other controls:
|
57
47
|
|
58
48
|
```ruby
|
59
|
-
require 'mpg321'
|
60
|
-
mog321 = Mpg321.new
|
61
|
-
|
62
49
|
mpg321.pause #=> Pause / unpause song
|
63
50
|
|
64
|
-
mpg321.stop #=> Stop playing song
|
51
|
+
mpg321.stop #=> Stop playing song
|
52
|
+
```
|
53
|
+
|
54
|
+
Events:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
mpg321.on(:playback_finished) { } # Called when song ends.
|
58
|
+
|
59
|
+
mpg321.on(:file_not_found) { } # Called when song could not be found.
|
60
|
+
|
61
|
+
mpg321.on(:error) { } # Called in error case. Do not use object
|
62
|
+
# afterwards.
|
63
|
+
```
|
64
|
+
|
65
|
+
Stop the mpg321 process:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
mpg321.quit
|
65
69
|
```
|
66
70
|
|
67
71
|
Contributing
|
data/history.rdoc
CHANGED
data/lib/mpg321.rb
CHANGED
@@ -1,101 +1,6 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
def initialize
|
8
|
-
@volume = 50
|
9
|
-
@paused = false
|
10
|
-
@music_input, @stdout, @stderr, _thread = Open3.popen3("mpg321 -R mpg321_ruby")
|
11
|
-
handle_stderr
|
12
|
-
handle_stdout
|
13
|
-
send_volume
|
14
|
-
end
|
15
|
-
|
16
|
-
def pause
|
17
|
-
@paused = !@paused
|
18
|
-
@music_input.puts "P"
|
19
|
-
end
|
20
|
-
|
21
|
-
def stop
|
22
|
-
@paused = !@paused
|
23
|
-
@music_input.puts "S"
|
24
|
-
end
|
25
|
-
|
26
|
-
def paused?
|
27
|
-
@paused
|
28
|
-
end
|
29
|
-
|
30
|
-
def play song_list
|
31
|
-
@paused = !@paused
|
32
|
-
@song_list = song_list
|
33
|
-
if song_list.class == Array
|
34
|
-
@list = true
|
35
|
-
play_song @song_list.shift
|
36
|
-
else
|
37
|
-
@list = false
|
38
|
-
play_song song_list
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
def volume_up volume
|
43
|
-
@volume += volume
|
44
|
-
@volume = [@volume, 100].min
|
45
|
-
send_volume
|
46
|
-
end
|
47
|
-
|
48
|
-
def volume_down volume
|
49
|
-
@volume -= volume
|
50
|
-
@volume = [@volume, 0].max
|
51
|
-
send_volume
|
52
|
-
end
|
53
|
-
|
54
|
-
def volume= volume
|
55
|
-
if volume < 0
|
56
|
-
@volume = 0
|
57
|
-
elsif volume > 100
|
58
|
-
@volume = 100
|
59
|
-
else
|
60
|
-
@volume = volume
|
61
|
-
end
|
62
|
-
send_volume
|
63
|
-
end
|
64
|
-
|
65
|
-
private
|
66
|
-
|
67
|
-
def play_song song
|
68
|
-
@music_input.puts "L #{song}"
|
69
|
-
end
|
70
|
-
|
71
|
-
def send_volume
|
72
|
-
@music_input.puts "G #{@volume}"
|
73
|
-
end
|
74
|
-
|
75
|
-
def handle_stderr
|
76
|
-
Thread.new do
|
77
|
-
loop do
|
78
|
-
|
79
|
-
#Not sure how to test this yet
|
80
|
-
begin
|
81
|
-
Timeout::timeout(1) { @stderr.readline }
|
82
|
-
rescue Timeout::Error
|
83
|
-
play @song_list if @list && !@paused
|
84
|
-
end
|
85
|
-
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
def handle_stdout
|
91
|
-
Thread.new do
|
92
|
-
loop do
|
93
|
-
#Not sure how to test this yet
|
94
|
-
@stout.readline
|
95
|
-
if @list && @line.match(/@P 3/)
|
96
|
-
play @song_list
|
97
|
-
end
|
98
|
-
end
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
1
|
+
require 'mpg321/control/playback'
|
2
|
+
require 'mpg321/control/volume'
|
3
|
+
require 'mpg321/client'
|
4
|
+
require 'mpg321/playlist'
|
5
|
+
require 'mpg321/process_wrapper'
|
6
|
+
require 'mpg321/version'
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Mpg321
|
2
|
+
class Client
|
3
|
+
include Control::Playback
|
4
|
+
include Control::Volume
|
5
|
+
|
6
|
+
extend Forwardable
|
7
|
+
def_delegator :@process, :send_command
|
8
|
+
def_delegator :@process, :on
|
9
|
+
def_delegator :@process, :quit
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@process = ProcessWrapper.new
|
13
|
+
send :volume=, 50
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Mpg321
|
2
|
+
module Control
|
3
|
+
module Playback
|
4
|
+
def play song
|
5
|
+
@loaded = true
|
6
|
+
@paused = false
|
7
|
+
send_command 'L', song
|
8
|
+
end
|
9
|
+
|
10
|
+
def pause
|
11
|
+
@paused = !@paused
|
12
|
+
send_command 'P'
|
13
|
+
end
|
14
|
+
|
15
|
+
def stop
|
16
|
+
@loaded = false
|
17
|
+
@paused = false
|
18
|
+
send_command 'S'
|
19
|
+
end
|
20
|
+
|
21
|
+
def paused?
|
22
|
+
!!@paused
|
23
|
+
end
|
24
|
+
|
25
|
+
def loaded?
|
26
|
+
!!@loaded
|
27
|
+
end
|
28
|
+
|
29
|
+
def playing?
|
30
|
+
loaded? && !paused?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Mpg321
|
2
|
+
module Control
|
3
|
+
module Volume
|
4
|
+
def volume
|
5
|
+
@volume
|
6
|
+
end
|
7
|
+
|
8
|
+
def volume= volume
|
9
|
+
@volume = [0, volume.to_i, 100].sort[1]
|
10
|
+
send_command 'G', @volume
|
11
|
+
end
|
12
|
+
|
13
|
+
def volume_up inc
|
14
|
+
send :volume=, @volume + inc
|
15
|
+
end
|
16
|
+
|
17
|
+
def volume_down dec
|
18
|
+
send :volume=, @volume - dec
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Mpg321
|
2
|
+
class Playlist
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
PLAYLIST_ADVANCE_EVENTS = [ :playback_finished, :file_not_found ]
|
6
|
+
|
7
|
+
def initialize autoplay = false, client = Client.new
|
8
|
+
@tracks = Array.new
|
9
|
+
@access = Mutex.new
|
10
|
+
@autoplay = autoplay
|
11
|
+
@client = client
|
12
|
+
|
13
|
+
PLAYLIST_ADVANCE_EVENTS.each do |event|
|
14
|
+
@client.on(event) { advance }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def enqueue song
|
19
|
+
@access.synchronize { @tracks << song }
|
20
|
+
advance if @autoplay && !@client.loaded?
|
21
|
+
end
|
22
|
+
|
23
|
+
def advance
|
24
|
+
if song = dequeue
|
25
|
+
@client.play song
|
26
|
+
else
|
27
|
+
@client.stop if @client.loaded?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def each &block
|
32
|
+
@access.synchronize { @tracks.each &block }
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def dequeue
|
38
|
+
@access.synchronize { @tracks.shift }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module Mpg321
|
4
|
+
class ProcessWrapper
|
5
|
+
def initialize
|
6
|
+
spawn_mpg321
|
7
|
+
|
8
|
+
@callbacks = Hash.new { |h, e| h[e] = Array.new }
|
9
|
+
@read_thr = async_handle_stdoe
|
10
|
+
end
|
11
|
+
|
12
|
+
def send_command command, *args
|
13
|
+
@stdin.puts [command, *(args.map(&:to_s))].join(' ')
|
14
|
+
end
|
15
|
+
|
16
|
+
def on(event, &block)
|
17
|
+
@callbacks[event] << block
|
18
|
+
end
|
19
|
+
|
20
|
+
def quit
|
21
|
+
send_command 'Q'
|
22
|
+
@read_thr.join
|
23
|
+
@wait_thr.value
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# This corresponds to the system-specific "No such file or directory" message.
|
29
|
+
# TODO: Would love to use strerror(3) instead, but Ruby doesn't expose that.
|
30
|
+
LOCALIZED_ENOENT_MESSAGE = SystemCallError.new('', Errno::ENOENT::Errno).message.gsub(' - ', '')
|
31
|
+
|
32
|
+
def spawn_mpg321
|
33
|
+
@stdin, @stdoe, @wait_thr = Open3.popen2e 'mpg321 -R mpg321_ruby'
|
34
|
+
end
|
35
|
+
|
36
|
+
def emit(event, *args)
|
37
|
+
@callbacks[event].each { |cb| cb.call *args }
|
38
|
+
end
|
39
|
+
|
40
|
+
# :nocov:
|
41
|
+
def async_handle_stdoe
|
42
|
+
Thread.new do
|
43
|
+
begin
|
44
|
+
loop { read_stdoe_line }
|
45
|
+
rescue EOFError
|
46
|
+
# Stream exhausted, ignore.
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def read_stdoe_line
|
52
|
+
line = @stdoe.readline
|
53
|
+
case line[0..1]
|
54
|
+
when '@F'
|
55
|
+
parts = line.split(' ')
|
56
|
+
emit :status_update, {
|
57
|
+
current_frame: parts[1].to_i,
|
58
|
+
frames_remaining: parts[2].to_i,
|
59
|
+
current_time: parts[3].to_f,
|
60
|
+
time_remaining: parts[4].to_f
|
61
|
+
}
|
62
|
+
when '@P'
|
63
|
+
# mpg321 sends '@P 3' when the song has finished playing.
|
64
|
+
if line[3] == '3'
|
65
|
+
emit :playback_finished
|
66
|
+
end
|
67
|
+
when '@E'
|
68
|
+
# This is sent in case of illegal syntax, e.g. an empty string
|
69
|
+
# as parameter to the LOAD command.
|
70
|
+
emit :error, line.strip
|
71
|
+
else
|
72
|
+
# Any critical error is sent to stderr and is not preprended
|
73
|
+
# by an @ character. mpg321 immediately exits afterwards.
|
74
|
+
if line[0] != '@'
|
75
|
+
if line.include? LOCALIZED_ENOENT_MESSAGE
|
76
|
+
# Handle file not found gracefully and restart mpg321.
|
77
|
+
@wait_thr.join
|
78
|
+
spawn_mpg321
|
79
|
+
emit :file_not_found
|
80
|
+
else
|
81
|
+
# TODO: This leaves the instance in a unusable state. Subsequent
|
82
|
+
# calls to #send_message etc. will respond with EPIPE.
|
83
|
+
emit :error, line.strip
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Mpg321::Client do
|
4
|
+
include_context 'fake_mpg321'
|
5
|
+
|
6
|
+
describe '#play' do
|
7
|
+
it 'sends a message to play a song' do
|
8
|
+
subject.play example_file
|
9
|
+
expect(last_command).to be_a_load_command_for example_file
|
10
|
+
end
|
11
|
+
|
12
|
+
context '#when paused' do
|
13
|
+
before do
|
14
|
+
subject.pause
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'unpauses' do
|
18
|
+
expect(subject.paused?).to be_truthy
|
19
|
+
subject.stop
|
20
|
+
expect(subject.paused?).to be_falsy
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#pause' do
|
26
|
+
it 'sends a message to pause / unpause the song' do
|
27
|
+
subject.pause
|
28
|
+
expect(last_command).to be_a_pause_command
|
29
|
+
end
|
30
|
+
|
31
|
+
context '#when paused' do
|
32
|
+
before do
|
33
|
+
subject.pause
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'unpauses' do
|
37
|
+
expect(subject.paused?).to be_truthy
|
38
|
+
subject.pause
|
39
|
+
expect(subject.paused?).to be_falsy
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '#stop' do
|
45
|
+
it 'sends a message to stop the song' do
|
46
|
+
subject.stop
|
47
|
+
expect(last_command).to be_a_stop_command
|
48
|
+
end
|
49
|
+
|
50
|
+
context '#when paused' do
|
51
|
+
before do
|
52
|
+
subject.pause
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'unpauses' do
|
56
|
+
expect(subject.paused?).to be_truthy
|
57
|
+
subject.stop
|
58
|
+
expect(subject.paused?).to be_falsy
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe '#paused?' do
|
64
|
+
it 'returns false if it is not paused' do
|
65
|
+
expect(subject.paused?).to be_falsy
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'returns false if it is not paused' do
|
69
|
+
expect(subject.paused?).to be_falsy
|
70
|
+
subject.pause
|
71
|
+
expect(subject.paused?).to be_truthy
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe '#loaded?' do
|
76
|
+
it 'returns false if no file has been loaded for playback' do
|
77
|
+
expect(subject.loaded?).to be_falsy
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'returns true if a file has been loaded for playback' do
|
81
|
+
subject.play '/some_path/file_name'
|
82
|
+
expect(subject.loaded?).to be_truthy
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe '#playing?' do
|
87
|
+
it 'returns false if no file has been loaded' do
|
88
|
+
expect(subject.playing?).to be_falsy
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'returns true if a file has been loaded and playback is not paused' do
|
92
|
+
subject.play '/some_path/file_name'
|
93
|
+
expect(subject.playing?).to be_truthy
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'returns false if a file has been loaded but playback is paused' do
|
97
|
+
subject.play '/some_path/file_name'
|
98
|
+
subject.pause
|
99
|
+
expect(subject.playing?).to be_falsy
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Mpg321::Playlist do
|
4
|
+
include_context 'fake_mpg321'
|
5
|
+
|
6
|
+
let(:client) { subject.instance_variable_get(:@client) }
|
7
|
+
let(:process) { client.instance_variable_get(:@process) }
|
8
|
+
let(:fake_read_thread) { process.instance_variable_get(:@read_thr) }
|
9
|
+
|
10
|
+
let(:song) { 'song' }
|
11
|
+
let(:songs) { %w{ A B C } }
|
12
|
+
|
13
|
+
before(:each) do
|
14
|
+
songs.each { |s| subject.enqueue s }
|
15
|
+
end
|
16
|
+
|
17
|
+
after(:each) { subject.instance_variable_get(:@tracks).clear }
|
18
|
+
|
19
|
+
describe 'enqueue' do
|
20
|
+
it "adds a song to the playlist's back" do
|
21
|
+
expect { subject.enqueue song }.to change { subject.entries.size }.by 1
|
22
|
+
expect(subject.entries).to eq (songs + [song])
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'when the autoplay flag is set' do
|
26
|
+
let(:client2) { Mpg321::Client.new }
|
27
|
+
subject { Mpg321::Playlist.new(true, client2) }
|
28
|
+
|
29
|
+
context 'and the player has not loaded a file' do
|
30
|
+
it 'automatically starts playback' do
|
31
|
+
expect(client2).to receive(:loaded?).and_return false
|
32
|
+
expect(subject).to receive :advance
|
33
|
+
subject.enqueue song
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'and the player has loaded a file' do
|
38
|
+
it 'automatically starts playback' do
|
39
|
+
expect(client2).to receive(:loaded?).and_return true
|
40
|
+
expect(subject).to_not receive :advance
|
41
|
+
subject.enqueue song
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe 'advance' do
|
48
|
+
context 'when the playlist contains songs' do
|
49
|
+
it 'removes the first song from the playlist' do
|
50
|
+
expect { subject.advance }.to change { subject.entries.size }.by -1
|
51
|
+
expect(subject).to_not include songs.first
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'starts playback of the next song' do
|
55
|
+
expect(client).to receive(:play).with songs.first
|
56
|
+
subject.advance
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'when the playlist is empty' do
|
61
|
+
before { subject.instance_variable_get(:@tracks).clear }
|
62
|
+
|
63
|
+
it 'stops playback of the current song if the player is loaded' do
|
64
|
+
expect(client).to receive(:loaded?).and_return true
|
65
|
+
expect(client).to receive :stop
|
66
|
+
subject.advance
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe '(events)' do
|
72
|
+
context 'in case of a playback_finished event' do
|
73
|
+
it 'advances to the next track' do
|
74
|
+
expect(subject).to receive :advance
|
75
|
+
|
76
|
+
fake_mpg321.finish_playback
|
77
|
+
fake_read_thread.run_once
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'in case of a file_not_found event' do
|
82
|
+
it 'advances to the next track' do
|
83
|
+
expect(subject).to receive :advance
|
84
|
+
|
85
|
+
fake_mpg321.send_file_not_found
|
86
|
+
fake_read_thread.run_once
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Mpg321::ProcessWrapper do
|
4
|
+
include_context 'fake_mpg321'
|
5
|
+
|
6
|
+
let(:fake_read_thread) { subject.instance_variable_get(:@read_thr) }
|
7
|
+
|
8
|
+
describe 'quit' do
|
9
|
+
it 'sends a quit message' do
|
10
|
+
subject.quit
|
11
|
+
expect(last_command).to be_a_quit_command
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'waits for completion of the reader thread' do
|
15
|
+
# We expect the main thread to wait for the reader threads termination.
|
16
|
+
expect(fake_read_thread).to receive :join
|
17
|
+
subject.quit
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'collects the status of the mpg321 process (and does not leave zombies)' do
|
21
|
+
expect(fake_mpg321.wait_thr).to receive :value
|
22
|
+
subject.quit
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'returns the exitstatus of the mpg321 process' do
|
26
|
+
expect(subject.quit).to respond_to :exitstatus
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '(error handling)' do
|
31
|
+
context 'when a file can not be found' do
|
32
|
+
it 'notifies interested observers' do
|
33
|
+
callback = Proc.new {}
|
34
|
+
subject.on :file_not_found, &callback
|
35
|
+
expect(callback).to receive :call
|
36
|
+
|
37
|
+
fake_mpg321.send_file_not_found
|
38
|
+
fake_read_thread.run_once
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'respawns the dead mpg321 process' do
|
42
|
+
expect(fake_mpg321.wait_thr).to receive(:join)
|
43
|
+
expect(subject).to receive(:spawn_mpg321).and_return(fake_mpg321)
|
44
|
+
|
45
|
+
fake_mpg321.send_file_not_found
|
46
|
+
fake_read_thread.run_once
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'in case of a syntax error in a command' do
|
51
|
+
it 'notifies interested observers' do
|
52
|
+
callback = Proc.new {}
|
53
|
+
subject.on :error, &callback
|
54
|
+
expect(callback).to receive(:call)
|
55
|
+
|
56
|
+
fake_mpg321.send_command_syntax_error
|
57
|
+
fake_read_thread.run_once
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'in any other error case (e.g., no sound card found)' do
|
62
|
+
it 'notifies interested observers' do
|
63
|
+
callback = Proc.new {}
|
64
|
+
subject.on :error, &callback
|
65
|
+
expect(callback).to receive(:call)
|
66
|
+
|
67
|
+
fake_mpg321.send_fatal_unknown_error
|
68
|
+
fake_read_thread.run_once
|
69
|
+
end
|
70
|
+
|
71
|
+
# TODO: ProcessWrapper should probably remember that mpg321 is dead
|
72
|
+
# and return some sane error message afterwards.
|
73
|
+
it 'puts itself into a sane state'
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe '(read thread)' do
|
78
|
+
it 'dies when mpg321 exits' do
|
79
|
+
# When mpg321 dies, the stdout pipe will go away, resulting
|
80
|
+
# in an exception in the reader thread. We simulate this
|
81
|
+
# here by closing the StringIO.
|
82
|
+
fake_mpg321.stdoe.close
|
83
|
+
expect { fake_read_thread.run_once }.to raise_error IOError
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe '(events)' do
|
88
|
+
context 'when playback of a file has finished' do
|
89
|
+
it 'notifies interested observers' do
|
90
|
+
callback = Proc.new {}
|
91
|
+
subject.on :playback_finished, &callback
|
92
|
+
expect(callback).to receive :call
|
93
|
+
|
94
|
+
fake_mpg321.finish_playback
|
95
|
+
fake_read_thread.run_once
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context 'when frame decoding status update is received' do
|
100
|
+
let(:update_data) do
|
101
|
+
{ current_frame: 1, frames_remaining: 10, current_time: 1.00, time_remaining: 10.00 }
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'notifies interested observers' do
|
105
|
+
callback = Proc.new {}
|
106
|
+
subject.on :status_update, &callback
|
107
|
+
expect(callback).to receive(:call).with(update_data)
|
108
|
+
|
109
|
+
fake_mpg321.send_status_update update_data
|
110
|
+
fake_read_thread.run_once
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
-
require 'bundler/setup'
|
2
1
|
require 'simplecov'
|
2
|
+
SimpleCov.add_filter '/spec/'
|
3
3
|
SimpleCov.start
|
4
4
|
|
5
|
+
require 'mpg321'
|
5
6
|
|
6
|
-
Dir['./spec/support/*.rb'].
|
7
|
+
Dir['./spec/support/*.rb'].each { |f| require f }
|
7
8
|
|
8
9
|
RSpec.configure do |config|
|
9
10
|
config.filter_run :focus => true
|
@@ -0,0 +1,13 @@
|
|
1
|
+
RSpec.shared_context 'fake_mpg321' do
|
2
|
+
let(:fake_mpg321) { FakeMpg321.new }
|
3
|
+
let(:last_command) { fake_mpg321.last_command }
|
4
|
+
let(:example_file) { '/somepath/somefile.mp3' }
|
5
|
+
|
6
|
+
before do
|
7
|
+
allow(Open3).to receive(:popen2e).and_return(fake_mpg321.open2e_returns)
|
8
|
+
|
9
|
+
allow_any_instance_of(Mpg321::ProcessWrapper).to receive(:async_handle_stdoe) do |instance|
|
10
|
+
FakeReadThread.new(instance) { read_stdoe_line }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'rspec/expectations'
|
2
|
+
|
3
|
+
RSpec::Matchers.define :be_a_pause_command do
|
4
|
+
match do |actual|
|
5
|
+
actual[0] == 'P'
|
6
|
+
end
|
7
|
+
|
8
|
+
failure_message do |actual|
|
9
|
+
"expected that #{actual} would be a mpg321 pause command ('P'/'PAUSE')"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
RSpec::Matchers.define :be_a_stop_command do
|
14
|
+
match do |actual|
|
15
|
+
actual[0] == 'S'
|
16
|
+
end
|
17
|
+
|
18
|
+
failure_message do |actual|
|
19
|
+
"expected that #{actual} would be a mpg321 stop command ('S'/'STOP')"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
RSpec::Matchers.define :be_a_quit_command do
|
24
|
+
match do |actual|
|
25
|
+
actual[0] == 'Q'
|
26
|
+
end
|
27
|
+
|
28
|
+
failure_message do |actual|
|
29
|
+
"expected that #{actual} would be a mpg321 quit command ('Q'/'QUIT')"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
RSpec::Matchers.define :set_volume_to do |vol|
|
34
|
+
match do |actual|
|
35
|
+
actual == "G #{vol}"
|
36
|
+
end
|
37
|
+
|
38
|
+
failure_message do |actual|
|
39
|
+
"expected that #{actual} would be a mpg321 gain command ('G'/'LOAD') with '#{vol}'"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
RSpec::Matchers.define :be_a_load_command_for do |file|
|
44
|
+
match do |actual|
|
45
|
+
actual == "L #{file}"
|
46
|
+
end
|
47
|
+
|
48
|
+
failure_message do |actual|
|
49
|
+
"expected that #{actual} would be a mpg321 load command ('L'/'LOAD') for file '#{file}'"
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
class FakeReadThread
|
4
|
+
def initialize(ctx, &block)
|
5
|
+
@ctx = ctx
|
6
|
+
@action = block
|
7
|
+
end
|
8
|
+
|
9
|
+
def run_once
|
10
|
+
@ctx.instance_eval &@action
|
11
|
+
end
|
12
|
+
|
13
|
+
def join
|
14
|
+
self
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class FakeWaitThread
|
19
|
+
FakeExitStatus = Struct.new(:exitstatus)
|
20
|
+
|
21
|
+
def join
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def value
|
26
|
+
FakeExitStatus.new(0)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class FakeMpg321
|
31
|
+
attr_reader :stdin, :stdoe, :wait_thr
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
@stdin = StringIO.new
|
35
|
+
@stdoe = StringIO.new
|
36
|
+
@wait_thr = FakeWaitThread.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def open2e_returns
|
40
|
+
[@stdin, @stdoe, @wait_thr]
|
41
|
+
end
|
42
|
+
|
43
|
+
def last_command
|
44
|
+
@stdin.rewind
|
45
|
+
cmd = @stdin.gets.strip until @stdin.eof?
|
46
|
+
@stdin = StringIO.new
|
47
|
+
cmd
|
48
|
+
end
|
49
|
+
|
50
|
+
def send_status_update data
|
51
|
+
send_mpg321_output "@F #{data[:current_frame]} #{data[:frames_remaining]} #{data[:current_time]} #{data[:time_remaining]}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def finish_playback
|
55
|
+
send_mpg321_output '@P 3'
|
56
|
+
end
|
57
|
+
|
58
|
+
def send_command_syntax_error
|
59
|
+
send_mpg321_output "@E Missing argument to 'L'"
|
60
|
+
end
|
61
|
+
|
62
|
+
def send_file_not_found
|
63
|
+
send_mpg321_output "foobar: #{Mpg321::ProcessWrapper::LOCALIZED_ENOENT_MESSAGE}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def send_fatal_unknown_error
|
67
|
+
send_mpg321_output 'big boom'
|
68
|
+
end
|
69
|
+
|
70
|
+
def send_mpg321_output(line)
|
71
|
+
@stdoe.rewind
|
72
|
+
@stdoe.flush
|
73
|
+
@stdoe.puts line
|
74
|
+
@stdoe.rewind
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Mpg321::Client do
|
4
|
+
include_context 'fake_mpg321'
|
5
|
+
|
6
|
+
describe '#volume_up' do
|
7
|
+
it 'sends a message to increase the volume' do
|
8
|
+
subject.volume_up 10
|
9
|
+
expect(last_command).to set_volume_to 60
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'has a maximum volume of 100' do
|
13
|
+
subject.volume_up 55
|
14
|
+
expect(subject.volume).to eq 100
|
15
|
+
expect(last_command).to set_volume_to 100
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#volume_down' do
|
20
|
+
it 'sends a message to decrease the volume' do
|
21
|
+
subject.volume_down 10
|
22
|
+
expect(last_command).to set_volume_to 40
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'has a minimum volume of 0' do
|
26
|
+
subject.volume_down 55
|
27
|
+
expect(subject.volume).to eq 0
|
28
|
+
expect(last_command).to set_volume_to 0
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '#volume' do
|
33
|
+
it 'returns the current volume' do
|
34
|
+
expect(subject.volume).to eq 50
|
35
|
+
subject.volume_up 5
|
36
|
+
expect(subject.volume).to eq 55
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#volume=' do
|
41
|
+
it 'sets the volume' do
|
42
|
+
expect(subject.volume).to eq 50
|
43
|
+
subject.volume = 11
|
44
|
+
expect(subject.volume).to eq 11
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'casts float arguments to integer' do
|
48
|
+
expect(subject.volume).to eq 50
|
49
|
+
subject.volume = 47.11
|
50
|
+
expect(subject.volume).to eq 47
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'has a minimum of 0' do
|
54
|
+
subject.volume = -1
|
55
|
+
expect(subject.volume).to eq 0
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'has a maximum of 100' do
|
59
|
+
subject.volume = 101
|
60
|
+
expect(subject.volume).to eq 100
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'sends a message to set the volume' do
|
64
|
+
subject.volume = 11
|
65
|
+
expect(last_command).to set_volume_to 11
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mpg321
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Richard Vickerstaff
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-10-20 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: A simple ruby wrapper around mpg321
|
14
14
|
email:
|
@@ -17,22 +17,28 @@ executables: []
|
|
17
17
|
extensions: []
|
18
18
|
extra_rdoc_files: []
|
19
19
|
files:
|
20
|
-
- ".gitignore"
|
21
|
-
- ".semver"
|
22
|
-
- ".travis.yml"
|
23
|
-
- Gemfile
|
24
20
|
- LICENSE
|
25
21
|
- README.md
|
26
|
-
- Rakefile
|
27
22
|
- history.rdoc
|
28
23
|
- lib/mpg321.rb
|
29
|
-
- lib/
|
30
|
-
- mpg321.
|
31
|
-
-
|
24
|
+
- lib/mpg321/client.rb
|
25
|
+
- lib/mpg321/control/playback.rb
|
26
|
+
- lib/mpg321/control/volume.rb
|
27
|
+
- lib/mpg321/playlist.rb
|
28
|
+
- lib/mpg321/process_wrapper.rb
|
29
|
+
- lib/mpg321/version.rb
|
30
|
+
- spec/client_spec.rb
|
31
|
+
- spec/playback_control_spec.rb
|
32
|
+
- spec/playlist_spec.rb
|
33
|
+
- spec/process_wrapper_spec.rb
|
32
34
|
- spec/spec_helper.rb
|
35
|
+
- spec/support/context.rb
|
36
|
+
- spec/support/matcher.rb
|
37
|
+
- spec/support/mocks.rb
|
38
|
+
- spec/volume_control_spec.rb
|
33
39
|
homepage: https://github.com/RichardVickerstaff/mpg321
|
34
40
|
licenses:
|
35
|
-
-
|
41
|
+
- GPL-2.0
|
36
42
|
metadata: {}
|
37
43
|
post_install_message:
|
38
44
|
rdoc_options: []
|
@@ -50,7 +56,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
56
|
version: '0'
|
51
57
|
requirements: []
|
52
58
|
rubyforge_project:
|
53
|
-
rubygems_version: 2.4.
|
59
|
+
rubygems_version: 2.4.8
|
54
60
|
signing_key:
|
55
61
|
specification_version: 4
|
56
62
|
summary: Provides a ruby object to wrap the mpg321 'Remote control'
|
data/.gitignore
DELETED
data/.semver
DELETED
data/.travis.yml
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
language: ruby
|
2
|
-
rvm:
|
3
|
-
- 2.1.0
|
4
|
-
- 2.0.0
|
5
|
-
- 1.9.3
|
6
|
-
- jruby-1.7.12
|
7
|
-
- ruby-head
|
8
|
-
matrix:
|
9
|
-
allow_failures:
|
10
|
-
- rvm: ruby-head
|
11
|
-
deploy:
|
12
|
-
provider: rubygems
|
13
|
-
api_key:
|
14
|
-
secure: RmGYn4yb1PPjZxdWxgwUWTENnRaqp/PXUMfieJoAE0Dw1JrwkjxI3qhy/I6p36KusLsrFxrh4UdATj78QsWdtZVLJ9tvjONOsrAUjIszBwRmOXDWRkH8eLGCCmIYfYMZaRYvlQhPR2t5CC6BwfHS6W1AU5YtU5SSpk0HT+SRzII=
|
15
|
-
gem: mpg321
|
16
|
-
on:
|
17
|
-
tags: true
|
18
|
-
repo: RichardVickerstaff/mpg321
|
19
|
-
all_branches: true
|
data/Gemfile
DELETED
data/Rakefile
DELETED
data/lib/version.rb
DELETED
data/mpg321.gemspec
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
# coding: utf-8
|
2
|
-
require File.expand_path('../lib/version', __FILE__)
|
3
|
-
lib = File.expand_path('../lib', __FILE__)
|
4
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
-
|
6
|
-
Gem::Specification.new do |spec|
|
7
|
-
spec.name = "mpg321"
|
8
|
-
spec.version = Mpg321::VERSION
|
9
|
-
spec.authors = ["Richard Vickerstaff"]
|
10
|
-
spec.email = ["m3akq@btinternet.com"]
|
11
|
-
spec.description = "A simple ruby wrapper around mpg321"
|
12
|
-
spec.summary = "Provides a ruby object to wrap the mpg321 'Remote control'"
|
13
|
-
spec.homepage = "https://github.com/RichardVickerstaff/mpg321"
|
14
|
-
spec.license = "GNU GENERAL PUBLIC LICENSE"
|
15
|
-
spec.files = `git ls-files`.split($/)
|
16
|
-
spec.require_paths = ["lib"]
|
17
|
-
end
|
data/spec/mpg321_spec.rb
DELETED
@@ -1,157 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
require 'mpg321'
|
3
|
-
|
4
|
-
describe Mpg321 do
|
5
|
-
|
6
|
-
let(:thread) { double :thread }
|
7
|
-
let(:stderr) { double :stderr }
|
8
|
-
let(:stdout) { double :stdout }
|
9
|
-
let(:stdin) { double :stdin }
|
10
|
-
|
11
|
-
before do
|
12
|
-
allow(Open3).to receive(:popen3).and_return([stdin, stdout, stderr, thread])
|
13
|
-
allow(stdin).to receive(:puts)
|
14
|
-
end
|
15
|
-
|
16
|
-
describe 'initialize' do
|
17
|
-
it 'sets the volume to 50' do
|
18
|
-
expect(subject.volume).to eq 50
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
describe '#volume_up' do
|
23
|
-
it 'sends a message to increase the volume' do
|
24
|
-
expect(stdin).to receive(:puts).with "G 60"
|
25
|
-
subject.volume_up 10
|
26
|
-
end
|
27
|
-
|
28
|
-
it 'has a maximum volume of 100' do
|
29
|
-
expect(stdin).to receive(:puts).with "G 100"
|
30
|
-
subject.volume_up 55
|
31
|
-
expect(subject.volume).to eq 100
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
describe '#volume_down' do
|
36
|
-
it 'sends a message to decrease the volume' do
|
37
|
-
expect(stdin).to receive(:puts).with "G 40"
|
38
|
-
subject.volume_down 10
|
39
|
-
end
|
40
|
-
|
41
|
-
it 'has a minimum volume of 0' do
|
42
|
-
expect(stdin).to receive(:puts).with "G 0"
|
43
|
-
subject.volume_down 55
|
44
|
-
expect(subject.volume).to eq 0
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
describe '#volume' do
|
49
|
-
it 'returns the current volume' do
|
50
|
-
expect(subject.volume).to eq 50
|
51
|
-
subject.volume_up 5
|
52
|
-
expect(subject.volume).to eq 55
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
describe '#volume=' do
|
57
|
-
it 'sets the volume' do
|
58
|
-
expect(subject.volume).to eq 50
|
59
|
-
subject.volume = 11
|
60
|
-
expect(subject.volume).to eq 11
|
61
|
-
end
|
62
|
-
|
63
|
-
it 'has a minimum of 0' do
|
64
|
-
subject.volume = -1
|
65
|
-
expect(subject.volume).to eq 0
|
66
|
-
end
|
67
|
-
|
68
|
-
it 'has a maximum of 100' do
|
69
|
-
subject.volume = 101
|
70
|
-
expect(subject.volume).to eq 100
|
71
|
-
end
|
72
|
-
|
73
|
-
it 'sends a message to set the volume' do
|
74
|
-
expect(stdin).to receive(:puts).with "G 11"
|
75
|
-
subject.volume = 11
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
describe '#play' do
|
80
|
-
context 'when there is only one song' do
|
81
|
-
it 'sends a message to play a song' do
|
82
|
-
expect(stdin).to receive(:puts).with "L /some_path/file_name"
|
83
|
-
subject.play '/some_path/file_name'
|
84
|
-
end
|
85
|
-
|
86
|
-
context '#when paused' do
|
87
|
-
before do
|
88
|
-
subject.pause
|
89
|
-
end
|
90
|
-
|
91
|
-
it 'unpauses' do
|
92
|
-
expect(subject.paused?).to be_truthy
|
93
|
-
subject.stop
|
94
|
-
expect(subject.paused?).to be_falsy
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
context 'when there moer than one song' do
|
100
|
-
it 'sends a message to play the first song' do
|
101
|
-
expect(stdin).to receive(:puts).with "L /some_path/file_name"
|
102
|
-
subject.play ['/some_path/file_name', '/some_other_song']
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
describe '#pause' do
|
108
|
-
it 'sends a message to pause / unpause the song' do
|
109
|
-
expect(stdin).to receive(:puts).with "P"
|
110
|
-
subject.pause
|
111
|
-
end
|
112
|
-
|
113
|
-
context '#when paused' do
|
114
|
-
before do
|
115
|
-
subject.pause
|
116
|
-
end
|
117
|
-
|
118
|
-
it 'unpauses' do
|
119
|
-
expect(subject.paused?).to be_truthy
|
120
|
-
subject.pause
|
121
|
-
expect(subject.paused?).to be_falsy
|
122
|
-
end
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
describe '#stop' do
|
127
|
-
it 'sends a message to stop the song' do
|
128
|
-
expect(stdin).to receive(:puts).with "S"
|
129
|
-
subject.stop
|
130
|
-
end
|
131
|
-
|
132
|
-
context '#when paused' do
|
133
|
-
before do
|
134
|
-
subject.pause
|
135
|
-
end
|
136
|
-
|
137
|
-
it 'unpauses' do
|
138
|
-
expect(subject.paused?).to be_truthy
|
139
|
-
subject.stop
|
140
|
-
expect(subject.paused?).to be_falsy
|
141
|
-
end
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
describe '#paused?' do
|
146
|
-
it 'returns false if it is not paused' do
|
147
|
-
expect(subject.paused?).to be_falsy
|
148
|
-
end
|
149
|
-
|
150
|
-
it 'returns false if it is not paused' do
|
151
|
-
expect(subject.paused?).to be_falsy
|
152
|
-
subject.pause
|
153
|
-
expect(subject.paused?).to be_truthy
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
end
|