radiodan 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -11,13 +11,10 @@ spec/reports
11
11
  test/tmp
12
12
  test/version_tmp
13
13
 
14
- # YARD artifacts
15
- .yardoc
16
- _yardoc
17
- doc/
18
14
  .vagrant
19
15
 
20
16
  music/*.mp3
21
17
  playlists/*.m3u
22
18
  config.yml
23
19
  db/development.sqlite3
20
+ tmp
@@ -1,12 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- radiodan (0.0.1)
4
+ radiodan (0.0.2)
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
+ i18n (~> 0.6.4)
10
11
 
11
12
  GEM
12
13
  remote: https://rubygems.org/
@@ -15,7 +16,9 @@ GEM
15
16
  activesupport (= 3.0.0)
16
17
  activesupport (3.0.0)
17
18
  addressable (2.3.4)
19
+ coderay (1.0.9)
18
20
  cookiejar (0.3.0)
21
+ diff-lcs (1.2.4)
19
22
  em-http-request (1.0.3)
20
23
  addressable (>= 2.2.3)
21
24
  cookiejar
@@ -29,10 +32,53 @@ GEM
29
32
  em-synchrony (1.0.3)
30
33
  eventmachine (>= 1.0.0.beta.1)
31
34
  eventmachine (1.0.3)
35
+ ffi (1.8.1)
36
+ formatador (0.2.4)
37
+ guard (1.8.0)
38
+ formatador (>= 0.2.4)
39
+ listen (>= 1.0.0)
40
+ lumberjack (>= 1.0.2)
41
+ pry (>= 0.9.10)
42
+ thor (>= 0.14.6)
43
+ guard-rspec (2.6.0)
44
+ guard (>= 1.8)
45
+ rspec (~> 2.13)
32
46
  http_parser.rb (0.5.3)
47
+ i18n (0.6.4)
48
+ listen (1.0.3)
49
+ rb-fsevent (>= 0.9.3)
50
+ rb-inotify (>= 0.9)
51
+ rb-kqueue (>= 0.2)
52
+ lumberjack (1.0.3)
53
+ method_source (0.8.1)
54
+ pry (0.9.12.1)
55
+ coderay (~> 1.0.5)
56
+ method_source (~> 0.8)
57
+ slop (~> 3.4)
58
+ rake (10.1.0)
59
+ rb-fsevent (0.9.3)
60
+ rb-inotify (0.9.0)
61
+ ffi (>= 0.5.0)
62
+ rb-kqueue (0.2.0)
63
+ ffi (>= 0.5.0)
64
+ rspec (2.13.0)
65
+ rspec-core (~> 2.13.0)
66
+ rspec-expectations (~> 2.13.0)
67
+ rspec-mocks (~> 2.13.0)
68
+ rspec-core (2.13.1)
69
+ rspec-expectations (2.13.0)
70
+ diff-lcs (>= 1.1.3, < 2.0)
71
+ rspec-mocks (2.13.1)
72
+ slop (3.4.4)
73
+ terminal-notifier-guard (1.5.3)
74
+ thor (0.18.1)
33
75
 
34
76
  PLATFORMS
35
77
  ruby
36
78
 
37
79
  DEPENDENCIES
80
+ guard-rspec (~> 2.6.0)
38
81
  radiodan!
82
+ rake (~> 10.1.0)
83
+ rspec (~> 2.13.0)
84
+ terminal-notifier-guard (~> 1.5.0)
@@ -0,0 +1,9 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec' do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
9
+
data/Rakefile CHANGED
@@ -1 +1,7 @@
1
1
  require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
data/TODO CHANGED
@@ -1,3 +1,15 @@
1
- * Add some goddamn tests, dammnit.
2
- * Generate gem
3
- * Move BBC Downloader into own radio player thing, using radio_dan as gem
1
+ * Player
2
+ - explicitly set playlist when a new event is triggered, don't attempt to sync
3
+ - #sync reads state_sync methods
4
+ * #errors to determine which events to trigger
5
+ * MPD Adapter
6
+ - Figure out a way of testing interface with playlist object
7
+ * StateSync
8
+ - Attributes to sync:
9
+ + Position in playlist
10
+ + Seek position for current item in playlist
11
+ - Differentiate when a playlist
12
+ + is new (make this match exactly)
13
+ + is not (keep in bounds of acceptability)
14
+ * Panic Mode
15
+ - Add tests
@@ -0,0 +1,44 @@
1
+ State Management
2
+ ----------------
3
+
4
+ Two kinds of state:
5
+
6
+
7
+ 1. Requested state AKA a playlist
8
+ *"This is what I want the player to be doing"*
9
+
10
+ Includes:
11
+ * a Radiodan::Content object, defining a playlist of content
12
+ * a playback
13
+ * playing
14
+ * stopped
15
+ * paused?
16
+ * resume (play from position)
17
+
18
+ Examples:
19
+ * Play radio 1
20
+ * Play from this artist at random
21
+ * Resume play on this playlist from a defined position
22
+ * Don't play anything!
23
+
24
+ 2. Player feedback state
25
+ *"This is what the player is currently doing"*
26
+
27
+ Examples:
28
+ * Playing track x.mp3 at 1m15s
29
+ * Playing <URL> for 5 minutes
30
+
31
+ When the player syncs, we want to make sure the player state is within the parameters set by the request.
32
+
33
+ e.g. We don't expect the playlist to always be playing at a defined position, just that it resumed from there and that it is still playing the same set of podcasts.
34
+
35
+ You set expected state when you want the player to change direction.
36
+ Expected state defines how to respond to player state.
37
+
38
+ You want feedback info in order to populate feedback displays. When you ask the player what it's state is, you want this information.
39
+
40
+ radio = Radiodan.new
41
+
42
+ radio.playlist = Radiodan::Playlist.new(:playback => :playing, :content => :bbcradio1)
43
+ radio.state #=> <playing (radio1_url)>
44
+ radio.sync? #=> true
@@ -1,12 +1,12 @@
1
1
  require 'eventmachine'
2
2
  require 'em-synchrony'
3
3
 
4
- $: << './lib'
4
+ $: << File.dirname(__FILE__)+'/radiodan/'
5
5
 
6
6
  require 'em_additions'
7
- require 'radiodan/logging'
8
- require 'radiodan/builder'
9
- require 'radiodan/version'
7
+ require 'logging'
8
+ require 'builder'
9
+ require 'version'
10
10
 
11
11
  class Radiodan
12
12
  include Logging
@@ -22,7 +22,7 @@ class Radiodan
22
22
  EM.synchrony do
23
23
  trap_signals!
24
24
 
25
- EM.next_tick do
25
+ EM::Synchrony.next_tick do
26
26
  @builder.call_middleware!
27
27
  end
28
28
 
@@ -0,0 +1,85 @@
1
+ require 'forwardable'
2
+
3
+ require_relative './mpd/connection'
4
+ require_relative './mpd/playlist_parser'
5
+
6
+ class Radiodan
7
+ class MPD
8
+ include Logging
9
+ extend Forwardable
10
+
11
+ def_delegators :@connection, :cmd
12
+
13
+ COMMANDS = %w{stop pause clear play next previous}
14
+ attr_reader :player
15
+
16
+ def initialize(options={})
17
+ @connection = Connection.new(options)
18
+ end
19
+
20
+ def player=(player)
21
+ @player = player
22
+
23
+ # register typical player commands
24
+ COMMANDS.each do |command|
25
+ @player.register_event command do |data|
26
+ if data
27
+ self.send(command, data)
28
+ else
29
+ self.send(command)
30
+ end
31
+ end
32
+ end
33
+
34
+ # register new playlist events
35
+ @player.register_event :playlist do |playlist|
36
+ self.playlist = playlist
37
+ end
38
+ end
39
+
40
+ def playlist=(playlist)
41
+ # get rid of current playlist, stop playback
42
+ clear
43
+
44
+ if enqueue playlist
45
+ play playlist.position
46
+ else
47
+ raise "Cannot load playlist #{playlist}"
48
+ end
49
+ end
50
+
51
+ def enqueue(playlist)
52
+ playlist.tracks.each do |track|
53
+ cmd(%Q{add "#{track[:file]}"})
54
+ end
55
+ end
56
+
57
+ def play(song_number=nil)
58
+ cmd("play #{song_number}")
59
+ end
60
+
61
+ def playlist
62
+ status = cmd("status")
63
+ tracks = cmd("playlistinfo")
64
+
65
+ PlaylistParser.parse(status, tracks)
66
+ end
67
+
68
+ def respond_to?(method)
69
+ if COMMANDS.include?(method.to_s)
70
+ true
71
+ else
72
+ super
73
+ end
74
+ end
75
+
76
+ private
77
+ def method_missing(method, *args, &block)
78
+ if COMMANDS.include?(method.to_s)
79
+ cmd(method.to_s, *args, &block)
80
+ else
81
+ super
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,11 @@
1
+ class Radiodan::MPD
2
+ class Ack
3
+ FORMAT = /ACK \[(\d)+@(\d)+\] \{(.*)\} (.*)/
4
+ attr_accessor :error_id, :position, :command, :description
5
+
6
+ def intialize
7
+ matches = FORMAT.match(ack)
8
+ error_id, position, command, description = *matches[1..-1].join
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,42 @@
1
+ require 'em-simple_telnet'
2
+ require 'logging'
3
+
4
+ require_relative 'response'
5
+
6
+ class Radiodan
7
+ class MPD
8
+ class Connection
9
+ include Logging
10
+
11
+ def initialize(options={})
12
+ @port = options[:port] || 6600
13
+ @host = options[:host] || 'localhost'
14
+ end
15
+
16
+ def cmd(command, options={})
17
+ options = {match: /^(OK|ACK)/}.merge(options)
18
+ response = false
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
+ EM::P::SimpleTelnet.new(host: @host, port: @port, prompt: /^(OK|ACK)(.*)$/) do |host|
36
+ host.waitfor(/^OK MPD \d{1,2}\.\d{1,2}\.\d{1,2}$/)
37
+ yield(host)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,36 @@
1
+ require 'playlist'
2
+
3
+ class Radiodan
4
+ class MPD
5
+ module PlaylistParser
6
+ def self.parse(attributes={}, tracks=[])
7
+ options = parse_attributes(attributes)
8
+ options[:tracks] = parse_tracks(tracks)
9
+
10
+ Playlist.new(options)
11
+ end
12
+
13
+ private
14
+ def self.parse_attributes(attributes)
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
+
23
+ options
24
+ end
25
+
26
+ def self.parse_tracks(tracks)
27
+ p tracks.first.inspect
28
+ tracks.collect{ |t| Track.new(t) }
29
+ end
30
+
31
+ def self.parse_mode(attributes)
32
+ attributes['random'] == '1' ? :random : :sequential
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,99 @@
1
+ require_relative 'ack'
2
+
3
+ class Radiodan::MPD
4
+ class Response
5
+ attr_accessor :value, :string
6
+ alias_method :to_s, :string
7
+
8
+ MULTILINE_COMMANDS = %w{playlistinfo}
9
+
10
+ def initialize(response_string, command=nil)
11
+ @string = response_string
12
+ @command = command
13
+ end
14
+
15
+ def value
16
+ @value ||= parse(@string, @command)
17
+ end
18
+
19
+ def is_ack?
20
+ value.is_a?(Ack)
21
+ end
22
+
23
+ def method_missing(method, *args, &block)
24
+ if value.respond_to?(method)
25
+ value.send(method, *args, &block)
26
+ else
27
+ super
28
+ end
29
+ end
30
+
31
+ def respond_to?(method)
32
+ if value.respond_to?(method)
33
+ true
34
+ else
35
+ super
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # returns true, ACK or formatted values
42
+ def parse(response, command)
43
+ case
44
+ when response == 'OK'
45
+ true
46
+ when response =~ /^ACK/
47
+ parse_ack(response)
48
+ when response.split.size == 1
49
+ # set value -> value
50
+ Hash[*(response.split.*2)]
51
+ when MULTILINE_COMMANDS.include?(command)
52
+ # create array of hash values
53
+ parse_multiline(response)
54
+ else
55
+ split = split_response(response).flatten
56
+ Hash[*split]
57
+ end
58
+ end
59
+
60
+ def parse_ack(response)
61
+ ack = Ack.new(response)
62
+ logger.warn ack
63
+
64
+ ack
65
+ end
66
+
67
+ def parse_multiline(response)
68
+ multiline = []
69
+ values = {}
70
+
71
+ split_response(response) do |key, value|
72
+ if values.include?(key)
73
+ multiline << values
74
+ values = {}
75
+ end
76
+
77
+ values[key] = value
78
+ end
79
+
80
+ multiline << values
81
+ end
82
+
83
+ def split_response(response)
84
+ response = response.split("\n")
85
+ # remove first response: "OK"
86
+ response.pop
87
+
88
+ response.collect do |r|
89
+ split = r.split(':')
90
+ key = split.shift.strip
91
+ value = split.join(':').strip
92
+
93
+ yield(key, value) if block_given?
94
+
95
+ [key, value]
96
+ end
97
+ end
98
+ end
99
+ end