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