plllayer 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # plllayer
2
+
3
+ `plllayer` provides a Ruby interface to an external media player, such as `mplayer`.
4
+
5
+ It takes a playlist to play (which may just be an `Array` of `String`s containing the paths to the audio files), and lets you control playback as the playlist plays, with commands such as `pause`, `resume`, `seek`, `skip`, and so on.
6
+
7
+ ## Install
8
+
9
+ $ gem install plllayer
10
+
11
+ ## Example
12
+
13
+ Here's how to play your music library in random order:
14
+
15
+ $ irb
16
+ irb> require "plllayer"
17
+ => true
18
+ irb> player = Plllayer.new(Dir["Music/**/*.{mp3,m4a,ogg}"])
19
+ => #<Plllayer: ... >
20
+ irb> player.shuffle
21
+ => true
22
+ irb> player.play
23
+ => true
24
+ irb>
25
+
26
+ Then, while it's playing, you can type `player.track` to see what song it's playing, `player.skip` to skip to the next song, and so on.
27
+
28
+ ## Usage
29
+
30
+ Make a `Plllayer` like this:
31
+
32
+ player = Plllayer.new
33
+
34
+ This initializes a `Plllayer` with an empty playlist, and will use whatever external audio player is available on the system. You can also pass in an initial playlist, or specify what external audio player you want to use:
35
+
36
+ player = Plllayer.new(playlist)
37
+ player = Plllayer.new(external_player: :mplayer)
38
+
39
+ Only `mplayer` is supported at the moment.
40
+
41
+ ### Playlists
42
+
43
+ A playlist is just an `Array` of tracks. A track is either a `String` containing the path to an audio file, or an object with a `#location` attribute that returns the path.
44
+
45
+ A singleton playlist, with only one track, doesn't need to be in an `Array`. That's ugly. You can just pass a track object to any method that expects a playlist and it'll understand.
46
+
47
+ To tell the `Plllayer` what playlist to play, either initialize the `Plllayer` with the playlist or use `Plllayer#append`:
48
+
49
+ player = Plllayer.new
50
+ player.append(playlist)
51
+ player << more_tracks # the << operator is an alias for append
52
+
53
+ The playlist can be accessed by `Plllayer#playlist`.
54
+
55
+ The playlist can be shuffled using `Plllayer#shuffle`:
56
+
57
+ player.shuffle
58
+
59
+ If one of the tracks is currently playing, it will be kept at the top of the playlist while the rest of the tracks will be shuffled.
60
+
61
+ The playlist can also be sorted:
62
+
63
+ player.sort
64
+
65
+ This delegates to Ruby's `Array#sort`, which sorts the tracks using their `<=>` method. This also means you can pass a block to compare tracks by, like this:
66
+
67
+ player.sort { |a, b| [a.artist, a.album, a.track_number] <=> [b.artist, b.album, b.track_number] }
68
+
69
+ This is safe to do when a track is currently playing, and then the next track that plays will be whatever comes next in the sorted playlist.
70
+
71
+ Lastly, the player's playlist can be reset to an empty playlist using `Plllayer#clear`:
72
+
73
+ player.clear
74
+
75
+ This stops playback if it's currently playing.
76
+
77
+ ### Playback commands
78
+
79
+ There are many commands that influence playback. Think of them as buttons on a physical media player: you can push them even when they don't apply to the state of the player. They always return false if this is the case, otherwise they return a truthy value.
80
+
81
+ See `lib/plllayer.rb` for all the available commands, with documentation.
82
+
83
+ ## Todo
84
+
85
+ * Support multiple external players
86
+ * Work around mplayer's bug where it reports the length of the track totally wrong
87
+ * Fix bug with mplayer returning weird answers sometimes
88
+ * Write tests
89
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "rspec/core/rake_task"
2
+
3
+ RSpec::Core::RakeTask.new(:spec)
4
+
5
+ task default: :spec
6
+
@@ -0,0 +1,11 @@
1
+ class Module
2
+ def synchronize(method, mutex_name)
3
+ alias_method :"_unsynchronized_#{method}", method.to_sym
4
+ define_method(method.to_sym) do |*args, &blk|
5
+ instance_variable_get(mutex_name.to_sym).synchronize do
6
+ send(:"_unsynchronized_#{method}", *args, &blk)
7
+ end
8
+ end
9
+ end
10
+ end
11
+
data/lib/plllayer.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require "open4"
2
2
 
3
- require "plllayer/time_helpers.rb"
3
+ require "plllayer/time_helpers"
4
+ require "plllayer/synchronize"
5
+
4
6
  require "plllayer/single_player"
5
7
  require "plllayer/single_players/mplayer"
6
8
  require "plllayer/single_players/nop"
@@ -49,6 +51,8 @@ class Plllayer
49
51
  @paused = false
50
52
  @playing = false
51
53
  @repeat_mode = nil
54
+
55
+ @index_mutex = Mutex.new
52
56
  end
53
57
 
54
58
  # Append tracks to the playlist. Can be done while the playlist is playing.
@@ -57,8 +61,8 @@ class Plllayer
57
61
  # An ArgumentError is raised when you try to pass a non-track.
58
62
  #
59
63
  # This method is aliased as the << operator.
60
- def append(tracks)
61
- tracks = Array(tracks)
64
+ def append(*tracks)
65
+ tracks = tracks.flatten
62
66
  tracks.each do |track|
63
67
  if !track.is_a?(String) && !track.respond_to?(:location)
64
68
  raise ArgumentError, "a #{track.class} is not a track (try adding a #location method)"
@@ -67,8 +71,74 @@ class Plllayer
67
71
  @playlist += tracks
68
72
  @playlist.dup
69
73
  end
74
+ synchronize :append, :@index_mutex
70
75
  alias :<< :append
71
76
 
77
+ # Insert one or more tracks in the playlist, right after the currently-playing
78
+ # track. If no track is playing, insert to the head of the playlist.
79
+ def insert(*tracks)
80
+ after = @index || -1
81
+ _unsynchronized_insert_at(after + 1, *tracks)
82
+ @playlist.dup
83
+ end
84
+ synchronize :insert, :@index_mutex
85
+
86
+ # Insert one or more tracks anywhere in the playlist. A negative index may be
87
+ # given to refer to the end of the playlist.
88
+ def insert_at(index, *tracks)
89
+ index = @playlist.length + index + 1 if index < 0
90
+ unless (0..@playlist.length).include? index
91
+ raise IndexError, "index is out of range"
92
+ end
93
+ @playlist.insert(index, *tracks)
94
+ @index += tracks.length if @index && index < @index
95
+ @playlist.dup
96
+ end
97
+ synchronize :insert_at, :@index_mutex
98
+
99
+ # Remove one or more tracks from the playlist by value. If the currently-playing
100
+ # track is removed, it will start playing the next track in the playlist.
101
+ def remove(*tracks)
102
+ n = nil
103
+ n = tracks.pop if tracks.last.is_a?(Fixnum)
104
+ index = 0
105
+ current_track_removed = false
106
+ @playlist = @playlist.inject([]) do |playlist, track|
107
+ if tracks.include?(track) && (n.nil? || n > 0)
108
+ n &&= n - 1
109
+ @index -= 1 if @index && index < @index
110
+ current_track_removed = true if index == @index
111
+ else
112
+ playlist << track
113
+ end
114
+ index += 1
115
+ playlist
116
+ end
117
+ _unsynchronized_change_track(0) if current_track_removed
118
+ @playlist.dup
119
+ end
120
+ synchronize :remove, :@index_mutex
121
+
122
+ # Remove one or more tracks from the playlist at a particular index. A negative
123
+ # index may be given to refer to the end of the playlist. If the currently-playing
124
+ # track is removed, it will start playing the next track in the playlist.
125
+ def remove_at(index, n = 1)
126
+ index = @playlist.length + index if index < 0
127
+ if @playlist.empty? || index < 0 || index + n > @playlist.length
128
+ raise IndexError, "index is out of range"
129
+ end
130
+ @playlist.slice!(index, n)
131
+ if @index && @index > index
132
+ if @index < index + n
133
+ _unsynchronized_change_track(index - @index)
134
+ else
135
+ @index -= n
136
+ end
137
+ end
138
+ @playlist.dup
139
+ end
140
+ synchronize :remove_at, :@index_mutex
141
+
72
142
  # Returns a copy of the playlist.
