radiodan 0.0.4 → 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.
@@ -0,0 +1,17 @@
1
+ require 'thin'
2
+
3
+ class Radiodan
4
+ class WebServer
5
+ include Logging
6
+
7
+ def initialize(*config)
8
+ @klass = config.shift
9
+ @options = config.shift || {}
10
+ @port = @options.fetch(:port, 3000)
11
+ end
12
+
13
+ def call(player)
14
+ Thin::Server.start @klass.new(player), '0.0.0.0', @port, :signals => false
15
+ end
16
+ end
17
+ end
@@ -10,8 +10,7 @@ class Player
10
10
  include EventBinding
11
11
 
12
12
  attr_reader :adapter, :playlist
13
- def_delegators :adapter, :stop
14
-
13
+
15
14
  def adapter=(adapter)
16
15
  @adapter = adapter
17
16
  @adapter.player = self
@@ -23,17 +22,22 @@ class Player
23
22
 
24
23
  def playlist=(new_playlist)
25
24
  @playlist = new_playlist
26
- trigger_event(:playlist, @playlist)
27
- # run sync to explicitly conform to new playlist?
25
+
26
+ trigger_event(:playlist, @playlist)
27
+ trigger_event(:player_state, @adapter.playlist) if @adapter
28
28
 
29
29
  @playlist
30
30
  end
31
31
 
32
+ def state
33
+ adapter.playlist
34
+ end
35
+
32
36
  =begin
33
37
  Sync checks the current status of the player.
34
38
  Is it paused? Playing? What is it playing?
35
39
  It compares the expected to actual statuses and
36
- makes changes required to keep them the same.
40
+ triggers events if there is a difference.
37
41
  =end
38
42
  def sync
39
43
  return false unless adapter?
@@ -41,29 +45,52 @@ class Player
41
45
  current = adapter.playlist
42
46
  expected = playlist
43
47
 
44
- state = Radiodan::PlaylistSync.new expected, current
48
+ sync = Radiodan::PlaylistSync.new expected, current
49
+ synced = sync.sync?
45
50
 
46
- if state.sync?
47
- true
48
- else
51
+ unless synced
49
52
  # playback state
50
- if state.errors.include? :state
51
- logger.debug "Expected: #{expected.state} Got: #{current.state}"
52
- trigger_event :play_state, expected.state
53
- end
54
-
55
- if state.errors.include? :mode
56
- logger.debug "Expected: #{expected.mode} Got: #{current.mode}"
57
- trigger_event :play_mode, expected.mode
53
+ sync.errors.each do |e|
54
+ case e
55
+ when :state
56
+ logger.debug "Expected State: #{expected.state} Got: #{current.state}"
57
+ trigger_event :play_state, current.state
58
+ when :mode
59
+ logger.debug "Expected Mode: #{expected.mode} Got: #{current.mode}"
60
+ trigger_event :play_mode, current.mode
61
+ when :new_tracks
62
+ logger.debug "Expected: #{expected.current.inspect} Got: #{current.current.inspect}"
63
+ trigger_event :playlist, expected
64
+ when :add_tracks
65
+ logger.debug "Found additional tracks to enqueue"
66
+ trigger_event :enqueue, expected.tracks[current.tracks.size..-1]
67
+ trigger_event :play_pending if sync.errors.include?(:state) && current.state == :stop
68
+ when :volume
69
+ logger.debug "Expected Volume: #{expected.volume} Got: #{current.volume}"
70
+ trigger_event :volume, expected.volume
71
+ end
58
72
  end
59
-
60
- # playlist
61
- if state.errors.include? :playlist
62
- logger.debug "Expected: #{expected.current} Got: #{current.current}"
63
- trigger_event :playlist, expected
64
- end
65
-
66
- false
73
+ end
74
+
75
+ trigger_event :sync, current
76
+ synced
77
+ end
78
+
79
+ def respond_to?(method)
80
+ if adapter.respond_to? method
81
+ true
82
+ else
83
+ super
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def method_missing(method, *args, &block)
90
+ if adapter.respond_to? method
91
+ adapter.send method, *args, &block
92
+ else
93
+ super
67
94
  end
