radiodan 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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/.gitignore
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
radiodan (0.0.
|
4
|
+
radiodan (0.0.2)
|
5
5
|
active_support (~> 3.0.0)
|
6
6
|
em-http-request (~> 1.0.3)
|
7
7
|
em-simple_telnet (~> 0.0.6)
|
8
8
|
em-synchrony (~> 1.0.3)
|
9
9
|
eventmachine (~> 1.0.3)
|
10
|
+
i18n (~> 0.6.4)
|
10
11
|
|
11
12
|
GEM
|
12
13
|
remote: https://rubygems.org/
|
@@ -15,7 +16,9 @@ GEM
|
|
15
16
|
activesupport (= 3.0.0)
|
16
17
|
activesupport (3.0.0)
|
17
18
|
addressable (2.3.4)
|
19
|
+
coderay (1.0.9)
|
18
20
|
cookiejar (0.3.0)
|
21
|
+
diff-lcs (1.2.4)
|
19
22
|
em-http-request (1.0.3)
|
20
23
|
addressable (>= 2.2.3)
|
21
24
|
cookiejar
|
@@ -29,10 +32,53 @@ GEM
|
|
29
32
|
em-synchrony (1.0.3)
|
30
33
|
eventmachine (>= 1.0.0.beta.1)
|
31
34
|
eventmachine (1.0.3)
|
35
|
+
ffi (1.8.1)
|
36
|
+
formatador (0.2.4)
|
37
|
+
guard (1.8.0)
|
38
|
+
formatador (>= 0.2.4)
|
39
|
+
listen (>= 1.0.0)
|
40
|
+
lumberjack (>= 1.0.2)
|
41
|
+
pry (>= 0.9.10)
|
42
|
+
thor (>= 0.14.6)
|
43
|
+
guard-rspec (2.6.0)
|
44
|
+
guard (>= 1.8)
|
45
|
+
rspec (~> 2.13)
|
32
46
|
http_parser.rb (0.5.3)
|
47
|
+
i18n (0.6.4)
|
48
|
+
listen (1.0.3)
|
49
|
+
rb-fsevent (>= 0.9.3)
|
50
|
+
rb-inotify (>= 0.9)
|
51
|
+
rb-kqueue (>= 0.2)
|
52
|
+
lumberjack (1.0.3)
|
53
|
+
method_source (0.8.1)
|
54
|
+
pry (0.9.12.1)
|
55
|
+
coderay (~> 1.0.5)
|
56
|
+
method_source (~> 0.8)
|
57
|
+
slop (~> 3.4)
|
58
|
+
rake (10.1.0)
|
59
|
+
rb-fsevent (0.9.3)
|
60
|
+
rb-inotify (0.9.0)
|
61
|
+
ffi (>= 0.5.0)
|
62
|
+
rb-kqueue (0.2.0)
|
63
|
+
ffi (>= 0.5.0)
|
64
|
+
rspec (2.13.0)
|
65
|
+
rspec-core (~> 2.13.0)
|
66
|
+
rspec-expectations (~> 2.13.0)
|
67
|
+
rspec-mocks (~> 2.13.0)
|
68
|
+
rspec-core (2.13.1)
|
69
|
+
rspec-expectations (2.13.0)
|
70
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
71
|
+
rspec-mocks (2.13.1)
|
72
|
+
slop (3.4.4)
|
73
|
+
terminal-notifier-guard (1.5.3)
|
74
|
+
thor (0.18.1)
|
33
75
|
|
34
76
|
PLATFORMS
|
35
77
|
ruby
|
36
78
|
|
37
79
|
DEPENDENCIES
|
80
|
+
guard-rspec (~> 2.6.0)
|
38
81
|
radiodan!
|
82
|
+
rake (~> 10.1.0)
|
83
|
+
rspec (~> 2.13.0)
|
84
|
+
terminal-notifier-guard (~> 1.5.0)
|
data/Guardfile
ADDED
data/Rakefile
CHANGED
data/TODO
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
-
*
|
2
|
-
|
3
|
-
|
1
|
+
* Player
|
2
|
+
- explicitly set playlist when a new event is triggered, don't attempt to sync
|
3
|
+
- #sync reads state_sync methods
|
4
|
+
* #errors to determine which events to trigger
|
5
|
+
* MPD Adapter
|
6
|
+
- Figure out a way of testing interface with playlist object
|
7
|
+
* StateSync
|
8
|
+
- Attributes to sync:
|
9
|
+
+ Position in playlist
|
10
|
+
+ Seek position for current item in playlist
|
11
|
+
- Differentiate when a playlist
|
12
|
+
+ is new (make this match exactly)
|
13
|
+
+ is not (keep in bounds of acceptability)
|
14
|
+
* Panic Mode
|
15
|
+
- Add tests
|
data/doc/state.markdown
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
State Management
|
2
|
+
----------------
|
3
|
+
|
4
|
+
Two kinds of state:
|
5
|
+
|
6
|
+
|
7
|
+
1. Requested state AKA a playlist
|
8
|
+
*"This is what I want the player to be doing"*
|
9
|
+
|
10
|
+
Includes:
|
11
|
+
* a Radiodan::Content object, defining a playlist of content
|
12
|
+
* a playback
|
13
|
+
* playing
|
14
|
+
* stopped
|
15
|
+
* paused?
|
16
|
+
* resume (play from position)
|
17
|
+
|
18
|
+
Examples:
|
19
|
+
* Play radio 1
|
20
|
+
* Play from this artist at random
|
21
|
+
* Resume play on this playlist from a defined position
|
22
|
+
* Don't play anything!
|
23
|
+
|
24
|
+
2. Player feedback state
|
25
|
+
*"This is what the player is currently doing"*
|
26
|
+
|
27
|
+
Examples:
|
28
|
+
* Playing track x.mp3 at 1m15s
|
29
|
+
* Playing <URL> for 5 minutes
|
30
|
+
|
31
|
+
When the player syncs, we want to make sure the player state is within the parameters set by the request.
|
32
|
+
|
33
|
+
e.g. We don't expect the playlist to always be playing at a defined position, just that it resumed from there and that it is still playing the same set of podcasts.
|
34
|
+
|
35
|
+
You set expected state when you want the player to change direction.
|
36
|
+
Expected state defines how to respond to player state.
|
37
|
+
|
38
|
+
You want feedback info in order to populate feedback displays. When you ask the player what it's state is, you want this information.
|
39
|
+
|
40
|
+
radio = Radiodan.new
|
41
|
+
|
42
|
+
radio.playlist = Radiodan::Playlist.new(:playback => :playing, :content => :bbcradio1)
|
43
|
+
radio.state #=> <playing (radio1_url)>
|
44
|
+
radio.sync? #=> true
|
data/lib/radiodan.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
require 'eventmachine'
|
2
2
|
require 'em-synchrony'
|
3
3
|
|
4
|
-
$: << '
|
4
|
+
$: << File.dirname(__FILE__)+'/radiodan/'
|
5
5
|
|
6
6
|
require 'em_additions'
|
7
|
-
require '
|
8
|
-
require '
|
9
|
-
require '
|
7
|
+
require 'logging'
|
8
|
+
require 'builder'
|
9
|
+
require 'version'
|
10
10
|
|
11
11
|
class Radiodan
|
12
12
|
include Logging
|
@@ -22,7 +22,7 @@ class Radiodan
|
|
22
22
|
EM.synchrony do
|
23
23
|
trap_signals!
|
24
24
|
|
25
|
-
EM.next_tick do
|
25
|
+
EM::Synchrony.next_tick do
|
26
26
|
@builder.call_middleware!
|
27
27
|
end
|
28
28
|
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
require_relative './mpd/connection'
|
4
|
+
require_relative './mpd/playlist_parser'
|
5
|
+
|
6
|
+
class Radiodan
|
7
|
+
class MPD
|
8
|
+
include Logging
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
def_delegators :@connection, :cmd
|
12
|
+
|
13
|
+
COMMANDS = %w{stop pause clear play next previous}
|
14
|
+
attr_reader :player
|
15
|
+
|
16
|
+
def initialize(options={})
|
17
|
+
@connection = Connection.new(options)
|
18
|
+
end
|
19
|
+
|
20
|
+
def player=(player)
|
21
|
+
@player = player
|
22
|
+
|
23
|
+
# register typical player commands
|
24
|
+
COMMANDS.each do |command|
|
25
|
+
@player.register_event command do |data|
|
26
|
+
if data
|
27
|
+
self.send(command, data)
|
28
|
+
else
|
29
|
+
self.send(command)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# register new playlist events
|
35
|
+
@player.register_event :playlist do |playlist|
|
36
|
+
self.playlist = playlist
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def playlist=(playlist)
|
41
|
+
# get rid of current playlist, stop playback
|
42
|
+
clear
|
43
|
+
|
44
|
+
if enqueue playlist
|
45
|
+
play playlist.position
|
46
|
+
else
|
47
|
+
raise "Cannot load playlist #{playlist}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def enqueue(playlist)
|
52
|
+
playlist.tracks.each do |track|
|
53
|
+
cmd(%Q{add "#{track[:file]}"})
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def play(song_number=nil)
|
58
|
+
cmd("play #{song_number}")
|
59
|
+
end
|
60
|
+
|
61
|
+
def playlist
|
62
|
+
status = cmd("status")
|
63
|
+
tracks = cmd("playlistinfo")
|
64
|
+
|
65
|
+
PlaylistParser.parse(status, tracks)
|
66
|
+
end
|
67
|
+
|
68
|
+
def respond_to?(method)
|
69
|
+
if COMMANDS.include?(method.to_s)
|
70
|
+
true
|
71
|
+
else
|
72
|
+
super
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
def method_missing(method, *args, &block)
|
78
|
+
if COMMANDS.include?(method.to_s)
|
79
|
+
cmd(method.to_s, *args, &block)
|
80
|
+
else
|
81
|
+
super
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class Radiodan::MPD
|
2
|
+
class Ack
|
3
|
+
FORMAT = /ACK \[(\d)+@(\d)+\] \{(.*)\} (.*)/
|
4
|
+
attr_accessor :error_id, :position, :command, :description
|
5
|
+
|
6
|
+
def intialize
|
7
|
+
matches = FORMAT.match(ack)
|
8
|
+
error_id, position, command, description = *matches[1..-1].join
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'em-simple_telnet'
|
2
|
+
require 'logging'
|
3
|
+
|
4
|
+
require_relative 'response'
|
5
|
+
|
6
|
+
class Radiodan
|
7
|
+
class MPD
|
8
|
+
class Connection
|
9
|
+
include Logging
|
10
|
+
|
11
|
+
def initialize(options={})
|
12
|
+
@port = options[:port] || 6600
|
13
|
+
@host = options[:host] || 'localhost'
|
14
|
+
end
|
15
|
+
|
16
|
+
def cmd(command, options={})
|
17
|
+
options = {match: /^(OK|ACK)/}.merge(options)
|
18
|
+
response = false
|
19
|
+
|
20
|
+
connect do |c|
|
21
|
+
begin
|
22
|
+
logger.debug command
|
23
|
+
response = c.cmd(command, options).strip
|
24
|
+
rescue Exception => e
|
25
|
+
logger.error "#{command}, #{options} - #{e.to_s}"
|
26
|
+
raise
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
Response.new(response, command)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def connect(&blk)
|
35
|
+
EM::P::SimpleTelnet.new(host: @host, port: @port, prompt: /^(OK|ACK)(.*)$/) do |host|
|
36
|
+
host.waitfor(/^OK MPD \d{1,2}\.\d{1,2}\.\d{1,2}$/)
|
37
|
+
yield(host)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'playlist'
|
2
|
+
|
3
|
+
class Radiodan
|
4
|
+
class MPD
|
5
|
+
module PlaylistParser
|
6
|
+
def self.parse(attributes={}, tracks=[])
|
7
|
+
options = parse_attributes(attributes)
|
8
|
+
options[:tracks] = parse_tracks(tracks)
|
9
|
+
|
10
|
+
Playlist.new(options)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
def self.parse_attributes(attributes)
|
15
|
+
options = {}
|
16
|
+
options[:state] = attributes['state'].to_sym
|
17
|
+
options[:mode] = parse_mode(attributes)
|
18
|
+
options[:repeat] = attributes['repeat'] == '1'
|
19
|
+
options[:position] = attributes['song'].to_i
|
20
|
+
options[:seek] = attributes['elapsed'].to_f
|
21
|
+
options[:volume] = attributes['volume'].to_i
|
22
|
+
|
23
|
+
options
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.parse_tracks(tracks)
|
27
|
+
p tracks.first.inspect
|
28
|
+
tracks.collect{ |t| Track.new(t) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.parse_mode(attributes)
|
32
|
+
attributes['random'] == '1' ? :random : :sequential
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require_relative 'ack'
|
2
|
+
|
3
|
+
class Radiodan::MPD
|
4
|
+
class Response
|
5
|
+
attr_accessor :value, :string
|
6
|
+
alias_method :to_s, :string
|
7
|
+
|
8
|
+
MULTILINE_COMMANDS = %w{playlistinfo}
|
9
|
+
|
10
|
+
def initialize(response_string, command=nil)
|
11
|
+
@string = response_string
|
12
|
+
@command = command
|
13
|
+
end
|
14
|
+
|
15
|
+
def value
|
16
|
+
@value ||= parse(@string, @command)
|
17
|
+
end
|
18
|
+
|
19
|
+
def is_ack?
|
20
|
+
value.is_a?(Ack)
|
21
|
+
end
|
22
|
+
|
23
|
+
def method_missing(method, *args, &block)
|
24
|
+
if value.respond_to?(method)
|
25
|
+
value.send(method, *args, &block)
|
26
|
+
else
|
27
|
+
super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def respond_to?(method)
|
32
|
+
if value.respond_to?(method)
|
33
|
+
true
|
34
|
+
else
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# returns true, ACK or formatted values
|
42
|
+
def parse(response, command)
|
43
|
+
case
|
44
|
+
when response == 'OK'
|
45
|
+
true
|
46
|
+
when response =~ /^ACK/
|
47
|
+
parse_ack(response)
|
48
|
+
when response.split.size == 1
|
49
|
+
# set value -> value
|
50
|
+
Hash[*(response.split.*2)]
|
51
|
+
when MULTILINE_COMMANDS.include?(command)
|
52
|
+
# create array of hash values
|
53
|
+
parse_multiline(response)
|
54
|
+
else
|
55
|
+
split = split_response(response).flatten
|
56
|
+
Hash[*split]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_ack(response)
|
61
|
+
ack = Ack.new(response)
|
62
|
+
logger.warn ack
|
63
|
+
|
64
|
+
ack
|
65
|
+
end
|
66
|
+
|
67
|
+
def parse_multiline(response)
|
68
|
+
multiline = []
|
69
|
+
values = {}
|
70
|
+
|
71
|
+
split_response(response) do |key, value|
|
72
|
+
if values.include?(key)
|
73
|
+
multiline << values
|
74
|
+
values = {}
|
75
|
+
end
|
76
|
+
|
77
|
+
values[key] = value
|
78
|
+
end
|
79
|
+
|
80
|
+
multiline << values
|
81
|
+
end
|
82
|
+
|
83
|
+
def split_response(response)
|
84
|
+
response = response.split("\n")
|
85
|
+
# remove first response: "OK"
|
86
|
+
response.pop
|
87
|
+
|
88
|
+
response.collect do |r|
|
89
|
+
split = r.split(':')
|
90
|
+
key = split.shift.strip
|
91
|
+
value = split.join(':').strip
|
92
|
+
|
93
|
+
yield(key, value) if block_given?
|
94
|
+
|
95
|
+
[key, value]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|