radiodan 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,9 +1,8 @@
1
1
  require 'active_support'
2
2
  require 'active_support/core_ext/string'
3
3
 
4
- require 'radiodan/logging'
5
- require 'radiodan/player'
6
- require 'radiodan/state'
4
+ require 'logging'
5
+ require 'player'
7
6
 
8
7
  class Radiodan
9
8
  class Builder
@@ -13,19 +12,19 @@ class Builder
13
12
  @middleware = []
14
13
  @player = Player.new
15
14
 
16
- yield(self)
15
+ yield(self) if block_given?
17
16
  end
18
17
 
19
18
  def use(klass, *config)
20
- @middleware << register(klass, *config)
19
+ @middleware << register(klass, 'middleware', *config)
21
20
  end
22
21
 
23
22
  def adapter(klass, *config)
24
- @player.adapter = register(klass, *config)
23
+ player.adapter = register(klass, 'adapter', *config)
25
24
  end
26
25
 
27
- def state(options)
28
- @player.state = State.new(options) if @player
26
+ def playlist(new_playlist)
27
+ player.playlist = new_playlist if player
29
28
  end
30
29
 
31
30
  def log(log)
@@ -33,11 +32,11 @@ class Builder
33
32
  end
34
33
 
35
34
  def call_middleware!
36
- @middleware.each{ |m| m.call(@player) }
35
+ middleware.each{ |m| m.call(@player) }
37
36
  end
38
37
 
39
38
  private
40
- def register(klass, *config)
39
+ def register(klass, klass_type, *config)
41
40
  klass = klass.to_s
42
41
 
43
42
  begin
@@ -47,12 +46,12 @@ class Builder
47
46
  raise if klass_path
48
47
 
49
48
  # attempt to require from middleware
50
- klass_path = Pathname.new("#{File.dirname(__FILE__)}/middleware/#{klass.underscore}.rb")
49
+ klass_path = Pathname.new(File.join(File.dirname(__FILE__), klass_type, "#{klass.underscore}.rb"))
51
50
  require klass_path if klass_path.exist?
52
-
51
+
53
52
  retry
54
53
  end
55
-
54
+
56
55
  if config.empty?
57
56
  radio_klass.new
58
57
  else
@@ -2,18 +2,18 @@ require 'logger'
2
2
 
3
3
  class Radiodan
4
4
  module Logging
5
- @output = '/dev/null'
5
+ @@output = '/dev/null'
6
6
 
7
7
  def self.included(klass)
8
8
  klass.extend ClassMethods
9
9
  end
10
10
 
11
11
  def self.output=(output)
12
- @output = output
12
+ @@output = output
13
13
  end
14
14
 
15
15
  def self.output
16
- @output
16
+ @@output
17
17
  end
18
18
 
19
19
  def logger
@@ -5,7 +5,7 @@ class Panic
5
5
  def initialize(config)
6
6
  @panic = false
7
7
  @timeout = config.delete(:duration).to_i
8
- @state = State.new(config)
8
+ @playlist = Playlist.new(config)
9
9
  end
10
10
 
11
11
  def call(player)
@@ -25,11 +25,11 @@ class Panic
25
25
 
26
26
  @panic = true
27
27
 
28
- original_state = @player.state
28
+ original_state = @player.playlist
29
29
 
30
30
  Thread.new do
31
31
  logger.debug "panic for #{@timeout} seconds"
32
- @player.state = @state
32
+ @player.playlist = @playlist
33
33
  sleep(@timeout)
34
34
  return_to_state original_state
35
35
  end
@@ -40,7 +40,7 @@ class Panic
40
40
  def return_to_state(state)
41
41
  logger.debug "calming"
42
42
  @panic = false
43
- @player.state = state
43
+ @player.playlist = playlist
44
44
  end
45
45
  end
46
46
  end
@@ -1,15 +1,13 @@
1
- require 'radiodan/event_binding'
1
+ require 'logging'
2
+ require 'event_binding'
3
+ require 'playlist_sync'
2
4
 
