radiodan 0.0.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 98308159d0c67a5989de555035862e73754604f0
4
+ data.tar.gz: 7d5e7f602adc3f0223524e97debd9f1a38280b7f
5
+ SHA512:
6
+ metadata.gz: 9fbdab2af2852649db310b85489318dc0e9c3ac88826223fa938369699463379e9a88a39310bdc2a81d8392126d5e42eb5cb782ece3a23808892daf1b1f3ce98
7
+ data.tar.gz: 55076df49a6d487c6b4f3dda9922e941e9f91c840375cc4d52c2a460aaf220f7395994b8d89b5c4c6a51bab2964ece92c40bb86c052fe2112a33052d460bc9b4
data/Gemfile.lock CHANGED
@@ -1,13 +1,16 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- radiodan (0.0.4)
4
+ radiodan (1.0.0)
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
10
  i18n (~> 0.6.4)
11
+ sinatra (~> 1.4.2)
12
+ sinatra-synchrony (~> 0.4.1)
13
+ thin (~> 1.5.1)
11
14
 
12
15
  GEM
13
16
  remote: https://rubygems.org/
@@ -18,6 +21,7 @@ GEM
18
21
  addressable (2.3.5)
19
22
  coderay (1.0.9)
20
23
  cookiejar (0.3.0)
24
+ daemons (1.1.9)
21
25
  diff-lcs (1.2.4)
22
26
  em-http-request (1.0.3)
23
27
  addressable (>= 2.2.3)
@@ -25,7 +29,8 @@ GEM
25
29
  em-socksify
26
30
  eventmachine (>= 1.0.0.beta.4)
27
31
  http_parser.rb (>= 0.5.3)
28
- em-simple_telnet (0.0.9)
32
+ em-resolv-replace (1.1.3)
33
+ em-simple_telnet (0.0.14)
29
34
  eventmachine (>= 1.0.0)
30
35
  em-socksify (0.3.0)
31
36
  eventmachine (>= 1.0.0.beta.4)
@@ -44,7 +49,7 @@ GEM
44
49
  guard (>= 1.8)
45
50
  rspec (~> 2.13)
46
51
  http_parser.rb (0.5.3)
47
- i18n (0.6.4)
52
+ i18n (0.6.5)
48
53
  listen (1.0.3)
49
54
  rb-fsevent (>= 0.9.3)
50
55
  rb-inotify (>= 0.9)
@@ -55,6 +60,10 @@ GEM
55
60
  coderay (~> 1.0.5)
56
61
  method_source (~> 0.8)
57
62
  slop (~> 3.4)
63
+ rack (1.5.2)
64
+ rack-fiber_pool (0.9.3)
65
+ rack-protection (1.5.1)
66
+ rack
58
67
  rake (10.1.0)
59
68
  rb-fsevent (0.9.3)
60
69
  rb-inotify (0.9.0)
@@ -69,9 +78,25 @@ GEM
69
78
  rspec-expectations (2.13.0)
70
79
  diff-lcs (>= 1.1.3, < 2.0)
71
80
  rspec-mocks (2.13.1)
81
+ sinatra (1.4.4)
82
+ rack (~> 1.4)
83
+ rack-protection (~> 1.4)
84
+ tilt (~> 1.3, >= 1.3.4)
85
+ sinatra-synchrony (0.4.1)
86
+ em-http-request (~> 1.0)
87
+ em-resolv-replace (~> 1.1)
88
+ em-synchrony (~> 1.0.1)
89
+ eventmachine (~> 1.0.0)
90
+ rack-fiber_pool (~> 0.9)
91
+ sinatra (~> 1.0)
72
92
  slop (3.4.4)
73
93
  terminal-notifier-guard (1.5.3)
94
+ thin (1.5.1)
95
+ daemons (>= 1.0.9)
96
+ eventmachine (>= 0.12.6)
97
+ rack (>= 1.0.0)
74
98
  thor (0.18.1)
99
+ tilt (1.4.1)
75
100
 
76
101
  PLATFORMS
77
102
  ruby
data/TODO CHANGED
@@ -1,10 +1,13 @@
1
1
  * Player
2
2
  - #sync determines which events to trigger under which circumstances
3
- - Set random / repeat flags in MPD
4
- - Resume playlists from last position / seek point
5
- * Search for music in library, return playlist / array of tracks
3
+ * Playlist
4
+ - Resume mode updates seek mode on syc
6
5
  * MPD Adapter
