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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 11c12e85e497f78a257b911218d636a6c9c0f796
4
- data.tar.gz: de65fc5363ed72e636dca7d84ea04fc5f9081a7f
3
+ metadata.gz: 945d500d8a3ca04c58284f2456ba90ace17c8f60
4
+ data.tar.gz: b4d8b06747ad4c9457902093fe76ff50a27ead0b
5
5
  SHA512:
6
- metadata.gz: 17ecfe4f4e8a21c17af1b8690dd76f2807083badb4420ad8a419948c0dc737df56e45d23478818dc96f13979560052d27907cdca9f0d2c386b4e4c050df1b111
7
- data.tar.gz: 29b170b7b75dab7fa7cef5124ec95b47719d3909db57639c573a5b4cae90640713dba5128907f878e980db9679f13c176f417a31385d0669353c642ddb06fbb1
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
- mog321 = Mpg321.new
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 / song list
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
@@ -1,3 +1,10 @@
1
+ == v1.0.0 (20 October 2015)
2
+
3
+ * Add better namspacing for the gem
4
+ * Add callback for 'file_not_found'
5
+ * Add callback for 'playback_finished'
6
+ * Add callback for 'error'
7
+
1
8
  == v0.4.0 (04 July 2015)
2
9
 
3
10
  * Add paused? mehod
data/lib/mpg321.rb CHANGED
@@ -1,101 +1,6 @@
1
- require 'open3'
2
- require 'timeout'
3
-
4
- class Mpg321
5
- attr_reader :volume
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
@@ -0,0 +1,3 @@
1
+ module Mpg321
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mpg321::Client do
4
+ include_context 'fake_mpg321'
5
+
6
+ describe 'initialize' do
7
+ it 'sets the volume to 50' do
8
+ expect(subject.volume).to eq 50
9
+ end
10
+ end
11
+
12
+ end
@@ -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'].map {|f| require f }
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.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-07-04 00:00:00.000000000 Z
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/version.rb
30
- - mpg321.gemspec
31
- - spec/mpg321_spec.rb
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
- - GNU GENERAL PUBLIC LICENSE
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.5
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
@@ -1,4 +0,0 @@
1
- /coverage/*
2
- *.gem
3
- *.sw?
4
- Gemfile.lock
data/.semver DELETED
@@ -1,6 +0,0 @@
1
- ---
2
- :major: 0
3
- :minor: 4
4
- :patch: 0
5
- :special: ''
6
- :metadata: ''
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
@@ -1,9 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- group :test, :development do
4
- gem 'simplecov'
5
- gem 'semver2'
6
- gem 'bundler-audit'
7
- gem 'rake-n-bake'
8
- gem 'rspec'
9
- end
data/Rakefile DELETED
@@ -1,10 +0,0 @@
1
- require'rake_n_bake'
2
-
3
- @external_dependencies = %w[ruby]
4
-
5
- task :default => [
6
- :"bake:check_external_dependencies",
7
- :"bake:code_quality:all",
8
- :"bake:rspec",
9
- :"bake:ok",
10
- ]
data/lib/version.rb DELETED
@@ -1,3 +0,0 @@
1
- module Mpg321
2
- VERSION = "0.4.0"
3
- end
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