3
5
  class Radiodan
4
6
  class Player
5
7
  include Logging
6
8
  include EventBinding
7
9
 
8
- attr_reader :adapter, :state
9
-
10
- def initialize
11
- @state = State.new(:playback => 'stopped')
12
- end
10
+ attr_reader :adapter, :playlist
13
11
 
14
12
  def adapter=(adapter)
15
13
  @adapter = adapter
@@ -20,11 +18,12 @@ class Player
20
18
  !adapter.nil?
21
19
  end
22
20
 
23
- def state=(new_state)
24
- @state = new_state
25
- trigger_event(:state, @state)
21
+ def playlist=(new_playlist)
22
+ @playlist = new_playlist
23
+ trigger_event(:playlist, @playlist)
24
+ # run sync to explicitly conform to new playlist?
26
25
 
27
- @state
26
+ @playlist
28
27
  end
29
28
 
30
29
  =begin
@@ -34,19 +33,34 @@ class Player
34
33
  makes changes required to keep them the same.
35
34
  =end
36
35
  def sync
37
- current_state = adapter.state
38
- expected_state = state
36
+ return false unless adapter?
39
37
 
40
- # playlist
41
- unless expected_state.content.files.include?(current_state.file)
42
- logger.debug "Expected: #{expected_state.content.files.first} Got: #{current_state.file}"
43
- trigger_event :playlist, expected_state.content
44
- end
38
+ current = adapter.playlist
39
+ expected = playlist
40
+
41
+ state = Radiodan::PlaylistSync.new expected, current
45
42
 
46
- # playback state
47
- unless expected_state.playback == current_state.state
48
- logger.debug "Expected: #{expected_state.playback} Got: #{current_state.state}"
49
- trigger_event expected_state.playback
43
+ if state.sync?
44
+ true
45
+ else
46
+ # playback state
47
+ if state.errors.include? :state
48
+ logger.debug "Expected: #{expected.state} Got: #{current.state}"
49
+ trigger_event :play_state, expected.state
50
+ end
51
+
52
+ if state.errors.include? :mode
53
+ logger.debug "Expected: #{expected.mode} Got: #{current.mode}"
54
+ trigger_event :play_mode, expected.mode
55
+ end
56
+
57
+ # playlist
58
+ if state.errors.include? :playlist
59
+ logger.debug "Expected: #{expected.current} Got: #{current.current}"
60
+ trigger_event :playlist, expected
61
+ end
62
+
63
+ false
50
64
  end
51
65
  end
52
66
  end