7
6
  - Figure out a way of testing interface with playlist object
7
+ * MPD::Response
8
+ - Add tests
8
9
  * Panic Mode
9
10
  - Add tests
10
- * Remove eventmachine?
11
+ * Event binding
12
+ - Replace event_binding with EM::Channel?
13
+ - Add a :all event, call block every time any event is triggered.
@@ -1,11 +1,11 @@
1
1
  class Radiodan::MPD
2
2
  class Ack
3
3
  FORMAT = /ACK \[(\d)+@(\d)+\] \{(.*)\} (.*)/
4
- attr_accessor :error_id, :position, :command, :description
4
+ attr_reader :error_id, :position, :command, :description
5
5
 
6
- def intialize
6
+ def initialize(ack)
7
7
  matches = FORMAT.match(ack)
8
- error_id, position, command, description = *matches[1..-1].join
8
+ @error_id, @position, @command, @description = *matches[1..-1]
9
9
  end
10
10
  end
11
11
  end
@@ -15,27 +15,16 @@ class Connection
15
15
 
16
16
  def cmd(command, options={})
17
17
  options = {match: /^(OK|ACK)/}.merge(options)
18
- response = false
18
+ response = nil
19
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
20
  EM::P::SimpleTelnet.new(host: @host, port: @port, prompt: /^(OK|ACK)(.*)$/) do |host|
36
21
  host.waitfor(/^OK MPD \d{1,2}\.\d{1,2}\.\d{1,2}$/)
37
- yield(host)
22
+ logger.debug command
23
+ result = host.cmd(command, options).strip
24
+ response = Response.new(result, command)
38
25
  end
26
+
27
+ response
39
28
  end
40
29
  end
41
30
  end
@@ -13,18 +13,25 @@ module PlaylistParser
13
13
  private
14
14
  def self.parse_attributes(attributes)
15
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
16
 
23
- options
17
+ begin
18
+ options[:state] = attributes['state'].to_sym
19
+ options[:mode] = parse_mode(attributes)
20
+ options[:repeat] = attributes['repeat'] == '1'
21
+ options[:position] = attributes['song'].to_i
22
+ options[:seek] = attributes['elapsed'].to_f
23
+ options[:volume] = attributes['volume'].to_i
24
+ ensure
25
+ return options
26
+ end
24
27
  end
25
28
 
26
29
  def self.parse_tracks(tracks)
27
- tracks.collect{ |t| Track.new(t) } rescue []
30
+ if tracks.respond_to?(:collect)
31
+ tracks.collect{ |t| Track.new(t) }
32
+ else
33
+ []
34
+ end
28
35
  end
29
36
 
30
37
  def self.parse_mode(attributes)
@@ -1,23 +1,32 @@
1
+ require 'logging'
1
2
  require_relative 'ack'
2
3
 
3
- class Radiodan::MPD
4
+ class Radiodan
5
+ class MPD
4
6
  class Response
7
+ include Logging
5
8
  attr_accessor :value, :string
6
9
  alias_method :to_s, :string
7
10
 
8
- MULTILINE_COMMANDS = %w{playlistinfo}
11
+ MULTILINE_COMMANDS = %w{playlistinfo search find}
9
12
 
10
13
  def initialize(response_string, command=nil)
11
- @string = response_string
14
+ @string = response_string
12
15
  @command = command
16
+ @value = parse(@string, @command)
17
+
18
+ if ack?
19
+ logger.error "ACK #{@command}, #{@value.inspect}"
20
+ raise AckError, @value.description
21
+ end
13
22
  end
14
23
 
15
- def value
16
- @value ||= parse(@string, @command)
24
+ def ack?
25
+ value.is_a?(Ack)
17
26
  end
18
27
 
19
- def is_ack?
20
- value.is_a?(Ack)
28
+ def ==(other)
29
+ self.value == other
21
30
  end
22
31
 
23
32
  def method_missing(method, *args, &block)
@@ -44,11 +53,11 @@ class Radiodan::MPD
44
53
  when response == 'OK'
45
54
  true
46
55
  when response =~ /^ACK/
47
- parse_ack(response)
56
+ Ack.new(response)
48
57
  when response.split.size == 1
49
58
  # set value -> value
50
59
  Hash[*(response.split.*2)]
