radiodan 0.0.1 → 0.0.2

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