radiodan 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: