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 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