68
95
  end
69
96
  end
@@ -29,7 +29,8 @@ class Playlist
29
29
  STATES = [:play, :stop, :pause]
30
30
  MODES = [:sequential, :resume, :random]
31
31
  attr_reader :state, :mode, :repeat, :tracks, :position, :seek, :volume
32
- def_delegators :@tracks, :size
32
+ alias_method :repeat?, :repeat
33
+ def_delegators :@tracks, :size, :length, :empty?
33
34
 
34
35
  def initialize(options={})
35
36
  self.state = options.fetch(:state, STATES.first)
@@ -44,7 +45,15 @@ class Playlist
44
45
  def current
45
46
  tracks[position]
46
47
  end
47
-
48
+
49
+ def random?
50
+ self.mode == :random
51
+ end
52
+
53
+ def state
54
+ empty? ? :stop : @state
55
+ end
56
+
48
57
  def state=(new_state)
49
58
  state = new_state.to_sym
50
59
 
@@ -98,15 +107,27 @@ class Playlist
98
107
  end
99
108
 
100
109
  def volume=(new_volume)
110
+ # -1 is allowed when volume cannot be determined
101
111
  begin
102
112
  new_volume = Integer(new_volume)
103
113
 
104
- raise ArgumentError if new_volume > 100 || new_volume < 0
114
+ raise ArgumentError if new_volume > 100 || new_volume < -1
105
115
  rescue ArgumentError
106
- raise VolumeError, "#{new_volume} not an integer 0-100"
116
+ raise VolumeError, "#{new_volume} not an integer -1-100"
107
117
  end
108
118
 
109
119
  @volume = new_volume
110
120
  end
121
+
122
+ def attributes
123
+ { :state => state,
124
+ :mode => mode,
125
+ :repeat => repeat,
126
+ :tracks => begin tracks.collect(&:attributes) rescue []; end,
127
+ :position => position,
128
+ :seek => seek,
129
+ :volume => volume }
130
+ end
131
+ alias_method :as_json, :attributes
111
132
  end
112
133
  end
@@ -1,5 +1,9 @@
1
+ require 'logging'
2
+
1
3
  class Radiodan
2
4
  class PlaylistSync
5
+ include Logging
6
+
3
7
  class SyncError < Exception; end
4
8
  attr_accessor :expected, :current
5
9
  attr_reader :errors
@@ -11,37 +15,57 @@ class PlaylistSync
11
15
  end
12
16
 
13
17
  def sync?
14
- prerequisites_check
15
- compare_playback_state & compare_playback_mode & compare_playlist
18
+ if ready?
19
+ compare_playback_state & compare_playback_mode & compare_tracks & compare_volume
20
+ end
16
21
  end
17
22
 
18
- private
19
- def prerequisites_check
20
- raise SyncError, 'No expected playlist to compare to' if expected.nil?
21
- raise SyncError, 'No current playlist to compare to' if current.nil?
23
+ def ready?
24
+ if expected.nil? || current.nil?
25
+ logger.warn 'Require two playlists to compare'
26
+ false
27
+ else
28
+ true
29
+ end
22
30
  end
23
31
 
32
+ private
24
33
  def compare_playback_state
25
34
  # add rules about when this is ok to be out of sync
26
35
  # e.g. sequential expected runs out of tracks and stops
27
- compare(:state) { @expected.state == @current.state }
36
+ report(:state) { @expected.state != @current.state }
28
37
  end
29
38
 
30
39
  def compare_playback_mode
31
- compare(:mode) { @expected.mode == @current.mode }
40
+ report(:mode) { @expected.mode != @current.mode }
32
41
  end
33
42
 
34
- def compare_playlist
35
- compare(:playlist) do
36
- @expected.size == @current.size && \
37
- @expected.tracks == @current.tracks
43
+ def compare_tracks
44
+ report(:add_tracks) do
45
+ # more tracks are added and
46
+ # original tracks are all in the same position in playlist
47
+ @expected.size > @current.size && !@current.empty? &&
48
+ @current.tracks.all? {|x| i=@current.tracks.index(x); @expected.tracks[i] == x }
49
+ end
50
+
51
+ return false if errors.include?(:add_tracks)
52
+
53
+ report(:new_tracks) do
54
+ @expected.size != @current.size ||
55
+ @expected.tracks != @current.tracks
56
+ end
57
+ end
58
+
59
+ def compare_volume
60
+ report(:volume) do
61
+ @expected.volume != @current.volume
38
62
  end
39
63
  end
40
64
 
41
- def compare(type, &blk)
65
+ def report(type, &blk)
42
66
  result = blk.call
43
- errors << type unless result
44
- result
67
+ errors << type if result
68
+ !result
45
69
  end
46
70
  end
47
71
  end
@@ -0,0 +1,13 @@
1
+ require 'sinatra/base'
2
+ require 'sinatra/synchrony'
3
+
4
+ class Radiodan::Sinatra < Sinatra::Base
5
+ register Sinatra::Synchrony
6
+
7
+ attr_reader :player
8
+
9
+ def initialize(player)
10
+ @player = player
11
+ super()
12
+ end
13
+ end
@@ -4,15 +4,18 @@ require 'active_support/core_ext/hash/indifferent_access'
4
4
  class Radiodan
5
5
  class Track
6
6
  class NoFileError < Exception; end
7
- extend Forwardable
7
+ extend Forwardable
8
+ attr_reader :attributes
8
9
  def_delegators :@attributes, :[]
9
10
 
11
+
10
12
  alias_method :eql?, :==
11
13
 
12
14
  def initialize(attributes={})
13
15
  @attributes = HashWithIndifferentAccess.new(attributes)
14
- raise NoFileError, 'No file given for track' \
15
- unless @attributes.include?(:file)
16
+ unless @attributes.has_key?(:file)
17
+ raise NoFileError, 'No file given for track'
18
+ end
16
19
  end
17
20
 
18
21
  def ==(other)
@@ -1,3 +1,3 @@
1
1
  class Radiodan
2
- VERSION = "0.0.4"
2
+ VERSION = "1.0.0"
3
3
  end
data/lib/radiodan.rb CHANGED
@@ -19,14 +19,13 @@ class Radiodan
19
19
  # keep player running on schedule
20
20
  raise "no player set" unless player.adapter?
21
21
 
22
+ stop_player_on_exit
23
+
22
24
  EM.synchrony do
23
25
  trap_signals!
26
+ EventMachine::Synchrony.next_tick { @builder.call_middleware! }
24
27
 
25
- EM::Synchrony.next_tick do
26
- @builder.call_middleware!
27
- end
28
-
29
- EM.now_and_every(seconds: 1) do
28
+ EM::Synchrony.add_periodic_timer(1) do
30
29
  logger.info "SYNC!"
31
30
  player.sync if player
32
31
  end
@@ -55,17 +54,18 @@ class Radiodan
55
54
  end
56
55
  end
57
56
 
57
+ def stop_player_on_exit
58
+ at_exit do
59
+ logger.info 'Stopping player'
60
+ stop
61
+ end
62
+ end
63
+
58
64
  def trap_signals!
59
65
  %w{INT TERM SIGHUP SIGINT SIGTERM}.each do |signal|
60
66
  Signal.trap(signal) do
61
67
  logger.info "Trapped #{signal}"
62
- EM::Synchrony.next_tick do
63
- begin
64
- stop
65
- ensure
66
- EM.stop
67
- end
68
- end
68
+ EM.stop
69
69
  end
70
70
  end
71
71
  end
data/radiodan.gemspec CHANGED
@@ -25,7 +25,10 @@ Gem::Specification.new do |gem|
25
25
  gem.add_dependency 'eventmachine', EM_VERSION
26
26
  gem.add_dependency 'em-synchrony', EM_VERSION
27
27
  gem.add_dependency 'em-http-request', EM_VERSION
28
- gem.add_dependency 'em-simple_telnet', '~> 0.0.6'
29
- gem.add_dependency 'active_support', '~> 3.0.0'
30
- gem.add_dependency 'i18n', '~> 0.6.4'
28
+ gem.add_dependency 'em-simple_telnet', '~> 0.0.6'
29
+ gem.add_dependency 'active_support', '~> 3.0.0'
30
+ gem.add_dependency 'i18n', '~> 0.6.4'
31
+ gem.add_dependency 'thin', '~> 1.5.1'
32
+ gem.add_dependency 'sinatra', '~> 1.4.2'
33
+ gem.add_dependency 'sinatra-synchrony', '~> 0.4.1'
31
34
  end
@@ -7,10 +7,10 @@ describe Radiodan::Builder do
7
7
  Radiodan::Player.stub(:new).and_return(@player)
8
8
  end
9
9
 
10
- it 'passes a playlist to the player' do
10
+ it 'passes a playlist to the correct middleware' do
11
11
  playlist = mock
12
12
 
13
- @player.should_receive(:playlist=).with(playlist)
13
+ Radiodan::Builder.any_instance.should_receive(:use).with(:playlist_to_start, playlist)
14
14
 
15
15
  builder = Radiodan::Builder.new do |b|
16
16
  b.playlist playlist
@@ -30,6 +30,27 @@ describe Radiodan::Builder do
30
30
  end
31
31
  end
32
32
 
33
+ describe 'logger' do
34
+ it 'sets a log output and level' do
35
+ builder = Radiodan::Builder.new do |b|
36
+ b.log '/dev/null', :fatal
37
+ end
38
+
39
+ Radiodan::Logging.level.should == Logger::FATAL
40
+ Radiodan::Logging.output.should == '/dev/null'
41
+ end
42
+
43
+ it "has an optional log level" do
44
+ old_level = Radiodan::Logging.level
45
+
46
+ builder = Radiodan::Builder.new do |b|
47
+ b.log '/dev/null'
48
+ end
49
+
50
+ Radiodan::Logging.level.should == old_level
51
+ end
52
+ end
53
+
33
54
  describe 'middleware' do
34
55
  it 'creates an instance of middleware and stores internally' do
35
56
  class Radiodan::MockMiddle; end
@@ -40,7 +61,7 @@ describe Radiodan::Builder do
40
61
  builder = Radiodan::Builder.new do |b|
41
62
  b.use :mock_middle, options
42
63
  end
43
-
64
+
44
65
  builder.middleware.size.should == 1
45
66
  builder.middleware.should include middleware
46
67
  end
@@ -2,7 +2,12 @@ require 'spec_helper'
2
2
  require 'logging'
3
3
 
4
4
  describe Radiodan::Logging do
5
- it 'exists' do
6
- subject.is_a?(Class)
5
+ it 'sets a log level' do
6
+ subject.level = :warn
7
+ class Test
8
+ include Radiodan::Logging
9
+ end
10
+
11
+ Test.new.logger.level.should == Logger::WARN
7
12
  end
8
13
  end
@@ -19,9 +19,12 @@ describe Radiodan::Player do
19
19
 
20
20
  context 'playlist' do
21
21
  it 'triggers a new playlist event' do
22
- playlist = mock
22
+ playlist = stub
23
+ adapter = mock(:player= => nil, :playlist => playlist)
23
24
 
25
+ subject.adapter = adapter
24
26
  subject.should_receive(:trigger_event).with(:playlist, playlist)
27
+ subject.should_receive(:trigger_event).with(:player_state, playlist)
25
28
  subject.playlist = playlist
26
29
  end
27
30
  end
@@ -43,14 +46,15 @@ describe Radiodan::Player do
43
46
  subject.sync.should == true
44
47
  end
45
48
 
46
- context 'sync error triggers events' do
49
+ context 'sync error triggers events:' do
47
50
  before :each do
48
51
  Radiodan::PlaylistSync.any_instance.stub(:sync?).and_return(false)
52
+ subject.should_receive(:trigger_event).with(:sync, subject.adapter.playlist)
49
53
  end
50
54
 
51
55
  it 'playback state' do
52
56
  Radiodan::PlaylistSync.any_instance.stub(:errors).and_return([:state])
53
- subject.playlist.stub(:state => :playing)
57
+ subject.adapter.playlist.stub(:state => :playing)
54
58
 
55
59
  subject.should_receive(:trigger_event).with(:play_state, :playing)
56
60
  subject.sync.should == false
