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.
- data/.gitignore +1 -4
- data/Gemfile.lock +47 -1
- data/Guardfile +9 -0
- data/Rakefile +6 -0
- data/TODO +15 -3
- data/doc/state.markdown +44 -0
- data/lib/radiodan.rb +5 -5
- data/lib/radiodan/adapter/mpd.rb +85 -0
- data/lib/radiodan/adapter/mpd/ack.rb +11 -0
- data/lib/radiodan/adapter/mpd/connection.rb +42 -0
- data/lib/radiodan/adapter/mpd/playlist_parser.rb +36 -0
- data/lib/radiodan/adapter/mpd/response.rb +99 -0
- data/lib/radiodan/builder.rb +12 -13
- data/lib/{em_additions.rb → radiodan/em_additions.rb} +0 -0
- data/lib/radiodan/logging.rb +3 -3
- data/lib/radiodan/middleware/panic.rb +4 -4
- data/lib/radiodan/player.rb +35 -21
- data/lib/radiodan/playlist.rb +112 -0
- data/lib/radiodan/playlist_sync.rb +47 -0
- data/lib/radiodan/track.rb +38 -0
- data/lib/radiodan/version.rb +1 -1
- data/radiodan.gemspec +10 -5
- data/spec/lib/builder_spec.rb +57 -0
- data/spec/lib/event_binding_spec.rb +8 -0
- data/spec/lib/logging_spec.rb +8 -0
- data/spec/lib/player_spec.rb +78 -0
- data/spec/lib/playlist_parser_spec.rb +23 -0
- data/spec/lib/playlist_spec.rb +147 -0
- data/spec/lib/playlist_sync_spec.rb +97 -0
- data/spec/lib/track_spec.rb +41 -0
- data/spec/spec_helper.rb +15 -0
- metadata +118 -8
- data/lib/radiodan/content.rb +0 -18
- data/lib/radiodan/middleware/mpd.rb +0 -145
- data/lib/radiodan/state.rb +0 -24
data/lib/radiodan/builder.rb
CHANGED
@@ -1,9 +1,8 @@
|
|
1
1
|
require 'active_support'
|
2
2
|
require 'active_support/core_ext/string'
|
3
3
|
|
4
|
-
require '
|
5
|
-
require '
|
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
|
-
|
23
|
+
player.adapter = register(klass, 'adapter', *config)
|
25
24
|
end
|
26
25
|
|
27
|
-
def
|
28
|
-
|
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
|
-
|
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(
|
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
|
File without changes
|
data/lib/radiodan/logging.rb
CHANGED
@@ -2,18 +2,18 @@ require 'logger'
|
|
2
2
|
|
3
3
|
class Radiodan
|
4
4
|
module Logging
|
5
|
-
|
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
|
-
|
12
|
+
@@output = output
|
13
13
|
end
|
14
14
|
|
15
15
|
def self.output
|
16
|
-
|
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
|
-
@
|
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.
|
28
|
+
original_state = @player.playlist
|
29
29
|
|
30
30
|
Thread.new do
|
31
31
|
logger.debug "panic for #{@timeout} seconds"
|
32
|
-
@player.
|
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.
|
43
|
+
@player.playlist = playlist
|
44
44
|
end
|
45
45
|
end
|
46
46
|
end
|
data/lib/radiodan/player.rb
CHANGED
@@ -1,15 +1,13 @@
|
|
1
|
-
require '
|
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, :
|
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
|
24
|
-
@
|
25
|
-
trigger_event(:
|
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
|
-
@
|
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
|
-
|
38
|
-
expected_state = state
|
36
|
+
return false unless adapter?
|
39
37
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
end
|
38
|
+
current = adapter.playlist
|
39
|
+
expected = playlist
|
40
|
+
|
41
|
+
state = Radiodan::PlaylistSync.new expected, current
|
45
42
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
data/lib/radiodan/version.rb
CHANGED
data/radiodan.gemspec
CHANGED
@@ -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 =
|
9
|
+
gem.name = 'radiodan'
|
10
10
|
gem.version = Radiodan::VERSION
|
11
|
-
gem.authors = [
|
12
|
-
gem.email = [
|
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 =
|
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 = [
|
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
|