51
- when MULTILINE_COMMANDS.include?(command)
60
+ when MULTILINE_COMMANDS.include?(command.split.first)
52
61
  # create array of hash values
53
62
  parse_multiline(response)
54
63
  else
@@ -57,23 +66,16 @@ class Radiodan::MPD
57
66
  end
58
67
  end
59
68
 
60
- def parse_ack(response)
61
- ack = Ack.new(response)
62
- logger.warn ack
63
-
64
- ack
65
- end
66
-
67
69
  def parse_multiline(response)
68
70
  multiline = []
69
71
  values = {}
70
72
 
71
73
  split_response(response) do |key, value|
72
- if values.include?(key)
74
+ if key == 'file' && values.has_key?('file')
73
75
  multiline << values
74
76
  values = {}
75
77
  end
76
-
78
+
77
79
  values[key] = value
78
80
  end
79
81
 
@@ -82,10 +84,11 @@ class Radiodan::MPD
82
84
 
83
85
  def split_response(response)
84
86
  response = response.split("\n")
85
- # remove first response: "OK"
86
- response.pop
87
87
 
88
88
  response.collect do |r|
89
+ # remove "OK" responses
90
+ next if r == 'OK'
91
+
89
92
  split = r.split(':')
90
93
  key = split.shift.strip
91
94
  value = split.join(':').strip
@@ -93,7 +96,8 @@ class Radiodan::MPD
93
96
  yield(key, value) if block_given?
94
97
 
95
98
  [key, value]
96
- end
99
+ end.compact
97
100
  end
98
101
  end
99
102
  end
103
+ end
@@ -5,22 +5,26 @@ require_relative './mpd/playlist_parser'
5
5
 
6
6
  class Radiodan
7
7
  class MPD
8
+ class AckError < Exception; end
8
9
  include Logging
9
10
  extend Forwardable
10
11
 
11
12
  def_delegators :@connection, :cmd
12
13
 
13
- COMMANDS = %w{stop pause clear play next previous}
14
+ COMMANDS = %w{stop pause clear play next previous enqueue search update}
15
+ SEARCH_SCOPE = %w{artist album title track name genre date composer performer comment disc filename any}
14
16
  attr_reader :player
15
17
 
16
18
  def initialize(options={})
17
19
  @connection = Connection.new(options)
20
+ @connection.cmd('clear')
21
+ @connection.cmd('update')
18
22
  end
19
-
23
+
20
24
  def player=(player)
21
25
  @player = player
22
26
 
23
- # register typical player commands
27
+ # register available player commands
24
28
  COMMANDS.each do |command|
25
29
  @player.register_event command do |data|
26
30
  if data
@@ -35,21 +39,47 @@ class MPD
35
39
  @player.register_event :playlist do |playlist|
36
40
  self.playlist = playlist
37
41
  end
42
+
43
+ # register volume changes
44
+ @player.register_event :volume do |volume|
45
+ self.volume = volume
46
+ end
38
47
  end
39
48
 
40
49
  def playlist=(playlist)
41
50
  # get rid of current playlist, stop playback
42
51
  clear