@@ -58,21 +62,21 @@ describe Radiodan::Player do
58
62
 
59
63
  it 'playback mode' do
60
64
  Radiodan::PlaylistSync.any_instance.stub(:errors).and_return([:mode])
61
- subject.playlist.stub(:mode => :random)
65
+ subject.adapter.playlist.stub(:mode => :random)
62
66
 
63
67
  subject.should_receive(:trigger_event).with(:play_mode, :random)
64
68
  subject.sync.should == false
65
69
  end
66
70
 
67
71
  it 'playlist' do
68
- Radiodan::PlaylistSync.any_instance.stub(:errors).and_return([:playlist])
72
+ Radiodan::PlaylistSync.any_instance.stub(:errors).and_return([:new_tracks])
69
73
 
70
74
  playlist_content = stub
71
75
  subject.playlist.stub(:content => playlist_content)
72
76
 
73
77
  subject.should_receive(:trigger_event).with(:playlist, subject.playlist)
74
78
  subject.sync.should == false
75
- end
79
+ end
76
80
  end
77
81
  end
78
82
  end
@@ -3,7 +3,12 @@ require 'playlist'
3
3
 
4
4
  describe Radiodan::Playlist do
5
5
  describe 'default attributes' do
6
- it 'has a state of playing' do
6
+ it 'has a state of stop' do
7
+ subject.state.should == :stop
8
+ end
9
+
10
+ it 'has a state of play if there are tracks' do
11
+ subject.tracks << mock
7
12
  subject.state.should == :play
8
13
  end
9
14
 
@@ -33,9 +38,22 @@ describe Radiodan::Playlist do
33
38
  end
34
39
 
35
40
  describe 'playback state' do
36
- it 'can be set' do
37
- subject.state = :play
38
- subject.state.should == :play
41
+ it 'is always stop if playlist is empty' do
42
+ subject.empty?.should be_true
43
+
44
+ subject.state = :pause
45
+ subject.state.should == :stop
46
+
47
+ subject.tracks << mock
48
+
49
+ subject.empty?.should be_false
50
+ subject.state.should == :pause
51
+ end
52
+
53
+ it 'can be set if tracks are present' do
54
+ subject.tracks << mock
55
+ subject.state = :pause
56
+ subject.state.should == :pause
39
57
  end
40
58
 
41
59
  it 'cannot be set to an unknown state' do
@@ -71,6 +89,17 @@ describe Radiodan::Playlist do
71
89
  end
72
90
  end
73
91
 
92
+ describe 'random mode' do
93
+ it 'is off by default' do
94
+ subject.random?.should == false
95
+ end
96
+
97
+ it 'can be set' do
98
+ subject.mode = :random
99
+ subject.random?.should == true
100
+ end
101
+ end
102
+
74
103
  describe 'tracks' do
75
104
  it 'creates an array of tracks' do
76
105
  subject.tracks = 'x.mp3'
@@ -138,10 +167,23 @@ describe Radiodan::Playlist do
138
167
  subject.volume.should == 24
139
168
  end
140
169
 
141
- it 'has a legal range of 0-100' do
170
+ it 'has a legal range of -1-100' do
142
171
  expect { subject.volume = '999' }.to raise_error subject.class::VolumeError
143
172
  expect { subject.volume = -29 }.to raise_error subject.class::VolumeError
144
173
  subject.volume.should == 100
145
174
  end
146
175
  end
176
+
177
+ describe 'attributes' do
178
+ it 'should be well formed' do
179
+ expected = {:state=>:stop, :mode=>:sequential, :repeat=>false, :tracks=>[], :position=>0, :seek=>0.0, :volume=>100}
180
+ expect subject.attributes.should == expected
181
+ end
182
+
183
+ it 'should include track attributes' do
184
+ subject.tracks << Radiodan::Track.new(:file => 'dan.mp3')
185
+ expected = {:state=>:play, :mode=>:sequential, :repeat=>false, :tracks=>[{'file' => 'dan.mp3'}], :position=>0, :seek=>0.0, :volume=>100}
186
+ expect subject.attributes.should == expected
187
+ end
188
+ end
147
189
  end