@@ -0,0 +1,112 @@
1
+ =begin
2
+ The playlist object defines the source of audio
3
+ for the player.
4
+
5
+ We cant return the name of a stored playlist from mpd: we might as well build it up
6
+ in memory.
7
+
8
+ Attributes:
9
+ name: name of playlist (optional?)
10
+ state: playing, stopped, paused
11
+ mode: sequential, random, resume
12
+ tracks: an array of URIs to play
13
+ position: song to resume play
14
+ seek: position to resume from (seconds)
15
+ =end
16
+ require 'forwardable'
17
+ require 'track'
18
+
19
+ class Radiodan
20
+ class Playlist
21
+ class StateError < Exception; end
22
+ class ModeError < Exception; end
23
+ class PositionError < Exception; end
24
+ class SeekError < Exception; end
25
+ class VolumeError < Exception; end
26
+
27
+ extend Forwardable
28
+
29
+ STATES = [:play, :stop, :pause]
30
+ MODES = [:sequential, :resume, :random]
31
+ attr_reader :state, :mode, :repeat, :tracks, :position, :seek, :volume
32
+ def_delegators :@tracks, :size
33
+
34
+ def initialize(options={})
35
+ self.state = options.fetch(:state, STATES.first)
36
+ self.mode = options.fetch(:mode, MODES.first)
37
+ self.repeat = options.fetch(:repeat, false)
38
+ self.tracks = options.fetch(:tracks, Array.new)
39
+ self.position = options.fetch(:position, 0)
40
+ self.seek = options.fetch(:seek, 0.0)
41
+ self.volume = options.fetch(:volume, 100)
42
+ end
43
+
44
+ def current
45
+ tracks[position]
46
+ end
47
+
48
+ def state=(new_state)
49
+ state = new_state.to_sym
50
+
51
+ if STATES.include? state
52
+ @state = state
53
+ else
54
+ raise StateError
55
+ end
56
+ end
57
+
58
+ def mode=(new_mode)
59
+ mode = new_mode.to_sym
60
+
61
+ if MODES.include? mode
62
+ @mode = mode
63
+ else
64
+ raise ModeError
65
+ end
66
+ end
67
+
68
+ def repeat=(new_repeat)
69
+ @repeat = (new_repeat === true)
70
+ end
71
+
72
+ def tracks=(new_tracks)
73
+ if new_tracks.is_a?(String)
74
+ new_tracks = Track.new(file: new_tracks)
75
+ end
76
+
77
+ @tracks = Array(new_tracks)
78
+ @position = 0
79
+ end
80
+
81
+ def position=(new_position)
82
+ begin
83
+ position = Integer(new_position)
84
+ raise ArgumentError if position > tracks.size
85
+ rescue ArgumentError
86
+ raise PositionError, "Item #{new_position} invalid for playlist size #{tracks.size}"
87
+ end
88
+
89
+ @position = position
90
+ end
91
+
92
+ def seek=(new_seek)
93
+ begin
94
+ @seek = Float(new_seek)
95
+ rescue ArgumentError
96
+ raise SeekError, "#{new_seek} invalid"
97
+ end
98
+ end
99
+
100
+ def volume=(new_volume)
101
+ begin
102
+ new_volume = Integer(new_volume)
103
+
104
+ raise ArgumentError if new_volume > 100 || new_volume < 0
105
+ rescue ArgumentError
106
+ raise VolumeError, "#{new_volume} not an integer 0-100"
107
+ end
108
+
109
+ @volume = new_volume
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,47 @@
1
+ class Radiodan
2
+ class PlaylistSync
3
+ class SyncError < Exception; end
4
+ attr_accessor :expected, :current
5
+ attr_reader :errors
6
+
7
+ def initialize(expected = nil, current = nil)
8
+ @expected = expected
9
+ @current = current
10
+ @errors = []
11
+ end
12
+
13
+ def sync?
14
+ prerequisites_check
15
+ compare_playback_state & compare_playback_mode & compare_playlist
16
+ end
17
+
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?
22
+ end
23
+
24
+ def compare_playback_state
25
+ # add rules about when this is ok to be out of sync
26
+ # e.g. sequential expected runs out of tracks and stops
27
+ compare(:state) { @expected.state == @current.state }
28
+ end
29
+
30
+ def compare_playback_mode
31
+ compare(:mode) { @expected.mode == @current.mode }
32
+ end
33
+
34
+ def compare_playlist
35
+ compare(:playlist) do
36
+ @expected.size == @current.size && \
37
+ @expected.tracks == @current.tracks
38
+ end
39
+ end
40
+
41
+ def compare(type, &blk)
42
+ result = blk.call
43
+ errors << type unless result
44
+ result
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,38 @@
1
+ require 'forwardable'
2
+ require 'active_support/core_ext/hash/indifferent_access'
3
+
4
+ class Radiodan
5
+ class Track
6
+ class NoFileError < Exception; end
7
+ extend Forwardable
8
+ def_delegators :@attributes, :[]
9
+
10
+ alias_method :eql?, :==
11
+
12
+ def initialize(attributes={})
13
+ @attributes = HashWithIndifferentAccess.new(attributes)
14
+ raise NoFileError, 'No file given for track' \
15
+ unless @attributes.include?(:file)
16
+ end
17
+
18
+ def ==(other)
19
+ self[:file] == other[:file]
20
+ end
21
+
22
+ def method_missing(method, *args, &block)
23
+ if @attributes.include?(method)
24
+ @attributes[method]
25
+ else
26
+ super
27
+ end
28
+ end
29
+
30
+ def respond_to?(method)
31
+ if @attributes.include?(method)
32
+ true
33
+ else
34
+ super
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,3 +1,3 @@
1
1
  class Radiodan
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -6,21 +6,26 @@ require 'radiodan/version'
6
6
  EM_VERSION = '~> 1.0.3'
