radiodan 0.0.4 → 1.0.0

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