52
+
53
+ # set random & repeat
54
+ cmd(%Q{random #{boolean_to_s(playlist.random?)}})
55
+ cmd(%Q{repeat #{boolean_to_s(playlist.repeat?)}})
56
+
57
+ # set volume
58
+ begin
59
+ volume = playlist.volume
60
+ rescue AckError => e
61
+ logger.error e.msg
62
+ end
63
+
64
+ if playlist.empty?
65
+ logger.error 'Playlist empty, nothing to do'
66
+ return false
67
+ end
43
68
 
44
- if enqueue playlist
45
- play playlist.position
69
+ if enqueue playlist.tracks
70
+ # set for seek position (will play from seek point)
71
+ cmd(%Q{seek #{playlist.position} #{Integer(playlist.seek)}})
46
72
  else
47
73
  raise "Cannot load playlist #{playlist}"
48
74
  end
49
75
  end
76
+
77
+ def volume=(new_volume)
78
+ cmd(%Q{setvol #{Integer(new_volume)}})
79
+ end
50
80
 
51
- def enqueue(playlist)
52
- playlist.tracks.each do |track|
81
+ def enqueue(tracks)
82
+ tracks.each do |track|
53
83
  cmd(%Q{add "#{track[:file]}"})
54
84
  end
55
85
  end
@@ -59,10 +89,61 @@ class MPD
59
89
  end
60
90
 
61
91
  def playlist
62
- status = cmd("status")
63
- tracks = cmd("playlistinfo")
92
+ begin
93
+ status = cmd('status')
94
+ tracks = cmd('playlistinfo')
64
95
 
65
- PlaylistParser.parse(status, tracks)
96
+ playlist = PlaylistParser.parse(status, tracks)
97
+ playlist
98
+ rescue Playlist::StateError,
99
+ Playlist::ModeError,
100
+ Playlist::PositionError,
101
+ Playlist::SeekError,
102
+ Playlist::VolumeError => e
103
+ logger.warn("Playlist parsing raised error: #{e}")
104
+ retry
105
+ end
106
+ end
107
+
108
+ # search :artist => "Bob Marley", :exact => true
109
+ # search :filename => './bob.mp3'
110
+ # search "Bob Marley"
111
+ def search(args)
112
+ if args.nil?
113
+ logger.error 'no query found'
114
+ return []
115
+ end
116
+
117
+ if args.to_s == args
118
+ args = {'any' => args}
119
+ end
120
+
121
+ if args.delete(:exact)
122
+ command = 'find'
123
+ else
124
+ command = 'search'
125
+ end
126
+
127
+ if args.keys.size > 1
128
+ raise 'Too many arguments for search'
129
+ end
130
+
131
+ scope = args.keys.first.to_s
132
+ term = args.values.first
133
+
134
+ unless SEARCH_SCOPE.include?(scope)
135
+ raise "Unknown search scope #{scope}"
136
+ end
137
+
138
+ cmd_string = %Q{#{command} #{scope} "#{term}"}
139
+
140
+ tracks = cmd(cmd_string)
141
+
142
+ if tracks.respond_to?(:collect)
143
+ tracks.collect { |t| Track.new(t) }
144
+ else
145
+ []
146
+ end
66
147
  end
67
148
 
68
149
  def respond_to?(method)
@@ -73,7 +154,6 @@ class MPD
73
154
  end
74
155
  end
75
156
 
76
- private
77
157
  def method_missing(method, *args, &block)
78
158
  if COMMANDS.include?(method.to_s)
79
159
  cmd(method.to_s, *args, &block)
@@ -81,5 +161,10 @@ class MPD
81
161
  super
82
162
  end
83
163
  end
164
+
165
+ private
166
+ def boolean_to_s(bool)
167
+ bool == true ? '1' : '0'
168
+ end
84
169
  end
85
170
  end
@@ -8,30 +8,31 @@ require 'player'
8
8
  class Radiodan
9
9
  class Builder
10
10
  attr_reader :middleware, :player
11
-
11
+
12
12
  def initialize(&blk)
13
13
  @middleware = []
14
14
  @player = Player.new
15
-
15
+
16
16
  yield(self) if block_given?
17
17
  end
18
18
 
19
19
  def use(klass, *config)
20
20
  @middleware << register(klass, 'middleware', *config)
21
21
  end
22
-
22
+
23
23
  def adapter(klass, *config)
24
24
  player.adapter = register(klass, 'adapter', *config)
25
25
  end
26
-
26
+
27
27
  def playlist(new_playlist)
28
- player.playlist = new_playlist if player
28
+ use :playlist_to_start, new_playlist
29
29
  end
30
-
31
- def log(log)
30
+
31
+ def log(log, level=nil)
32
32
  Logging.output = log
33
+ Logging.level = level unless level.nil?
33
34
  end
34
-
35
+
35
36
  def call_middleware!
36
37
  middleware.each{ |m| m.call(@player) }
37
38
  end
@@ -45,14 +46,14 @@ class Builder
45
46
  rescue NameError => e
46
47
  klass_path ||= false
47
48
  raise if klass_path
48
-
49
+
49
50
  # attempt to require from given path
50
51
  klass_path = Pathname.new(File.join(File.dirname(__FILE__), klass_type, "#{klass.underscore}.rb"))
51
52
  require klass_path if klass_path.exist?
52
53
 
53
54
  retry
54
55
  end
55
-
56
+
56
57
  if config.empty?
57
58
  radio_klass.new
58
59
  else
@@ -7,24 +7,26 @@
7
7
  seconds.
8
8
  =end
9
9
 
10
- module EventMachine
10
+ module EventMachine::Synchrony
11
11
  def self.now_and_every(period, &blk)
12
12
  seconds = case
13
13
  when period.respond_to?(:to_f)
14
- period.to_f
14
+ period
15
15
  when period.include?(:hours)
16
16
  period[:hours]*60*60
17
17
  when period.include?(:minutes)
18
18
  period[:minutes]*60
19
19
  else
20
20
  period[:seconds]
21
- end
21
+ end.to_f
22
+
23
+ raise "Period must be higher than 0" if period == 0.0
22
24
 
23
- EM::Synchrony.next_tick do
25
+ next_tick do
24
26
  yield
25
27
  end
26
28
 
27
- EM::Synchrony.add_periodic_timer(seconds) do
29
+ add_periodic_timer(seconds) do
28
30
  yield
29
31
  end
30
32
  end
@@ -16,8 +16,11 @@ module EventBinding
16
16
  logger.error "Event #{event} triggered but not found"
17
17
  end
18
18
 
19
+ # also, run the events bound to :all, no matter the event
20
+ bindings += event_bindings[:all]
21
+
19
22
  bindings.each do |blk|
20
- EM.next_tick { blk.call(data) }
23
+ EM::Synchrony.next_tick { blk.call(data) }
21
24
  end
22
25
  end
23
26
 
@@ -3,15 +3,17 @@ require 'logger'
3
3
  class Radiodan
4
4
  module Logging
5
5
  @@output = '/dev/null'
6
-
6
+ @@level = :DEBUG
7
+
7
8
  def self.included(klass)
8
9
  klass.extend ClassMethods
9
10
  end
10
-
11
+
11
12
  def self.output=(output)
12
13
  @@output = output
14
+ STDOUT.sync = true if @@output == STDOUT
13
15
  end
14
-
16
+
15
17
  def self.output
16
18
  @@output
17
19
  end
@@ -20,17 +22,26 @@ module Logging
20
22
  self.class.logger
21
23
  end
22
24
 
23
- module ClassMethods
25
+ def self.level=(level)
26
+ @@level = Logger.const_get(level.to_sym.upcase)
27
+ end
28
+
29
+ def self.level
30
+ @@level
31
+ end
32
+
33
+ module ClassMethods
24
34
  @@logs = {}
25
-
35
+
26
36
  def logger
27
37
  unless @@logs.include? self.name
28
38
  new_log = Logger.new(Logging.output)
29
39
  new_log.progname = self.name
30
-
40
+ new_log.level = Logging.level
41
+
31
42
  @@logs[self.name] = new_log
32
43
  end
33
-
44
+
34
45
  @@logs[self.name]
35
46
  end
36
47
  end
@@ -26,18 +26,12 @@ class Panic
26
26
  @panic = true
27
27
 
28
28
  original_state = @player.playlist
29
-
30
- EM.defer \
31
- proc {
32
- logger.debug "panic for #{@timeout} seconds"
33
- @player.playlist = @playlist
34
- sleep(@timeout)
35
- },
36
- proc {
37
- return_to_state original_state
38
- }
39
-
40
- @panic
29
+ logger.debug "panic for #{@timeout} seconds"
30
+ @player.playlist = @playlist
31
+
32
+ EM::Synchrony.add_timer(@timeout) do
33
+ return_to_state original_state
34
+ end
41
35
  end
42
36
 
43
37
  def return_to_state(playlist)
@@ -0,0 +1,11 @@
1
+ class Radiodan
2
+ class PlaylistToStart
3
+ def initialize(*config)
4
+ @playlist = config.shift
5
+ end
6
+
7
+ def call(player)
8
+ player.playlist = @playlist
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ class Radiodan
2
+ class TogglePlaylist
3
+ include Logging
4
+
5
+ def initialize(main_playlist, toggle_playlist)
6
+ @playlists = [main_playlist, toggle_playlist]
7
+ end
8
+
9
+ def call(player)
10
+ @player = player
11
+ @player.playlist = @playlists.shift
12
+
13
+ @player.register_event :toggle do
14
+ logger.info "Toggling playlist"
15
+ @player.playlist, @playlists = @playlists.shift, [@player.playlist]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -15,7 +15,7 @@ class TouchFile
15
15
  end
16
16
 
17
17
  def call(player)
18
- EM.now_and_every(0.5) do
18
+ EM::Synchrony.now_and_every(0.5) do
19
19
  player.events.each do |event|
20
20
  file = event.to_s
21
21
  p = Pathname.new(File.join(@path, file))