mpg321 0.4.0 → 1.0.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/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
|