7
7
 
8
8
  Gem::Specification.new do |gem|
9
- gem.name = "radiodan"
9
+ gem.name = 'radiodan'
10
10
  gem.version = Radiodan::VERSION
11
- gem.authors = ["Dan Nuttall"]
12
- gem.email = ["pixelblend@gmail.com"]
11
+ gem.authors = ['Dan Nuttall']
12
+ gem.email = ['pixelblend@gmail.com']
13
13
  gem.description = %q{Web-enabled radio that plays to my schedule.}
14
14
  gem.summary = %q{Web-enabled radio that plays to my schedule.}
15
- gem.homepage = "https://github.com/pixelblend/radiodan"
15
+ gem.homepage = 'https://github.com/pixelblend/radiodan'
16
16
 
17
17
  gem.files = `git ls-files`.split($/)
18
18
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
19
19
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
20
- gem.require_paths = ["lib"]
20
+ gem.require_paths = ['lib']
21
+ gem.add_development_dependency 'rake', '~> 10.1.0'
22
+ gem.add_development_dependency 'rspec', '~> 2.13.0'
23
+ gem.add_development_dependency 'guard-rspec', '~> 2.6.0'
24
+ gem.add_development_dependency 'terminal-notifier-guard', '~> 1.5.0'
21
25
  gem.add_dependency 'eventmachine', EM_VERSION
22
26
  gem.add_dependency 'em-synchrony', EM_VERSION
23
27
  gem.add_dependency 'em-http-request', EM_VERSION
24
28
  gem.add_dependency 'em-simple_telnet', '~> 0.0.6'
25
29
  gem.add_dependency 'active_support', '~> 3.0.0'
30
+ gem.add_dependency 'i18n', '~> 0.6.4'
26
31
  end
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+ require 'builder'
3
+
4
+ describe Radiodan::Builder do
5
+ before :each do
6
+ @player = mock
7
+ Radiodan::Player.stub(:new).and_return(@player)
8
+ end
9
+
10
+ it 'passes a playlist to the player' do
11
+ playlist = mock
12
+
13
+ @player.should_receive(:playlist=).with(playlist)
14
+
15
+ builder = Radiodan::Builder.new do |b|
16
+ b.playlist playlist
17
+ end
18
+ end
19
+
20
+ it 'passes an instance of an adapter class with options to the player' do
21
+ class Radiodan::MockAdapter; end
22
+ adapter, options = mock, mock
23
+
24
+ Radiodan::MockAdapter.should_receive(:new).with(options).and_return(adapter)
25
+
26
+ @player.should_receive(:adapter=).with(adapter)
27
+
28
+ builder = Radiodan::Builder.new do |b|
29
+ b.adapter :mock_adapter, options
30
+ end
31
+ end
32
+
33
+ describe 'middleware' do
34
+ it 'creates an instance of middleware and stores internally' do
35
+ class Radiodan::MockMiddle; end
36
+
37
+ options, middleware = mock, mock
38
+ Radiodan::MockMiddle.should_receive(:new).with(options).and_return(middleware)
39
+
40
+ builder = Radiodan::Builder.new do |b|
41
+ b.use :mock_middle, options
42
+ end
43
+
44
+ builder.middleware.size.should == 1
45
+ builder.middleware.should include middleware
46
+ end
47
+
48
+ it 'executes middleware, passing player instance' do
49
+ middleware = stub
50
+ middleware.should_receive(:call).with(@player)
51
+
52
+ builder = Radiodan::Builder.new
53
+ builder.should_receive(:middleware).and_return([middleware])
54
+ builder.call_middleware!
55
+ end
56
+ end
57
+ end