radiodan 0.0.1
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 +23 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +38 -0
- data/LICENSE.txt +22 -0
- data/README.md +30 -0
- data/Rakefile +1 -0
- data/TODO +3 -0
- data/lib/em_additions.rb +31 -0
- data/lib/radiodan/builder.rb +63 -0
- data/lib/radiodan/content.rb +18 -0
- data/lib/radiodan/event_binding.rb +49 -0
- data/lib/radiodan/logging.rb +38 -0
- data/lib/radiodan/middleware/mpd.rb +145 -0
- data/lib/radiodan/middleware/panic.rb +46 -0
- data/lib/radiodan/middleware/touch_file.rb +31 -0
- data/lib/radiodan/player.rb +53 -0
- data/lib/radiodan/state.rb +24 -0
- data/lib/radiodan/version.rb +3 -0
- data/lib/radiodan.rb +73 -0
- data/radiodan.gemspec +26 -0
- metadata +146 -0
data/.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
coverage
|
6
|
+
InstalledFiles
|
7
|
+
lib/bundler/man
|
8
|
+
pkg
|
9
|
+
rdoc
|
10
|
+
spec/reports
|
11
|
+
test/tmp
|
12
|
+
test/version_tmp
|
13
|
+
|
14
|
+
# YARD artifacts
|
15
|
+
.yardoc
|
16
|
+
_yardoc
|
17
|
+
doc/
|
18
|
+
.vagrant
|
19
|
+
|
20
|
+
music/*.mp3
|
21
|
+
playlists/*.m3u
|
22
|
+
config.yml
|
23
|
+
db/development.sqlite3
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
radiodan (0.0.1)
|
5
|
+
active_support (~> 3.0.0)
|
6
|
+
em-http-request (~> 1.0.3)
|
7
|
+
em-simple_telnet (~> 0.0.6)
|
8
|
+
em-synchrony (~> 1.0.3)
|
9
|
+
eventmachine (~> 1.0.3)
|
10
|
+
|
11
|
+
GEM
|
12
|
+
remote: https://rubygems.org/
|
13
|
+
specs:
|
14
|
+
active_support (3.0.0)
|
15
|
+
activesupport (= 3.0.0)
|
16
|
+
activesupport (3.0.0)
|
17
|
+
addressable (2.3.4)
|
18
|
+
cookiejar (0.3.0)
|
19
|
+
em-http-request (1.0.3)
|
20
|
+
addressable (>= 2.2.3)
|
21
|
+
cookiejar
|
22
|
+
em-socksify
|
23
|
+
eventmachine (>= 1.0.0.beta.4)
|
24
|
+
http_parser.rb (>= 0.5.3)
|
25
|
+
em-simple_telnet (0.0.9)
|
26
|
+
eventmachine (>= 1.0.0)
|
27
|
+
em-socksify (0.2.1)
|
28
|
+
eventmachine (>= 1.0.0.beta.4)
|
29
|
+
em-synchrony (1.0.3)
|
30
|
+
eventmachine (>= 1.0.0.beta.1)
|
31
|
+
eventmachine (1.0.3)
|
32
|
+
http_parser.rb (0.5.3)
|
33
|
+
|
34
|
+
PLATFORMS
|
35
|
+
ruby
|
36
|
+
|
37
|
+
DEPENDENCIES
|
38
|
+
radiodan!
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Dan Nuttall
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# Radiodan
|
2
|
+
|
3
|
+
Web-enabled radio that plays to my schedule.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'radiodan'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install radiodan
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
30
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/TODO
ADDED
data/lib/em_additions.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
=begin
|
2
|
+
Most periodic tasks need to be run both now
|
3
|
+
and after a specificed time period.
|
4
|
+
|
5
|
+
This Eventmachine helper does that. You can
|
6
|
+
specify the timeframe in hours, minutes or
|
7
|
+
seconds.
|
8
|
+
=end
|
9
|
+
|
10
|
+
module EventMachine
|
11
|
+
def self.now_and_every(period, &blk)
|
12
|
+
seconds = case
|
13
|
+
when period.respond_to?(:to_f)
|
14
|
+
period.to_f
|
15
|
+
when period.include?(:hours)
|
16
|
+
period[:hours]*60*60
|
17
|
+
when period.include?(:minutes)
|
18
|
+
period[:minutes]*60
|
19
|
+
else
|
20
|
+
period[:seconds]
|
21
|
+
end
|
22
|
+
|
23
|
+
EM::Synchrony.next_tick do
|
24
|
+
yield
|
25
|
+
end
|
26
|
+
|
27
|
+
EM::Synchrony.add_periodic_timer(seconds) do
|
28
|
+
yield
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/core_ext/string'
|
3
|
+
|
4
|
+
require 'radiodan/logging'
|
5
|
+
require 'radiodan/player'
|
6
|
+
require 'radiodan/state'
|
7
|
+
|
8
|
+
class Radiodan
|
9
|
+
class Builder
|
10
|
+
attr_reader :middleware, :player
|
11
|
+
|
12
|
+
def initialize(&blk)
|
13
|
+
@middleware = []
|
14
|
+
@player = Player.new
|
15
|
+
|
16
|
+
yield(self)
|
17
|
+
end
|
18
|
+
|
19
|
+
def use(klass, *config)
|
20
|
+
@middleware << register(klass, *config)
|
21
|
+
end
|
22
|
+
|
23
|
+
def adapter(klass, *config)
|
24
|
+
@player.adapter = register(klass, *config)
|
25
|
+
end
|
26
|
+
|
27
|
+
def state(options)
|
28
|
+
@player.state = State.new(options) if @player
|
29
|
+
end
|
30
|
+
|
31
|
+
def log(log)
|
32
|
+
Logging.output = log
|
33
|
+
end
|
34
|
+
|
35
|
+
def call_middleware!
|
36
|
+
@middleware.each{ |m| m.call(@player) }
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def register(klass, *config)
|
41
|
+
klass = klass.to_s
|
42
|
+
|
43
|
+
begin
|
44
|
+
radio_klass = Radiodan.const_get(klass.classify)
|
45
|
+
rescue NameError => e
|
46
|
+
klass_path ||= false
|
47
|
+
raise if klass_path
|
48
|
+
|
49
|
+
# attempt to require from middleware
|
50
|
+
klass_path = Pathname.new("#{File.dirname(__FILE__)}/middleware/#{klass.underscore}.rb")
|
51
|
+
require klass_path if klass_path.exist?
|
52
|
+
|
53
|
+
retry
|
54
|
+
end
|
55
|
+
|
56
|
+
if config.empty?
|
57
|
+
radio_klass.new
|
58
|
+
else
|
59
|
+
radio_klass.new(*config)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
=begin
|
2
|
+
The Content object defines the source of audio
|
3
|
+
for the player.
|
4
|
+
|
5
|
+
We cant return the name of a playlist from mpd: we might as well build it up
|
6
|
+
in memory.
|
7
|
+
|
8
|
+
Attributes:
|
9
|
+
type: playlist, a URL, or a single file
|
10
|
+
location: URI for the content
|
11
|
+
mode: sequential, random, resume
|
12
|
+
song_number: song to resume play
|
13
|
+
play_from: position to resume from (seconds)
|
14
|
+
=end
|
15
|
+
|
16
|
+
class Radiodan
|
17
|
+
class Content < Struct.new(:type, :name, :files, :mode, :song_number, :play_from); end
|
18
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class Radiodan
|
2
|
+
module EventBinding
|
3
|
+
def register_event(event, &blk)
|
4
|
+
logger.info "Registered event #{event}"
|
5
|
+
event = event.to_sym
|
6
|
+
event_bindings[event] << blk
|
7
|
+
|
8
|
+
true
|
9
|
+
end
|
10
|
+
|
11
|
+
def trigger_event(event, data=nil)
|
12
|
+
event = event.to_sym
|
13
|
+
bindings = event_bindings[event]
|
14
|
+
|
15
|
+
unless bindings
|
16
|
+
logger.error "Event #{event} triggered but not found"
|
17
|
+
end
|
18
|
+
|
19
|
+
bindings.each do |blk|
|
20
|
+
blk.call(data)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def events
|
25
|
+
event_bindings.keys.sort
|
26
|
+
end
|
27
|
+
|
28
|
+
def respond_to?(method)
|
29
|
+
if event_bindings.include?(method)
|
30
|
+
true
|
31
|
+
else
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
def event_bindings
|
38
|
+
@event_bindings ||= Hash.new{ |h, k| h[k] = [] }
|
39
|
+
end
|
40
|
+
|
41
|
+
def method_missing(method, *args, &block)
|
42
|
+
if event_bindings.include?(method)
|
43
|
+
trigger_event(method, *args)
|
44
|
+
else
|
45
|
+
super
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
class Radiodan
|
4
|
+
module Logging
|
5
|
+
@output = '/dev/null'
|
6
|
+
|
7
|
+
def self.included(klass)
|
8
|
+
klass.extend ClassMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.output=(output)
|
12
|
+
@output = output
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.output
|
16
|
+
@output
|
17
|
+
end
|
18
|
+
|
19
|
+
def logger
|
20
|
+
self.class.logger
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
@@logs = {}
|
25
|
+
|
26
|
+
def logger
|
27
|
+
unless @@logs.include? self.name
|
28
|
+
new_log = Logger.new(Logging.output)
|
29
|
+
new_log.progname = self.name
|
30
|
+
|
31
|
+
@@logs[self.name] = new_log
|
32
|
+
end
|
33
|
+
|
34
|
+
@@logs[self.name]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'em-simple_telnet'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
class Radiodan
|
5
|
+
class MPD
|
6
|
+
include Logging
|
7
|
+
COMMANDS = %w{stop pause clear play}
|
8
|
+
Ack = Struct.new(:error_id, :position, :command, :description)
|
9
|
+
class AckError < Exception; end
|
10
|
+
|
11
|
+
attr_reader :player
|
12
|
+
|
13
|
+
def initialize(options={})
|
14
|
+
@port = options[:port] || 6600
|
15
|
+
@host = options[:host] || 'localhost'
|
16
|
+
end
|
17
|
+
|
18
|
+
def player=(player)
|
19
|
+
@player = player
|
20
|
+
|
21
|
+
# register typical player commands
|
22
|
+
COMMANDS.each do |command|
|
23
|
+
@player.register_event command do |data|
|
24
|
+
if data
|
25
|
+
self.send(command, data)
|
26
|
+
else
|
27
|
+
self.send(command)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# register new playlist events
|
33
|
+
@player.register_event :playlist do |playlist|
|
34
|
+
self.playlist = playlist
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def playlist=(playlist)
|
39
|
+
# get rid of current playlist, stop playback
|
40
|
+
clear
|
41
|
+
|
42
|
+
if enqueue playlist
|
43
|
+
play playlist.song_number
|
44
|
+
else
|
45
|
+
raise "Cannot load playlist #{playlist}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def enqueue(playlist)
|
50
|
+
playlist.files.each do |file|
|
51
|
+
cmd(%Q{add "#{file}"})
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def play(song_number=nil)
|
56
|
+
cmd("play #{song_number}")
|
57
|
+
end
|
58
|
+
|
59
|
+
def state
|
60
|
+
@state = cmd("status")
|
61
|
+
playlist = cmd("playlistinfo")
|
62
|
+
|
63
|
+
unless playlist == true
|
64
|
+
@state.merge!(playlist)
|
65
|
+
end
|
66
|
+
|
67
|
+
OpenStruct.new(@state)
|
68
|
+
end
|
69
|
+
|
70
|
+
def respond_to?(method)
|
71
|
+
if COMMANDS.include?(method.to_s)
|
72
|
+
true
|
73
|
+
else
|
74
|
+
super
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
def method_missing(method, *args, &block)
|
80
|
+
if COMMANDS.include?(method.to_s)
|
81
|
+
cmd(method.to_s, *args, &block)
|
82
|
+
else
|
83
|
+
super
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def cmd(command, options={})
|
88
|
+
options = {match: /^(OK|ACK)/}.merge(options)
|
89
|
+
response = false
|
90
|
+
|
91
|
+
connect do |c|
|
92
|
+
begin
|
93
|
+
logger.debug command
|
94
|
+
response = c.cmd(command, options).strip
|
95
|
+
rescue Exception => e
|
96
|
+
logger.error "#{command}, #{options} - #{e.to_s}"
|
97
|
+
raise
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
format_response(response)
|
102
|
+
end
|
103
|
+
|
104
|
+
def connect(&blk)
|
105
|
+
EM::P::SimpleTelnet.new(host: @host, port: @port, prompt: /^(OK|ACK)(.*)$/) do |host|
|
106
|
+
host.waitfor(/^OK MPD \d{1,2}\.\d{1,2}\.\d{1,2}$/)
|
107
|
+
yield(host)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# returns true or hash of values
|
112
|
+
def format_response(response)
|
113
|
+
case
|
114
|
+
when response == 'OK'
|
115
|
+
true
|
116
|
+
when response =~ /^ACK/
|
117
|
+
ack = format_ack(response)
|
118
|
+
logger.warn ack
|
119
|
+
|
120
|
+
ack
|
121
|
+
when response.split.size == 1
|
122
|
+
# set value -> value
|
123
|
+
Hash[*(response.split.*2)]
|
124
|
+
else
|
125
|
+
response = response.split("\n")
|
126
|
+
# remove first response: "OK"
|
127
|
+
response.pop
|
128
|
+
|
129
|
+
split_response = response.collect do |r|
|
130
|
+
split = r.split(':')
|
131
|
+
key = split.shift
|
132
|
+
value = split.join(':')
|
133
|
+
[key.strip, value.strip]
|
134
|
+
end.flatten
|
135
|
+
|
136
|
+
Hash[*split_response]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def format_ack(ack)
|
141
|
+
matches = /ACK \[(\d)+@(\d)+\] \{(.*)\} (.*)/.match(ack)
|
142
|
+
AckError.new(*matches[1..-1].join)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class Radiodan
|
2
|
+
class Panic
|
3
|
+
include Logging
|
4
|
+
|
5
|
+
def initialize(config)
|
6
|
+
@panic = false
|
7
|
+
@timeout = config.delete(:duration).to_i
|
8
|
+
@state = State.new(config)
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(player)
|
12
|
+
@player = player
|
13
|
+
|
14
|
+
@player.register_event :panic do
|
15
|
+
panic!
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def panic?
|
20
|
+
@panic == true
|
21
|
+
end
|
22
|
+
|
23
|
+
def panic!
|
24
|
+
return true if panic?
|
25
|
+
|
26
|
+
@panic = true
|
27
|
+
|
28
|
+
original_state = @player.state
|
29
|
+
|
30
|
+
Thread.new do
|
31
|
+
logger.debug "panic for #{@timeout} seconds"
|
32
|
+
@player.state = @state
|
33
|
+
sleep(@timeout)
|
34
|
+
return_to_state original_state
|
35
|
+
end
|
36
|
+
|
37
|
+
@panic
|
38
|
+
end
|
39
|
+
|
40
|
+
def return_to_state(state)
|
41
|
+
logger.debug "calming"
|
42
|
+
@panic = false
|
43
|
+
@player.state = state
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
=begin
|
2
|
+
Controls the radio by touching files.
|
3
|
+
|
4
|
+
This class looks for matching files in
|
5
|
+
a given directory. When the file exists,
|
6
|
+
the corresponding event is triggered and
|
7
|
+
the file deleted.
|
8
|
+
=end
|
9
|
+
|
10
|
+
class Radiodan
|
11
|
+
class TouchFile
|
12
|
+
include Logging
|
13
|
+
def initialize(config)
|
14
|
+
@path = config[:dir]
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(player)
|
18
|
+
EM.now_and_every(0.5) do
|
19
|
+
player.events.each do |event|
|
20
|
+
file = event.to_s
|
21
|
+
p = Pathname.new(File.join(@path, file))
|
22
|
+
if p.exist?
|
23
|
+
logger.debug "Responding to file #{file}"
|
24
|
+
p.delete
|
25
|
+
player.trigger_event file
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'radiodan/event_binding'
|
2
|
+
|
3
|
+
class Radiodan
|
4
|
+
class Player
|
5
|
+
include Logging
|
6
|
+
include EventBinding
|
7
|
+
|
8
|
+
attr_reader :adapter, :state
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@state = State.new(:playback => 'stopped')
|
12
|
+
end
|
13
|
+
|
14
|
+
def adapter=(adapter)
|
15
|
+
@adapter = adapter
|
16
|
+
@adapter.player = self
|
17
|
+
end
|
18
|
+
|
19
|
+
def adapter?
|
20
|
+
!adapter.nil?
|
21
|
+
end
|
22
|
+
|
23
|
+
def state=(new_state)
|
24
|
+
@state = new_state
|
25
|
+
trigger_event(:state, @state)
|
26
|
+
|
27
|
+
@state
|
28
|
+
end
|
29
|
+
|
30
|
+
=begin
|
31
|
+
Sync checks the current status of the player.
|
32
|
+
Is it paused? Playing? What is it playing?
|
33
|
+
It compares the expected to actual statuses and
|
34
|
+
makes changes required to keep them the same.
|
35
|
+
=end
|
36
|
+
def sync
|
37
|
+
current_state = adapter.state
|
38
|
+
expected_state = state
|
39
|
+
|
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
|
45
|
+
|
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
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
=begin
|
2
|
+
The State object determines what the player
|
3
|
+
should be doing at any given moment.
|
4
|
+
|
5
|
+
Attributes:
|
6
|
+
playback: boolean
|
7
|
+
content: content object to be playing
|
8
|
+
|
9
|
+
It can be updated by any of the stimulus objects.
|
10
|
+
=end
|
11
|
+
|
12
|
+
require 'radiodan/content'
|
13
|
+
|
14
|
+
class Radiodan
|
15
|
+
class State
|
16
|
+
include Logging
|
17
|
+
attr_reader :playback, :content
|
18
|
+
|
19
|
+
def initialize(config={})
|
20
|
+
@playback = config[:playback] || 'play'
|
21
|
+
@content = config[:content]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/radiodan.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'em-synchrony'
|
3
|
+
|
4
|
+
$: << './lib'
|
5
|
+
|
6
|
+
require 'em_additions'
|
7
|
+
require 'radiodan/logging'
|
8
|
+
require 'radiodan/builder'
|
9
|
+
require 'radiodan/version'
|
10
|
+
|
11
|
+
class Radiodan
|
12
|
+
include Logging
|
13
|
+
|
14
|
+
def initialize(&blk)
|
15
|
+
@builder = Builder.new(&blk)
|
16
|
+
end
|
17
|
+
|
18
|
+
def start
|
19
|
+
# keep player running on schedule
|
20
|
+
raise "no player set" unless player.adapter?
|
21
|
+
|
22
|
+
EM.synchrony do
|
23
|
+
trap_signals!
|
24
|
+
|
25
|
+
EM.next_tick do
|
26
|
+
@builder.call_middleware!
|
27
|
+
end
|
28
|
+
|
29
|
+
EM.now_and_every(seconds: 1) do
|
30
|
+
logger.info "SYNC!"
|
31
|
+
player.sync if player
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def player
|
37
|
+
@builder.player
|
38
|
+
end
|
39
|
+
|
40
|
+
def respond_to?(method)
|
41
|
+
if player.respond_to? method
|
42
|
+
true
|
43
|
+
else
|
44
|
+
super
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def method_missing(method, *args, &block)
|
51
|
+
if player.respond_to? method
|
52
|
+
player.send method, *args, &block
|
53
|
+
else
|
54
|
+
super
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def trap_signals!
|
59
|
+
%w{INT TERM SIGHUP SIGINT SIGTERM}.each do |signal|
|
60
|
+
Signal.trap(signal) do
|
61
|
+
logger.info "Trapped #{signal}"
|
62
|
+
EM::Synchrony.next_tick do
|
63
|
+
begin
|
64
|
+
stop
|
65
|
+
ensure
|
66
|
+
EM.stop
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
data/radiodan.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'radiodan/version'
|
5
|
+
|
6
|
+
EM_VERSION = '~> 1.0.3'
|
7
|
+
|
8
|
+
Gem::Specification.new do |gem|
|
9
|
+
gem.name = "radiodan"
|
10
|
+
gem.version = Radiodan::VERSION
|
11
|
+
gem.authors = ["Dan Nuttall"]
|
12
|
+
gem.email = ["pixelblend@gmail.com"]
|
13
|
+
gem.description = %q{Web-enabled radio that plays to my schedule.}
|
14
|
+
gem.summary = %q{Web-enabled radio that plays to my schedule.}
|
15
|
+
gem.homepage = "https://github.com/pixelblend/radiodan"
|
16
|
+
|
17
|
+
gem.files = `git ls-files`.split($/)
|
18
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
19
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
20
|
+
gem.require_paths = ["lib"]
|
21
|
+
gem.add_dependency 'eventmachine', EM_VERSION
|
22
|
+
gem.add_dependency 'em-synchrony', EM_VERSION
|
23
|
+
gem.add_dependency 'em-http-request', EM_VERSION
|
24
|
+
gem.add_dependency 'em-simple_telnet', '~> 0.0.6'
|
25
|
+
gem.add_dependency 'active_support', '~> 3.0.0'
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: radiodan
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Dan Nuttall
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-05-03 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: eventmachine
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.0.3
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.0.3
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: em-synchrony
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 1.0.3
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.0.3
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: em-http-request
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.0.3
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.0.3
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: em-simple_telnet
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 0.0.6
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 0.0.6
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: active_support
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 3.0.0
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 3.0.0
|
94
|
+
description: Web-enabled radio that plays to my schedule.
|
95
|
+
email:
|
96
|
+
- pixelblend@gmail.com
|
97
|
+
executables: []
|
98
|
+
extensions: []
|
99
|
+
extra_rdoc_files: []
|
100
|
+
files:
|
101
|
+
- .gitignore
|
102
|
+
- Gemfile
|
103
|
+
- Gemfile.lock
|
104
|
+
- LICENSE.txt
|
105
|
+
- README.md
|
106
|
+
- Rakefile
|
107
|
+
- TODO
|
108
|
+
- lib/em_additions.rb
|
109
|
+
- lib/radiodan.rb
|
110
|
+
- lib/radiodan/builder.rb
|
111
|
+
- lib/radiodan/content.rb
|
112
|
+
- lib/radiodan/event_binding.rb
|
113
|
+
- lib/radiodan/logging.rb
|
114
|
+
- lib/radiodan/middleware/mpd.rb
|
115
|
+
- lib/radiodan/middleware/panic.rb
|
116
|
+
- lib/radiodan/middleware/touch_file.rb
|
117
|
+
- lib/radiodan/player.rb
|
118
|
+
- lib/radiodan/state.rb
|
119
|
+
- lib/radiodan/version.rb
|
120
|
+
- radiodan.gemspec
|
121
|
+
homepage: https://github.com/pixelblend/radiodan
|
122
|
+
licenses: []
|
123
|
+
post_install_message:
|
124
|
+
rdoc_options: []
|
125
|
+
require_paths:
|
126
|
+
- lib
|
127
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
128
|
+
none: false
|
129
|
+
requirements:
|
130
|
+
- - ! '>='
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
|
+
none: false
|
135
|
+
requirements:
|
136
|
+
- - ! '>='
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
requirements: []
|
140
|
+
rubyforge_project:
|
141
|
+
rubygems_version: 1.8.23
|
142
|
+
signing_key:
|
143
|
+
specification_version: 3
|
144
|
+
summary: Web-enabled radio that plays to my schedule.
|
145
|
+
test_files: []
|
146
|
+
has_rdoc:
|