73
143
  def playlist
74
144
  @playlist.dup
@@ -142,6 +212,7 @@ class Plllayer
142
212
  false
143
213
  end
144
214
  end
215
+ synchronize :stop, :@index_mutex
145
216
 
146
217
  # Pause playback.
147
218
  def pause
@@ -342,9 +413,6 @@ class Plllayer
342
413
 
343
414
  def change_track(by = 1, options = {})
344
415
  if playing?
345
- if options[:auto] && track.respond_to?(:increment_play_count)
346
- track.increment_play_count
347
- end
348
416
  @index += by
349
417
  if @repeat_mode
350
418
  case @repeat_mode
@@ -358,13 +426,14 @@ class Plllayer
358
426
  @single_player.stop if paused? && @single_player.active?
359
427
  play_track if not paused?
360
428
  else
361
- stop
429
+ _unsynchronized_stop
362
430
  end
363
431
  true
364
432
  else
365
433
  false
366
434
  end
367
435
  end
436
+ synchronize :change_track, :@index_mutex
368
437
 
369
438
  def play_track
370
439
  @single_player.play(track_path) do
data/plllayer.gemspec CHANGED
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "plllayer"
3
- s.version = "0.0.3"
4
- s.date = "2013-02-05"
3
+ s.version = "0.0.4"
4
+ s.date = "2013-02-12"
5
5
  s.summary = "An audio playback library for Ruby."
6
6
  s.description = "plllayer is an audio playback library for Ruby. It is a Ruby interface to some external media player, such as mplayer."
7
7
  s.author = "Jeremy Ruten"
@@ -10,8 +10,9 @@ Gem::Specification.new do |s|
10
10
  s.license = "MIT"
11
11
  s.required_ruby_version = ">= 1.9.2"
12
12
 
13
- s.files = ["Gemfile", "Gemfile.lock", "LICENSE", "plllayer.gemspec"]
13
+ s.files = ["Rakefile", "Gemfile", "Gemfile.lock", "LICENSE", "plllayer.gemspec", "README.md"]
14
14
  s.files += Dir["lib/**/*.rb"]
15
+ s.files += Dir["spec/**/*.{rb,mp3}"]
15
16
 
16
17
  %w(bundler open4).each do |gem_name|
17
18
  s.add_runtime_dependency gem_name
