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 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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in radiodan.gemspec
4
+ gemspec
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
@@ -0,0 +1,3 @@
1
+ * Add some goddamn tests, dammnit.
2
+ * Generate gem
3
+ * Move BBC Downloader into own radio player thing, using radio_dan as gem
@@ -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
@@ -0,0 +1,3 @@
1
+ class Radiodan
2
+ VERSION = "0.0.1"
3
+ 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: