radiodan 0.0.1 → 0.0.2

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