data/spec/10000ms.mp3 ADDED
Binary file
data/spec/250ms.mp3 ADDED
Binary file
data/spec/3000ms.mp3 ADDED
Binary file
data/spec/invalid.mp3 ADDED
@@ -0,0 +1 @@
1
+ this is an invalid mp3 file. let's see what mplayer does with it.
@@ -0,0 +1,185 @@
1
+ require "spec_helper"
2
+
3
+ PATH_3000MS = "spec/3000ms.mp3"
4
+ PATH_10000MS = "spec/10000ms.mp3"
5
+ NONEXISTANT_PATH = "spec/not_here.mp3"
6
+ INVALID_AUDIO_FILE = "spec/invalid.mp3"
7
+
8
+ describe Plllayer::SinglePlayers::MPlayer do
9
+ before(:each) do
10
+ @player = Plllayer::SinglePlayers::MPlayer.new
11
+ end
12
+
13
+ after(:each) do
14
+ @player.stop
15
+ end
16
+
17
+ it "is not playing by default" do
18
+ @player.should_not be_playing
19
+ end
20
+
21
+ it "starts playing a single audio file" do
22
+ @player.play(PATH_3000MS)
23
+ @player.should be_playing
24
+ sleep 3.1
25
+ @player.should_not be_playing
26
+ end
27
+
28
+ it "executes a callback when the track is done playing" do
29
+ done_playing = false
30
+ @player.play(PATH_3000MS) { done_playing = true }
31
+ done_playing.should be_false
32
+ sleep 3.1
33
+ done_playing.should be_true
34
+ end
35
+
36
+ it "doesn't play non-existant files" do
37
+ expect { @player.play NONEXISTANT_PATH }.to raise_error(Plllayer::FileNotFoundError)
38
+ end
39
+
40
+ it "doesn't play invalid audio files" do
41
+ expect { @player.play INVALID_AUDIO_FILE }.to raise_error(Plllayer::InvalidAudioFileError)
42
+ end
43
+
44
+ it "stops playback" do
45
+ callback_called = false
46
+ @player.play(PATH_3000MS) { callback_called = true }
47
+ @player.stop
48
+ @player.should_not be_playing
49
+ sleep 3.1
50
+ callback_called.should be_false
51
+ end
52
+
53
+ it "pauses and resumes playback" do
54
+ @player.play(PATH_3000MS)
55
+ @player.should_not be_paused
56
+ @player.pause
57
+ @player.should be_paused
58
+ position = @player.position
59
+ sleep 0.3
60
+ @player.position.should be_within(100).of(position)
61
+ @player.resume
62
+ @player.should_not be_paused
63
+ sleep 0.3
64
+ @player.position.should_not be_within(100).of(position)
65
+ end
66
+
67
+ it "seeks to an absolute position" do
68
+ @player.play(PATH_3000MS)
69
+ @player.pause
70
+ @player.seek(2000)
71
+ @player.position.should be_within(100).of(2000)
72
+ end
73
+
74
+ it "seeks to a relative position" do
75
+ @player.play(PATH_10000MS)
76
+ @player.pause
77
+ position = @player.position
78
+ @player.seek(5000, :relative)
79
+ @player.position.should be_within(2500).of(position + 5000)
80
+ @player.seek(-4000, :relative)
81
+ @player.position.should be_within(2500).of(position + 1000)
82
+ end
83
+
84
+ it "seeks to a percentage position" do
85
+ @player.play(PATH_3000MS)
86
+ @player.pause
87
+ @player.seek(50, :percent)
88
+ @player.position.should be_within(100).of(1500)
89
+ end
90
+
91
+ it "has a default speed of 1" do
92
+ @player.speed.should eq(1.0)
93
+ end
94
+
95
+ it "speeds up playback" do
96
+ @player.speed = 2.0
97
+ @player.play(PATH_3000MS)
98
+ @player.should be_playing
99
+ sleep 2.5
100
+ @player.should_not be_playing
101
+ end
102
+
103
+ it "slows down playback" do
104
+ @player.speed = 0.5
105
+ @player.play(PATH_3000MS)
106
+ @player.should be_playing
107
+ sleep 3.1
108
+ @player.should be_playing
109
+ sleep 3
110
+ @player.should_not be_playing
111
+ end
112
+
113
+ it "is not initially muted" do
114
+ @player.should_not be_muted
115
+ end
116
+
117
+ it "mutes the volume" do
118
+ @player.mute
119
+ @player.should be_muted
120
+ end
121
+
122
+ it "unmutes the volume" do
123
+ @player.mute
124
+ @player.unmute
125
+ @player.should_not be_muted
126
+ end
127
+
128
+ it "has a default volume of 50%" do
129
+ @player.volume.should eq(50)
130
+ end
131
+
132
+ it "changes the volume" do
133
+ @player.volume = 100
134
+ @player.volume.should eq(100)
135
+ end
136
+
137
+ it "unmutes when changing the volume" do
138
+ @player.mute
139
+ @player.volume = 50
140
+ @player.should_not be_muted
141
+ end
142
+
143
+ it "keeps track of the position of playback" do
144
+ @player.play(PATH_3000MS)
145
+ @player.position.should be_within(100).of(100)
146
+ sleep 0.5
147
+ @player.position.should be_within(200).of(600)
148
+ end
149
+
150
+ it "tells you the length of the track in milliseconds" do
151
+ @player.play(PATH_3000MS)
152
+ @player.track_length.should be_within(100).of(3000)
153
+ end
154
+
155
+ it "persists speed, volume, and mute settings from track to track" do
156
+ @player.play(PATH_3000MS)
157
+ @player.speed = 2.0
158
+ @player.volume = 5
159
+ @player.mute
160
+ @player.stop
161
+
162
+ @player.play(PATH_3000MS)
163
+ @player.speed.should eq(2.0)
164
+ @player.volume.should eq(5)
165
+ @player.should be_muted
166
+ end
167
+
168
+ it "returns false for most commands when playback is stopped" do
169
+ @player.stop.should eq(false)
170
+ @player.pause.should eq(false)
171
+ @player.resume.should eq(false)
172
+ @player.seek(0).should eq(false)
173
+ @player.position.should eq(false)
174
+ @player.track_length.should eq(false)
175
+ end
176
+
177
+ it "returns false when double pausing resuming" do
178
+ @player.play(PATH_3000MS)
179
+ @player.pause.should be_true
180
+ @player.pause.should eq(false)
181
+ @player.resume.should be_true
182
+ @player.resume.should eq(false)
183
+ end
184
+ end
185
+
data/spec/nop_spec.rb ADDED
@@ -0,0 +1,162 @@
1
+ require "spec_helper"
2
+
3
+ PATH = "spec/250ms.mp3"
4
+ NONEXISTANT_PATH = "spec/not_here.mp3"
5
+
6
+ describe Plllayer::SinglePlayers::Nop do
7
+ before(:each) do
8
+ @player = Plllayer::SinglePlayers::Nop.new
9
+ end
10
+
11
+ it "is not playing by default" do
12
+ @player.should_not be_playing
13
+ end
14
+
15
+ it "starts playing a single audio file" do
16
+ @player.play PATH
17
+ @player.should be_playing
18
+ sleep 0.5
19
+ @player.should_not be_playing
20
+ end
21
+
22
+ it "executes a callback when the track is done playing" do
23
+ done_playing = false
24
+ @player.play(PATH) { done_playing = true }
25
+ done_playing.should be_false
26
+ sleep 0.5
27
+ done_playing.should be_true
28
+ end
29
+
30
+ it "doesn't play non-existant files" do
31
+ expect { @player.play NONEXISTANT_PATH }.to raise_error(Plllayer::FileNotFoundError)
32
+ end
33
+
34
+ it "stops playback" do
35
+ callback_called = false
36
+ @player.play(PATH) { callback_called = true }
37
+ @player.stop
38
+ @player.should_not be_playing
39
+ sleep 0.5
40
+ callback_called.should be_false
41
+ end
42
+
43
+ it "pauses and resumes playback" do
44
+ @player.play(PATH)
45
+ @player.should_not be_paused
46
+ @player.pause
47
+ @player.should be_paused
48
+ position = @player.position
49
+ sleep 0.2
50
+ @player.position.should eq(position)
51
+ @player.resume
52
+ @player.should_not be_paused
53
+ sleep 0.2
54
+ @player.position.should_not eq(position)
55
+ end
56
+
57
+ it "seeks to an absolute position" do
58
+ @player.play(PATH)
59
+ @player.pause
60
+ @player.seek(100)
61
+ @player.position.should eq(100)
62
+ end
63
+
64
+ it "seeks to a relative position" do
65
+ @player.play(PATH)
66
+ @player.pause
67
+ position = @player.position
68
+ @player.seek(10, :relative)
69
+ @player.position.should eq(position + 10)
70
+ @player.seek(-10, :relative)
71
+ @player.position.should eq(position)
72
+ end
73
+
74
+ it "seeks to a percentage position" do
75
+ @player.play(PATH)
76
+ @player.pause
77
+ @player.seek(50, :percent)
78
+ @player.position.should be_within(1).of(125)
79
+ end
80
+
81
+ it "has a default speed of 1" do
82
+ @player.speed.should eq(1.0)
83
+ end
84
+
85
+ it "speeds up playback" do
86
+ @player.speed = 2.0
87
+ @player.play(PATH)
88
+ @player.should be_playing
89
+ sleep 0.15
90
+ @player.should_not be_playing
91
+ end
92
+
93
+ it "slows down playback" do
94
+ @player.speed = 0.5
95
+ @player.play(PATH)
96
+ @player.should be_playing
97
+ sleep 0.3
98
+ @player.should be_playing
99
+ sleep 0.3
100
+ @player.should_not be_playing
101
+ end
102
+
103
+ it "is not initially muted" do
104
+ @player.should_not be_muted
105
+ end
106
+
107
+ it "mutes the volume" do
108
+ @player.mute
109
+ @player.should be_muted
110
+ end
111
+
112
+ it "unmutes the volume" do
113
+ @player.mute
114
+ @player.unmute
115
+ @player.should_not be_muted
116
+ end
117
+
118
+ it "has a default volume of 50%" do
119
+ @player.volume.should eq(50)
120
+ end
121
+
122
+ it "changes the volume" do
123
+ @player.volume = 100
124
+ @player.volume.should eq(100)
125
+ end
126
+
127
+ it "unmutes when changing the volume" do
128
+ @player.mute
129
+ @player.volume = 50
130
+ @player.should_not be_muted
131
+ end
132
+
133
+ it "keeps track of the position of playback" do
134
+ @player.play(PATH)
135
+ @player.position.should be_within(5).of(5)
136
+ sleep 0.1
137
+ @player.position.should be_within(10).of(105)
138
+ end
139
+
140
+ it "tells you the length of the track in milliseconds" do
141
+ @player.play(PATH)
142
+ @player.track_length.should eq(250)
143
+ end
144
+
145
+ it "returns false for most commands when playback is stopped" do
146
+ @player.stop.should eq(false)
147
+ @player.pause.should eq(false)
148
+ @player.resume.should eq(false)
149
+ @player.seek(0).should eq(false)
150
+ @player.position.should eq(false)
151
+ @player.track_length.should eq(false)
152
+ end
153
+
154
+ it "returns false when double pausing resuming" do
155
+ @player.play(PATH)
156
+ @player.pause.should be_true
157
+ @player.pause.should eq(false)
158
+ @player.resume.should be_true
159
+ @player.resume.should eq(false)
160
+ end
161
+ end
162
+
data/spec/playback.mp3 ADDED
@@ -0,0 +1 @@
1
+ this is an invalid mp3 file. let's see what mplayer does with it.
@@ -0,0 +1,84 @@
1
+ require "spec_helper"
2
+
3
+ TRACK_1 = "spec/250ms.mp3"
4
+ TRACK_2 = "spec/3000ms.mp3"
5
+ TRACK_3 = "spec/10000ms.mp3"
6
+
7
+ describe Plllayer do
8
+ before(:each) do
9
+ @player = Plllayer.new(external_player: :nop)
10
+ end
11
+
12
+ after(:each) do
13
+ @player.stop
14
+ end
15
+
16
+ describe "playlist control" do
17
+ it "appends tracks to the end of the playlist" do
18
+ @player.append(TRACK_1, TRACK_2)
19
+ @player << [TRACK_2, TRACK_3]
20
+ @player.playlist.should eq([TRACK_1, TRACK_2, TRACK_2, TRACK_3])
21
+ end
22
+
23
+ it "inserts tracks anywhere in the playlist" do
24
+ @player.insert_at(0, TRACK_1)
25
+ @player.playlist.should eq([TRACK_1])
26
+ @player.insert_at(0, TRACK_2)
27
+ @player.playlist.should eq([TRACK_2, TRACK_1])
28
+ @player.insert_at(1, TRACK_3)
29
+ @player.playlist.should eq([TRACK_2, TRACK_3, TRACK_1])
30
+ @player.insert_at(3, TRACK_1, TRACK_2, TRACK_3)
31
+ @player.playlist.should eq([TRACK_2, TRACK_3, TRACK_1, TRACK_1, TRACK_2, TRACK_3])
32
+ end
33
+
34
+ it "inserts tracks after the currently playing track" do
35
+ @player.insert(TRACK_1)
36
+ @player.playlist.should eq([TRACK_1])
37
+ @player.insert(TRACK_2)
38
+ @player.playlist.should eq([TRACK_2, TRACK_1])
39
+ @player.play
40
+ @player.insert(TRACK_3)
41
+ @player.playlist.should eq([TRACK_2, TRACK_3, TRACK_1])
42
+ end
43
+
44
+ it "removes tracks by object comparison" do
45
+ @player << [TRACK_1, TRACK_2, TRACK_1, TRACK_3, TRACK_1]
46
+ @player.remove(TRACK_1)
47
+ @player.playlist.should eq([TRACK_2, TRACK_3])
48
+ @player << [TRACK_1, TRACK_1, TRACK_1]
49
+ @player.remove(TRACK_1, 2)
50
+ @player.playlist.should eq([TRACK_2, TRACK_3, TRACK_1])
51
+ @player.remove(TRACK_1, TRACK_2, TRACK_3)
52
+ @player.playlist.should be_empty
53
+ end
54
+
55
+ it "removes one or more tracks anywhere in the playlist" do
56
+ @player << [TRACK_1, TRACK_2, TRACK_3]
57
+ @player.remove_at(1)
58
+ @player.playlist.should eq([TRACK_1, TRACK_3])
59
+ @player.remove_at(0, 2)
60
+ @player.playlist.should be_empty
61
+ end
62
+
63
+ it "continues playing while playlist is modified" do
64
+ @player << [TRACK_3, TRACK_2, TRACK_1]
65
+ @player.play
66
+ @player << TRACK_1
67
+ @player.playlist.should eq([TRACK_3, TRACK_2, TRACK_1, TRACK_1])
68
+ @player.track.should eq(TRACK_3)
69
+ @player.remove(TRACK_3)
70
+ @player.playlist.should eq([TRACK_2, TRACK_1, TRACK_1])
71
+ @player.track.should eq(TRACK_2)
72
+ @player.remove(TRACK_2, TRACK_1)
73
+ @player.should_not be_playing
74
+ end
75
+
76
+ it "raises index error when inserting or removing out of the playlist's bounds" do
77
+ expect { @player.insert_at(-3, TRACK_1) }.to raise_error(IndexError)
78
+ expect { @player.insert_at(2, TRACK_1) }.to raise_error(IndexError)
79
+ expect { @player.remove_at(-3) }.to raise_error(IndexError)
80
+ expect { @player.remove_at(0) }.to raise_error(IndexError)
81
+ end
82
+ end
83
+ end
84
+
@@ -0,0 +1,19 @@
1
+ require "plllayer"
2
+
3
+ # This file was generated by the `rspec --init` command. Conventionally, all
4
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
5
+ # Require this file using `require "spec_helper"` to ensure that it is only
6
+ # loaded once.
7
+ #
8
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
9
+ RSpec.configure do |config|
10
+ config.treat_symbols_as_metadata_keys_with_true_values = true
11
+ config.run_all_when_everything_filtered = true
12
+ config.filter_run :focus
13
+
14
+ # Run specs in random order to surface order dependencies. If you find an
15
+ # order dependency and want to debug it, you can fix the order by providing
16
+ # the seed, which is printed after each run.
17
+ # --seed 1234
18
+ config.order = 'random'
19
+ end
@@ -0,0 +1,87 @@
1
+ require "spec_helper"
2
+
3
+ describe Plllayer::TimeHelpers do
4
+ describe :format_time do
5
+ it "formats a time of zero milliseconds" do
6
+ Plllayer.format_time(0).should eq("0:00")
7
+ end
8
+
9
+ it "formats a time of one millisecond" do
10
+ Plllayer.format_time(1).should eq("0:00.001")
11
+ end
12
+
13
+ it "formats a time of one second" do
14
+ Plllayer.format_time(1000).should eq("0:01")
15
+ end
16
+
17
+ it "formats a time of one second plus a millisecond" do
18
+ Plllayer.format_time(1001).should eq("0:01.001")
19
+ end
20
+
21
+ it "formats a time of one minute and thirty seconds" do
22
+ Plllayer.format_time(90000).should eq("1:30")
23
+ end
24
+
25
+ it "formats times of an hour" do
26
+ Plllayer.format_time(3600000).should eq("1:00:00")
27
+ end
28
+
29
+ it "formats times with multiple components" do
30
+ Plllayer.format_time(7200000 + 120000 + 2000 + 2).should eq("2:02:02.002")
31
+ end
32
+
33
+ it "can exclude milliseconds from the output" do
34
+ Plllayer.format_time(1500).should eq("0:01.500")
35
+ Plllayer.format_time(1500, include_milliseconds: false).should eq("0:01")
36
+ end
37
+
38
+ it "can't format negative times" do
39
+ expect { Plllayer.format_time(-1) }.to raise_error(ArgumentError)
40
+ end
41
+ end
42
+
43
+ describe :parse_time do
44
+ it "parses times of zero" do
45
+ Plllayer.parse_time("0").should eq(0)
46
+ Plllayer.parse_time("0:00").should eq(0)
47
+ Plllayer.parse_time("0:00.000").should eq(0)
48
+ Plllayer.parse_time("0:00:00.000").should eq(0)
49
+ end
50
+
51
+ it "parses milliseconds" do
52
+ Plllayer.parse_time("0.001").should eq(1)
53
+ Plllayer.parse_time("0:00.999").should eq(999)
54
+ Plllayer.parse_time("0:00:00.5").should eq(500)
55
+ end
56
+
57
+ it "parses seconds" do
58
+ Plllayer.parse_time("1").should eq(1000)
59
+ Plllayer.parse_time("0:05").should eq(5000)
60
+ Plllayer.parse_time("0:00:09.500").should eq(9500)
61
+ end
62
+
63
+ it "parses minutes" do
64
+ Plllayer.parse_time("2:30").should eq(150000)
65
+ end
66
+
67
+ it "parses hours" do
68
+ Plllayer.parse_time("1:00:00").should eq(3600000)
69
+ Plllayer.parse_time("2:02:02.002").should eq(7200000 + 120000 + 2000 + 2)
70
+ Plllayer.parse_time("1000:00:00").should eq(1000 * 3600000)
71
+ end
72
+
73
+ it "assumes empty components are zero" do
74
+ Plllayer.parse_time("1:").should eq(60000)
75
+ Plllayer.parse_time("1::.5").should eq(3600000 + 500)
76
+ Plllayer.parse_time("::.001").should eq(1)
77
+ Plllayer.parse_time("::6.").should eq(6000)
78
+ Plllayer.parse_time("").should eq(0)
79
+ end
80
+
81
+ it "can't parse invalid strings" do
82
+ expect { Plllayer.parse_time("-1") }.to raise_error(ArgumentError)
83
+ expect { Plllayer.parse_time("1:00:00:00") }.to raise_error(ArgumentError)
84
+ end
85
+ end
86
+ end
87
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plllayer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-02-05 00:00:00.000000000 Z
12
+ date: 2013-02-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -82,15 +82,28 @@ executables: []
82
82
  extensions: []
83
83
  extra_rdoc_files: []
84
84
  files:
85
+ - Rakefile
85
86
  - Gemfile
86
87
  - Gemfile.lock
87
88
  - LICENSE
88
89
  - plllayer.gemspec
90
+ - README.md
89
91
  - lib/plllayer/single_player.rb
90
92
  - lib/plllayer/single_players/mplayer.rb
91
93
  - lib/plllayer/single_players/nop.rb
94
+ - lib/plllayer/synchronize.rb
92
95
  - lib/plllayer/time_helpers.rb
93
96
  - lib/plllayer.rb
97
+ - spec/mplayer_spec.rb
98
+ - spec/nop_spec.rb
99
+ - spec/plllayer_spec.rb
100
+ - spec/spec_helper.rb
101
+ - spec/time_helpers_spec.rb
102
+ - spec/10000ms.mp3
103
+ - spec/250ms.mp3
104
+ - spec/3000ms.mp3
105
+ - spec/invalid.mp3
106
+ - spec/playback.mp3
94
107
  homepage: http://github.com/yjerem/plllayer
95
108
  licenses:
96
109
  - MIT