rmpd 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in rmpd.gemspec
4
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require "rspec/core/rake_task"
5
+ #require "cucumber"
6
+ #require "cucumber/rake/task"
7
+
8
+
9
+ RSpec::Core::RakeTask.new
10
+ # Cucumber::Rake::Task.new("cucumber") do
11
+ # puts "No cucumber features defined yet."
12
+ # end
13
+
14
+ task :default => [:spec] #, :cucumber]
data/lib/rmpd.rb ADDED
@@ -0,0 +1,25 @@
1
+ require File.join(File.dirname(__FILE__), "rmpd/config")
2
+ require File.join(File.dirname(__FILE__), "rmpd/commands")
3
+ require File.join(File.dirname(__FILE__), "rmpd/connection")
4
+ require File.join(File.dirname(__FILE__), "rmpd/multi_response")
5
+ require File.join(File.dirname(__FILE__), "rmpd/response")
6
+
7
+ module Rmpd
8
+ ACK_RE = /^ACK \[(\d+)@(\d+)\] \{([^}]*)\} (.*)$/
9
+ OK_RE = /^OK.*$/
10
+
11
+ class MpdError < Exception ; end
12
+
13
+ class MpdConnRefusedError < MpdError ; end
14
+
15
+ class MpdAckError < MpdError
16
+ def initialize(regex_match)
17
+ @error, @command_list_num, @current_command, @message = regex_match.values_at(1..-1)
18
+ end
19
+
20
+ def to_s
21
+ "ACK [#{@error}@#{@command_list_num}] {#{@current_command}} #{@message}"
22
+ end
23
+ end
24
+
25
+ end
@@ -0,0 +1,50 @@
1
+ require File.join(File.dirname(__FILE__), "commands/generators")
2
+ require File.join(File.dirname(__FILE__), "commands/admin")
3
+ require File.join(File.dirname(__FILE__), "commands/database")
4
+ require File.join(File.dirname(__FILE__), "commands/miscellaneous")
5
+ require File.join(File.dirname(__FILE__), "commands/playback")
6
+ require File.join(File.dirname(__FILE__), "commands/playlist")
7
+
8
+ module Rmpd
9
+ module Commands
10
+
11
+ private
12
+
13
+ def read_responses(regexp=/(^file: )/i)
14
+ read_response(MultiResponse, regexp)
15
+ end
16
+
17
+ def receive_server_response
18
+ lines = []
19
+ while lines << @socket.readline do
20
+ puts "recv: #{lines.last.strip} (#{OK_RE === lines.last})" if $DEBUG
21
+ case lines.last
22
+ when ACK_RE, OK_RE: break
23
+ end
24
+ end
25
+ return lines.join
26
+ end
27
+
28
+ def send_command(command, *args)
29
+ tries = 0
30
+
31
+ if $DEBUG
32
+ a = command == "password" ? args.map{|x| "*" * 8} : args
33
+ Kernel.puts "send: #{command.strip} #{a.join(" ")}".strip
34
+ end
35
+
36
+ begin
37
+ connect
38
+ @socket.puts("#{command} #{args.join(" ")}".strip)
39
+ rescue Errno::EPIPE, EOFError
40
+ @socket.close
41
+ if (tries += 1) < 5
42
+ retry
43
+ else
44
+ raise MpdError.new("Retry count exceeded")
45
+ end
46
+ end
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,21 @@
1
+ module Rmpd
2
+ module Commands
3
+
4
+ complex_command :outputs, :regexp => /(^outputid: )/i
5
+ complex_command :tagtypes, :regexp => /(^tagtype: )/i, :min_version => [0, 13, 0]
6
+
7
+ simple_command :disableoutput
8
+ simple_command :enableoutput
9
+ simple_command :update
10
+
11
+ def kill
12
+ send_command("kill")
13
+ @socket.close
14
+ end
15
+
16
+ alias_method :disable_output, :disableoutput
17
+ alias_method :tag_types, :tagtypes
18
+ alias_method :enable_output, :enableoutput
19
+
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ module Rmpd
2
+ module Commands
3
+
4
+ complex_command :count, :regexp => /(^songs: )/i
5
+ complex_command :find, :regexp => /(^file: )/i
6
+ complex_command :list, :regexp => /(^[^:]+: )/i
7
+ complex_command :listall, :regexp => /(^[^:]+: )/i
8
+ complex_command :listallinfo, :regexp => /(^(directory|file): )/i
9
+ complex_command :lsinfo, :regexp => /(^(directory|file): )/i
10
+ complex_command :search, :regexp => /(^file: )/i
11
+
12
+ alias_method :list_all, :listall
13
+ alias_method :list_all_info, :listallinfo
14
+ alias_method :ls_info, :lsinfo
15
+
16
+ end
17
+ end
@@ -0,0 +1,34 @@
1
+ module Rmpd
2
+ module Commands
3
+
4
+ private
5
+
6
+ def self.simple_command(name, args={})
7
+ block = lambda do |*a|
8
+ if args.include?(:min_version)
9
+ server_version_at_least(*args[:min_version])
10
+ end
11
+ send_command(name.to_s.gsub(/^_*/, ""), *quote(a))
12
+ read_response unless @in_command_list
13
+ end
14
+ send(:define_method, name, &block)
15
+ end
16
+
17
+ def self.complex_command(name, args={})
18
+ args = {:regexp => /(^file: )/i}.merge(args)
19
+ block = lambda do |*a|
20
+ if args.include?(:min_version)
21
+ server_version_at_least(*args[:min_version])
22
+ end
23
+ send_command(name.to_s.gsub(/^_*/, ""), *quote(a))
24
+ read_responses(args[:regexp]) unless @in_command_list
25
+ end
26
+ send(:define_method, name, &block)
27
+ end
28
+
29
+ def quote(args)
30
+ args.collect {|arg| "\"#{arg.to_s.gsub(/"/, "\\\"")}\""}
31
+ end
32
+ end
33
+
34
+ end
@@ -0,0 +1,33 @@
1
+ module Rmpd
2
+ module Commands
3
+
4
+ complex_command :commands, :regexp => /(^command: )/i
5
+ simple_command :notcommands, :regexp => /(^command: )/i
6
+
7
+ simple_command :clearerror
8
+ simple_command :idle
9
+ simple_command :noidle
10
+ simple_command :password
11
+ simple_command :ping
12
+ simple_command :stats
13
+ simple_command :status
14
+
15
+ def close
16
+ send_command("close")
17
+ @socket.close
18
+ end
19
+
20
+ alias_method :clear_error, :clearerror
21
+ alias_method :not_commands, :notcommands
22
+
23
+ def command_list
24
+ send_command("command_list_begin")
25
+ @in_command_list = true
26
+ yield self
27
+ send_command("command_list_end")
28
+ read_response
29
+ ensure
30
+ @in_command_list = false
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,30 @@
1
+ module Rmpd
2
+ module Commands
3
+
4
+ simple_command :_volume
5
+ simple_command :crossfade
6
+ simple_command :next
7
+ simple_command :pause
8
+ simple_command :play
9
+ simple_command :playid
10
+ simple_command :previous
11
+ simple_command :random
12
+ simple_command :repeat
13
+ simple_command :seek
14
+ simple_command :seekid
15
+ simple_command :setvol
16
+ simple_command :stop
17
+
18
+ def volume(*args)
19
+ $stderr.puts "volume is deprecated, use setvol instead"
20
+ _volume(*args)
21
+ end
22
+
23
+ alias_method :cross_fade, :crossfade
24
+ alias_method :play_id, :playid
25
+ alias_method :prev, :previous
26
+ alias_method :seek_id, :seekid
27
+ alias_method :set_vol, :setvol
28
+
29
+ end
30
+ end
@@ -0,0 +1,62 @@
1
+ module Rmpd
2
+ module Commands
3
+
4
+ complex_command :listplaylist, :min_version => [0, 13, 0]
5
+ complex_command :listplaylistinfo, :min_version => [0, 12, 0]
6
+ complex_command :playlistfind, :min_version => [0, 13, 0]
7
+ complex_command :playlistid
8
+ complex_command :playlistinfo
9
+ complex_command :playlistsearch, :min_version => [0, 13, 0]
10
+ complex_command :plchanges
11
+ complex_command :plchangesposid, :regexp => /(^Id: )/i
12
+
13
+ simple_command :_playlist
14
+ simple_command :add # The docs on the wiki don't line up with empirical
15
+ # results using 0.13.0
16
+ simple_command :clear
17
+ simple_command :currentsong
18
+ simple_command :delete
19
+ simple_command :deleteid
20
+ simple_command :load
21
+ simple_command :move
22
+ simple_command :moveid
23
+ simple_command :playlistadd, :min_version => [0, 13, 0]
24
+ simple_command :playlistclear, :min_version => [0, 13, 0]
25
+ simple_command :playlistdelete, :min_version => [0, 13, 0]
26
+ simple_command :playlistmove, :min_version => [0, 13, 0]
27
+ simple_command :rename
28
+ simple_command :rm
29
+ simple_command :save
30
+ simple_command :shuffle
31
+ simple_command :swap
32
+ simple_command :swapid
33
+
34
+
35
+ # must be a file only, cannot use with a director
36
+ def addid(path, pos=nil)
37
+ # pos is only for r7153+, but what version is that?
38
+ server_version_at_least(0, 14, 0) if pos
39
+ send_command("addid #{quote(path)} #{pos.to_s}")
40
+ read_response
41
+ end
42
+
43
+ alias_method :add_id, :addid
44
+ alias_method :current_song, :currentsong
45
+ alias_method :delete_id, :deleteid
46
+ alias_method :list_playlist, :listplaylist
47
+ alias_method :list_playlist_info, :listplaylistinfo
48
+ alias_method :move_id, :moveid
49
+ alias_method :pl_changes, :plchanges
50
+ alias_method :pl_changes_pos_id, :plchangesposid
51
+ alias_method :playlist_add, :playlistadd
52
+ alias_method :playlist_clear, :playlistclear
53
+ alias_method :playlist_delete, :playlistdelete
54
+ alias_method :playlist_find, :playlistfind
55
+ alias_method :playlist_id, :playlistid
56
+ alias_method :playlist_info, :playlistinfo
57
+ alias_method :playlist_move, :playlistmove
58
+ alias_method :playlist_search, :playlistsearch
59
+ alias_method :swap_id, :swapid
60
+
61
+ end
62
+ end
@@ -0,0 +1,77 @@
1
+ require "pathname"
2
+ require "yaml"
3
+
4
+ module Rmpd
5
+
6
+ class Config
7
+
8
+ DEFAULT_HOSTNAME = "localhost"
9
+ DEFAULT_PORT = 6600
10
+ DEFAULT_PASSWORD = nil
11
+
12
+ attr_accessor :hostname, :password, :port
13
+ alias_method :host, :hostname
14
+
15
+ def initialize(config_file=nil)
16
+ case config_file
17
+ when String, Pathname
18
+ config = YAML::load_file(config_file)
19
+ when File
20
+ config = YAML::load(config_file)
21
+ else
22
+ config = {}
23
+ end
24
+ config = config[Rails.env] if defined?(Rails) && config[Rails.env]
25
+ puts "config: #{config.inspect}" if $DEBUG
26
+ init_host_and_password(config)
27
+ init_port(config)
28
+ end
29
+
30
+
31
+ private
32
+
33
+ HOSTNAME_RE = /(.*)@(.*)/
34
+
35
+
36
+ def init_host_and_password(config)
37
+ if config["hostname"]
38
+ @hostname = parse_hostname(config["hostname"])
39
+ @password = parse_password(config["hostname"])
40
+ elsif ENV["MPD_HOST"]
41
+ @hostname = parse_hostname(ENV["MPD_HOST"])
42
+ @password = parse_password(ENV["MPD_HOST"])
43
+ else
44
+ @hostname = DEFAULT_HOSTNAME
45
+ @password = DEFAULT_PASSWORD
46
+ end
47
+ end
48
+
49
+ def init_port(config)
50
+ if config["port"]
51
+ @port = config["port"].to_i
52
+ elsif ENV["MPD_PORT"]
53
+ @port = ENV["MPD_PORT"].to_i
54
+ else
55
+ @port = DEFAULT_PORT
56
+ end
57
+ end
58
+
59
+ def parse_hostname(hostname)
60
+ if HOSTNAME_RE === hostname
61
+ $~[2]
62
+ else
63
+ hostname
64
+ end
65
+ end
66
+
67
+ def parse_password(hostname)
68
+ if HOSTNAME_RE === hostname
69
+ $~[1]
70
+ else
71
+ nil
72
+ end
73
+ end
74
+
75
+ end
76
+
77
+ end
@@ -0,0 +1,84 @@
1
+ require "socket"
2
+
3
+
4
+ module Rmpd
5
+ class Connection
6
+
7
+ include Rmpd::Commands
8
+ include Socket::Constants
9
+
10
+ attr_reader :error
11
+
12
+ def initialize(config_file=nil)
13
+ @config = Rmpd::Config.new(config_file)
14
+ @socket = nil
15
+ end
16
+
17
+ def server_version
18
+ "#{@server_version_major}.#{@server_version_minor}.#{@server_version_patch}"
19
+ end
20
+
21
+ attr_reader :socket
22
+
23
+ private
24
+
25
+ def authenticate
26
+ raise "Socket no good!" if (@socket.nil? || @socket.closed?)
27
+ send_command("password", @config.password)
28
+ read_response
29
+ true
30
+ end
31
+
32
+ def connect
33
+ return unless @socket.nil? || @socket.closed?
34
+ error = nil
35
+
36
+ Socket::getaddrinfo(@config.hostname, @config.port, nil, SOCK_STREAM).each do |info|
37
+ begin
38
+ puts "args: #{info.inspect}" if $DEBUG
39
+ sockaddr = Socket.pack_sockaddr_in(info[1], info[3])
40
+ @socket = Socket.new(info[4], info[5], 0)
41
+ @socket.connect(sockaddr)
42
+ rescue StandardError => error
43
+ $stderr.puts "Failed to connect to #{info[3]}: #{error}"
44
+ @socket = nil
45
+ else
46
+ break
47
+ end
48
+ end
49
+ raise MpdConnRefusedError.new(error) if @socket.nil?
50
+
51
+ parse_server_version(@socket.readline)
52
+ authenticate if @config.password
53
+ end
54
+
55
+ def parse_server_version(version)
56
+ /OK MPD (\d+)\.(\d+)\.(\d+)/.match(version.to_s)
57
+ @server_version_major = $~[1].to_i
58
+ @server_version_minor = $~[2].to_i
59
+ @server_version_patch = $~[3].to_i
60
+ end
61
+
62
+ def read_response(klass=Response, *args)
63
+ x = klass.new(receive_server_response, *args)
64
+ @error = x.error
65
+ x
66
+ end
67
+
68
+ def server_version_at_least(major, minor, patch)
69
+ connect
70
+ e = MpdError.new("Requires server version #{major}.#{minor}.#{patch}")
71
+
72
+ raise e if major > @server_version_major
73
+ return true if major < @server_version_major
74
+
75
+ raise e if minor > @server_version_minor
76
+ return true if minor < @server_version_minor
77
+
78
+ raise e if patch > @server_version_patch
79
+ return true if patch < @server_version_patch
80
+ true
81
+ end
82
+
83
+ end
84
+ end
@@ -0,0 +1,43 @@
1
+ require "delegate"
2
+
3
+ module Rmpd
4
+
5
+ class MultiResponse < DelegateClass(Array)
6
+
7
+ END_OF_RECORD = "\0x1E" # from ASCII
8
+
9
+ def initialize(data, sep)
10
+ super([])
11
+ @sep = sep
12
+ parse(data)
13
+ end
14
+
15
+ def ok?
16
+ self.all? {|x| x.ok?}
17
+ end
18
+
19
+ def ack?
20
+ !ok?
21
+ end
22
+
23
+ def error
24
+ self.find(nil) {|x| x.error}
25
+ end
26
+
27
+
28
+ private
29
+
30
+ def parse(data)
31
+ data.gsub!(@sep, "#{END_OF_RECORD}\\1")
32
+ data.split(END_OF_RECORD).each do |datum|
33
+ if datum.empty?
34
+ next
35
+ else
36
+ r = Response.new(datum)
37
+ end
38
+ self << r unless r.keys.size.zero? && r.ok?
39
+ end
40
+ end
41
+ end
42
+
43
+ end
@@ -0,0 +1,47 @@
1
+ require "delegate"
2
+
3
+ module Rmpd
4
+
5
+ KEY_VALUE_RE = /^([^:]+):\s*(.*)$/
6
+ KNOWN_INT_FIELDS = [:time, :pos, :id, :track, :playlistlength, :playlist,
7
+ :xfade, :repeat, :random, :queued, :volume, :song]
8
+
9
+ class Response < DelegateClass(Hash)
10
+
11
+ attr_reader :error
12
+
13
+ def initialize(data)
14
+ super({})
15
+ @error = nil
16
+ parse(data)
17
+ end
18
+
19
+ def ok?
20
+ @error.nil?
21
+ end
22
+
23
+ def ack?
24
+ !ok?
25
+ end
26
+
27
+ private
28
+
29
+ def register_key_val_pair(r)
30
+ key, val = r[1].downcase.to_sym, r[2]
31
+ val = val.to_i if KNOWN_INT_FIELDS.include?(key)
32
+ self[key] = include?(key) ? ([self[key]] << val).flatten : val
33
+ self.class.send(:define_method, key) {self[key]}
34
+ end
35
+
36
+ def parse(data)
37
+ data.split("\n").each do |line|
38
+ case line
39
+ when KEY_VALUE_RE: register_key_val_pair($~)
40
+ when OK_RE: @error = nil
41
+ when ACK_RE: @error = MpdAckError.new($~.values_at(0..-1))
42
+ end
43
+ end
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,3 @@
1
+ module Rmpd
2
+ VERSION = "1.0.0"
3
+ end
data/rmpd.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "rmpd/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "rmpd"
7
+ s.version = Rmpd::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Eric Wollesen"]
10
+ s.email = ["ericw@xmtp.net"]
11
+ s.homepage = ""
12
+ s.summary = %q{Music Player Daemon client in Ruby}
13
+ s.description = %q{Music Player Daemon client in Ruby}
14
+
15
+ # s.rubyforge_project = "rmpd"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {spec}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_development_dependency("rspec", "~> 2.6.0")
23
+ s.add_development_dependency("ruby-debug")
24
+ end
@@ -0,0 +1,2 @@
1
+ hostname: juggernaught@test.host
2
+ port: 1234
@@ -0,0 +1 @@
1
+ port: 1234
@@ -0,0 +1,2 @@
1
+ hostname: test.host
2
+ port: 1234
@@ -0,0 +1 @@
1
+ hostname: juggernaught@test.host
@@ -0,0 +1,3 @@
1
+ development:
2
+ hostname: localhost
3
+ port: 3000
@@ -0,0 +1,135 @@
1
+ require "spec_helper"
2
+
3
+ include Rmpd
4
+
5
+
6
+ shared_examples_for "a command with a song pos" do
7
+ it "should pass along the song pos" do
8
+ @socket.stub!(:readline).and_return(*@responses)
9
+ @socket.stub!(:puts).and_return(@socket.puts)
10
+ @socket.stub!(:eof?).and_return(false)
11
+ @command.call
12
+ end
13
+ end
14
+
15
+ describe Rmpd::Commands do
16
+
17
+ before(:each) do
18
+ @socket = mock_socket
19
+ @config = mock_config
20
+ @conn = Connection.new
21
+ end
22
+
23
+ def set_password(password="foobar")
24
+ @config.stub!(:password).and_return(password)
25
+ end
26
+
27
+ it "should handle a closed connection gracefully" do
28
+ @socket.stub!(:puts).and_raise(EOFError)
29
+ @socket.stub!(:readline).and_return(*(connect_response + ok))
30
+ @socket.should_receive(:close).exactly(5).times
31
+ lambda do
32
+ @conn.ping
33
+ end.should raise_error(Rmpd::MpdError, "Retry count exceeded")
34
+ end
35
+
36
+ it "should hide password output when in debug mode" do
37
+ set_password
38
+ @socket.stub!(:readline).and_return(*(connect_and_auth_responses + ok))
39
+ Kernel.should_receive(:puts).with("send: password #{"*"*8}")
40
+ Kernel.should_receive(:puts).with("send: ping")
41
+ $DEBUG = 1
42
+ @conn.ping
43
+ $DEBUG = nil
44
+ end
45
+
46
+ it "should abort after a limited number of tries against a closed connection" do
47
+ @socket.stub!(:puts).and_raise(EOFError)
48
+ @socket.stub!(:readline).and_return(*(connect_response + ok))
49
+ @socket.should_receive(:close).exactly(5).times
50
+ lambda do
51
+ @conn.ping
52
+ end.should raise_error(MpdError, "Retry count exceeded")
53
+ end
54
+
55
+ describe "close" do
56
+ it "should close the socket" do
57
+ @socket.should_receive(:puts).with("close")
58
+ @socket.stub!(:readline).and_return(*connect_response)
59
+ @socket.should_receive(:close).once
60
+ @conn.close
61
+ end
62
+ end
63
+
64
+ describe "volume" do
65
+ it "should be deprecated" do
66
+ set_password
67
+ @socket.stub!(:readline).and_return(*connect_and_auth_responses)
68
+ $stderr.should_receive(:puts).with(/deprecated/i)
69
+ @conn.volume(0)
70
+ end
71
+ end
72
+
73
+ describe "kill" do
74
+ before(:each) do
75
+ set_password
76
+ end
77
+
78
+ it "should kill the daemon" do
79
+ @socket.should_receive(:puts).with("kill")
80
+ @socket.stub!(:readline).and_return(*connect_and_auth_responses)
81
+ @socket.should_receive(:close)
82
+ @conn.kill
83
+ end
84
+ end
85
+
86
+ describe "playlistinfo" do
87
+ before(:each) do
88
+ set_password
89
+ @song_id = 1
90
+ @cmd = "playlistinfo \"#{@song_id}\""
91
+ @responses = connect_and_auth_responses + ["file: blah\n", "boobies: yay!\n"] + ok
92
+ @command = Proc.new {@conn.playlistinfo(@song_id)}
93
+ end
94
+
95
+ it_should_behave_like "a command with a song pos"
96
+ end
97
+
98
+ describe "play" do
99
+ before(:each) do
100
+ set_password
101
+ @song_id = 12
102
+ @cmd = "play \"#{@song_id}\""
103
+ @responses = connect_and_auth_responses + ok
104
+ @command = Proc.new {@conn.play(@song_id)}
105
+ end
106
+
107
+ it_should_behave_like "a command with a song pos"
108
+ end
109
+
110
+ describe "delete" do
111
+ before(:each) do
112
+ set_password
113
+ @song_id = 23
114
+ @cmd = "delete \"#{@song_id}\""
115
+ @responses = connect_and_auth_responses + ok
116
+ @command = Proc.new {@conn.delete(@song_id)}
117
+ end
118
+
119
+ it_should_behave_like "a command with a song pos"
120
+ end
121
+
122
+ describe "command_list" do
123
+ it "shouldn't wait for a response from the first command" do
124
+ @responses = connect_response + ok
125
+ @socket.stub!(:readline).and_return(*@responses)
126
+ @socket.stub!(:puts).and_return(@socket.puts)
127
+ @socket.stub!(:eof?).and_return(false)
128
+
129
+ @conn.command_list do |c|
130
+ c.status
131
+ c.stats
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,128 @@
1
+ require "spec_helper"
2
+
3
+ include Rmpd
4
+
5
+
6
+ describe Rmpd::Config do
7
+
8
+ before(:each) do
9
+ @filename = File.join(File.dirname(__FILE__), "../../spec/fixtures/config.yml")
10
+ end
11
+
12
+ describe "initialization" do
13
+ it "should parse successfully" do
14
+ lambda do
15
+ Rmpd::Config.new(@filename)
16
+ end.should_not raise_error
17
+ end
18
+ end
19
+
20
+ describe "rails" do
21
+ before(:each) do
22
+ class Rails
23
+ def self.env ; "development" ; end
24
+ end
25
+ @filename = File.join(File.dirname(__FILE__), "../../spec/fixtures/config_rails.yml")
26
+ end
27
+
28
+ it "should load the development environment" do
29
+ lambda do
30
+ Rmpd::Config.new(@filename)
31
+ end.should_not raise_error
32
+ end
33
+
34
+ it "should load the development environment" do
35
+ config = Rmpd::Config.new(@filename)
36
+ config.hostname.should == "localhost"
37
+ config.port.should == 3000
38
+ end
39
+ end
40
+
41
+ describe "operation" do
42
+ before(:each) do
43
+ @config = Rmpd::Config.new(@filename)
44
+ end
45
+
46
+ it "should have the correct port" do
47
+ 1234.should == @config.port
48
+ end
49
+
50
+ it "should have the correct hostname" do
51
+ @config.hostname.should == "test.host"
52
+ end
53
+
54
+ it "should have the correct password" do
55
+ @config.password.should == "juggernaught"
56
+ end
57
+ end
58
+
59
+ describe "without password" do
60
+ before(:each) do
61
+ @filename = File.join(File.dirname(__FILE__), "../../spec/fixtures/config_no_password.yml")
62
+ @config = Rmpd::Config.new(@filename)
63
+ end
64
+
65
+ it "should have the default password" do
66
+ @config.password.should == Rmpd::Config::DEFAULT_PASSWORD
67
+ end
68
+ end
69
+
70
+ describe "without hostname" do
71
+ before(:each) do
72
+ @filename = File.join(File.dirname(__FILE__), "../../spec/fixtures/config_no_hostname.yml")
73
+ @config = Rmpd::Config.new(@filename)
74
+ end
75
+
76
+ it "should have the default hostname" do
77
+ @config.hostname.should == Rmpd::Config::DEFAULT_HOSTNAME
78
+ end
79
+ end
80
+
81
+ describe "without port" do
82
+ before(:each) do
83
+ @filename = File.join(File.dirname(__FILE__), "../../spec/fixtures/config_no_port.yml")
84
+ @config = Rmpd::Config.new(@filename)
85
+ end
86
+
87
+ it "should have the default port" do
88
+ @config.port.should == Rmpd::Config::DEFAULT_PORT
89
+ end
90
+ end
91
+
92
+ describe "by default" do
93
+ before(:each) do
94
+ @filename = nil
95
+ ENV["MPD_HOST"] = nil
96
+ ENV["MPD_PORT"] = nil
97
+ @config = Rmpd::Config.new(@filename)
98
+ end
99
+
100
+ it "should have the default hostname" do
101
+ @config.hostname.should == Rmpd::Config::DEFAULT_HOSTNAME
102
+ end
103
+
104
+ it "should have the default port" do
105
+ @config.port.should == Rmpd::Config::DEFAULT_PORT
106
+ end
107
+
108
+ it "should have the default password" do
109
+ @config.password.should == Rmpd::Config::DEFAULT_PASSWORD
110
+ end
111
+ end
112
+
113
+ describe "with environment variables set" do
114
+ before(:each) do
115
+ ENV["MPD_HOST"] = "testing:host"
116
+ ENV["MPD_PORT"] = -1.to_s
117
+ @config = Rmpd::Config.new
118
+ end
119
+
120
+ it "should pickup the environment's host" do
121
+ @config.hostname.should == ENV["MPD_HOST"]
122
+ end
123
+
124
+ it "should pickup the environment's port" do
125
+ @config.port.should == ENV["MPD_PORT"].to_i
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,61 @@
1
+ require "spec_helper"
2
+
3
+ include Rmpd
4
+
5
+
6
+ describe Connection do
7
+
8
+ before(:each) do
9
+ @socket = mock_socket
10
+ @config = mock_config
11
+ @conn = Connection.new
12
+ end
13
+
14
+ def set_password(password="foobar")
15
+ @config.stub!(:password).and_return(password)
16
+ end
17
+
18
+ it "should connect successfully" do
19
+ responses = connect_response + ok
20
+ @socket.should_receive(:readline).and_return(*responses)
21
+ @conn.ping
22
+ end
23
+
24
+ it "should authenticate successfully" do
25
+ responses = connect_and_auth_responses + ok
26
+ set_password
27
+ @socket.should_receive(:readline).and_return(*responses)
28
+ @conn.ping
29
+ end
30
+
31
+ it "should generate a server version" do
32
+ version = "0.1.2"
33
+ responses = connect_response(version) + ok
34
+ @socket.should_receive(:readline).and_return(*responses)
35
+ @conn.ping # so we connect, and get the server version
36
+ @conn.server_version.should == version
37
+ end
38
+
39
+ it "should handle connection failures gracefully" do
40
+ @socket.stub!(:connect).and_raise(Errno::ECONNREFUSED.new("test"))
41
+ lambda do
42
+ @conn.ping
43
+ end.should raise_error(Rmpd::MpdError)
44
+ end
45
+
46
+ it "should restrict access based on server version" do
47
+ responses = connect_response("0.1.0")
48
+ @socket.stub!(:readline).and_return(*responses)
49
+ @conn.instance_eval do
50
+ def sval(*args)
51
+ server_version_at_least(*args)
52
+ end
53
+ end
54
+ lambda do
55
+ @conn.sval(0, 1, 2)
56
+ end.should raise_error(MpdError)
57
+ lambda do
58
+ @conn.sval(0, 1, 0)
59
+ end.should_not raise_error(MpdError)
60
+ end
61
+ end
@@ -0,0 +1,66 @@
1
+ require "spec_helper"
2
+
3
+ module Rmpd
4
+ describe Commands, "Generators" do
5
+
6
+ before(:each) do
7
+ @config = mock_config
8
+ @socket = mock_socket
9
+ @conn = Connection.new
10
+ end
11
+
12
+ describe "when version is ok" do
13
+
14
+ before(:each) do
15
+ version = "0.1.1"
16
+ responses = connect_response(version) + ok
17
+ @socket.should_receive(:readline).and_return(*responses)
18
+ end
19
+
20
+ describe "simple_command" do
21
+ it "should allow based on server version" do
22
+ Commands::simple_command(:test, :min_version => [0, 1, 1])
23
+ lambda do
24
+ @conn.test
25
+ end.should_not raise_error(MpdError)
26
+ end
27
+ end
28
+
29
+ describe "complex_command" do
30
+ it "should allow based on server version" do
31
+ Rmpd::Commands::complex_command(:test, :min_version => [0, 1, 1])
32
+ lambda do
33
+ @conn.test
34
+ end.should_not raise_error(MpdError)
35
+ end
36
+ end
37
+ end
38
+
39
+ describe "when version is NOT ok" do
40
+
41
+ before(:each) do
42
+ version = "0.1.1"
43
+ responses = connect_response(version)
44
+ @socket.should_receive(:readline).and_return(*responses)
45
+ end
46
+
47
+ describe "simple_command" do
48
+ it "should disallow based on server version" do
49
+ Rmpd::Commands::simple_command(:test, :min_version => [0, 1, 2])
50
+ lambda do
51
+ @conn.test
52
+ end.should raise_error(MpdError, /^Requires server version/)
53
+ end
54
+ end
55
+
56
+ describe "complex_command" do
57
+ it "should disallow based on server version" do
58
+ Rmpd::Commands::complex_command(:test, :min_version => [0, 1, 2])
59
+ lambda do
60
+ @conn.test
61
+ end.should raise_error(MpdError, /^Requires server version/)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,15 @@
1
+ require "spec_helper"
2
+
3
+ include Rmpd
4
+
5
+ describe MpdAckError do
6
+ before(:each) do
7
+ @err_msg = "ACK [2@0] {search} too few arguments for \"search\""
8
+ ACK_RE.match(@err_msg)
9
+ @error = MpdAckError.new($~)
10
+ end
11
+
12
+ it "should generate a useful string message" do
13
+ @error.to_s.should == @err_msg
14
+ end
15
+ end
@@ -0,0 +1,54 @@
1
+ require "spec_helper"
2
+
3
+ include Rmpd
4
+
5
+
6
+ describe Rmpd::MultiResponse do
7
+ describe "on success" do
8
+ before(:each) do
9
+ data = <<-EOF
10
+ foo: cat
11
+ bar: dog
12
+ foo: horse
13
+ bar: giraffe
14
+ OK
15
+ EOF
16
+ @response = Rmpd::MultiResponse.new(data, /(^foo:)/)
17
+ end
18
+
19
+ it "should have a size of 2" do
20
+ @response.should have(2).items
21
+ end
22
+
23
+ it "should be OK" do
24
+ # @response.should be_ok
25
+ # doesn't work correctly, see rspec bug #11526
26
+ @response.ok?.should be_true
27
+ end
28
+
29
+ it "should not be ACK" do
30
+ # @response.should_not be_ack
31
+ # doesn't work correctly, see rspec bug #11526
32
+ @response.ack?.should be_false
33
+ end
34
+ end
35
+
36
+ describe "on error" do
37
+ before(:each) do
38
+ @err_msg = "ACK [2@0] {search} too few arguments for \"search\"\n"
39
+ @response = Rmpd::MultiResponse.new(@err_msg, /(^foo:)/)
40
+ end
41
+
42
+ it "should not be OK" do
43
+ # @response.should_not be_ok
44
+ # doesn't work correctly, see rspec bug #11526
45
+ @response.ok?.should be_false
46
+ end
47
+
48
+ it "should be ACK" do
49
+ # @response.should be_ack
50
+ # doesn't work correctly, see rspec bug #11526
51
+ @response.ack?.should be_true
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,36 @@
1
+ require "spec_helper"
2
+
3
+ include Rmpd
4
+
5
+
6
+ describe Rmpd::Response do
7
+ before(:each) do
8
+ data = <<-EOF
9
+ foo: bar
10
+ OK
11
+ EOF
12
+ @response = Rmpd::Response.new(data)
13
+ end
14
+
15
+ it "should have a foo method" do
16
+ @response.respond_to?(:foo).should be_true
17
+ @response.foo.should == "bar"
18
+ end
19
+
20
+ it "should have a foo key" do
21
+ @response.should include(:foo)
22
+ @response[:foo].should == "bar"
23
+ end
24
+
25
+ it "should be ok" do
26
+ # @response.should be_ok
27
+ # doesn't work correctly, see rspec bug #11526
28
+ @response.ok?.should == true
29
+ end
30
+
31
+ it "should not be ack" do
32
+ # @response.should_not be_ack
33
+ # doesn't work correctly, see rspec bug #11526
34
+ @response.ack?.should_not == true
35
+ end
36
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1,6 @@
1
+ --colour
2
+ --format
3
+ specdoc
4
+ --loadby
5
+ mtime
6
+ --reverse
@@ -0,0 +1,43 @@
1
+ require File.dirname(__FILE__) + "/../lib/rmpd"
2
+
3
+
4
+ def mock_socket
5
+ socket = mock(Socket)
6
+ socket.stub!(:puts)
7
+ socket.stub!(:closed?).and_return(false)
8
+ socket.stub!(:connect)
9
+ socket.stub!(:eof?).and_return(false)
10
+ Socket.stub!(:pack_sockaddr_in).and_return("pack_sockaddr_in")
11
+ Socket.stub!(:new).and_return(socket)
12
+ socket
13
+ end
14
+
15
+ def mock_config(opts={})
16
+ opts = {
17
+ :hostname => Rmpd::Config::DEFAULT_HOSTNAME,
18
+ :port => Rmpd::Config::DEFAULT_PORT,
19
+ :password => Rmpd::Config::DEFAULT_PASSWORD,
20
+ }.merge(opts)
21
+ config = mock(Rmpd::Config)
22
+ config.stub!(:hostname).and_return(opts[:hostname])
23
+ config.stub!(:password).and_return(opts[:password])
24
+ config.stub!(:port).and_return(opts[:port])
25
+ Rmpd::Config.stub!(:new).and_return(config)
26
+ config
27
+ end
28
+
29
+ def connect_response(version="0.12.0")
30
+ ok("MPD #{version}")
31
+ end
32
+
33
+ def connect_and_auth_responses
34
+ connect_response + ok
35
+ end
36
+
37
+ def ok(txt="")
38
+ txt ? ["OK #{txt}\n"] : ["OK\n"]
39
+ end
40
+
41
+ def ack(x=1, y=2, cmd="foo_command", msg="No one's home dopey!")
42
+ ["ACK [#{x}@#{y}] {#{cmd}} #{msg}"]
43
+ end
@@ -0,0 +1,5 @@
1
+ --format
2
+ specdoc
3
+ --loadby
4
+ mtime
5
+ --reverse
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rmpd
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease: false
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Eric Wollesen
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-06-12 00:00:00 -06:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rspec
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 23
30
+ segments:
31
+ - 2
32
+ - 6
33
+ - 0
34
+ version: 2.6.0
35
+ type: :development
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: ruby-debug
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 3
46
+ segments:
47
+ - 0
48
+ version: "0"
49
+ type: :development
50
+ version_requirements: *id002
51
+ description: Music Player Daemon client in Ruby
52
+ email:
53
+ - ericw@xmtp.net
54
+ executables: []
55
+
56
+ extensions: []
57
+
58
+ extra_rdoc_files: []
59
+
60
+ files:
61
+ - .gitignore
62
+ - .rspec
63
+ - Gemfile
64
+ - Rakefile
65
+ - lib/rmpd.rb
66
+ - lib/rmpd/commands.rb
67
+ - lib/rmpd/commands/admin.rb
68
+ - lib/rmpd/commands/database.rb
69
+ - lib/rmpd/commands/generators.rb
70
+ - lib/rmpd/commands/miscellaneous.rb
71
+ - lib/rmpd/commands/playback.rb
72
+ - lib/rmpd/commands/playlist.rb
73
+ - lib/rmpd/config.rb
74
+ - lib/rmpd/connection.rb
75
+ - lib/rmpd/multi_response.rb
76
+ - lib/rmpd/response.rb
77
+ - lib/rmpd/version.rb
78
+ - rmpd.gemspec
79
+ - spec/fixtures/config.yml
80
+ - spec/fixtures/config_no_hostname.yml
81
+ - spec/fixtures/config_no_password.yml
82
+ - spec/fixtures/config_no_port.yml
83
+ - spec/fixtures/config_rails.yml
84
+ - spec/models/commands_spec.rb
85
+ - spec/models/config_spec.rb
86
+ - spec/models/connection_spec.rb
87
+ - spec/models/generators_spec.rb
88
+ - spec/models/mildred_mpd_spec.rb
89
+ - spec/models/multi_response_spec.rb
90
+ - spec/models/response_spec.rb
91
+ - spec/spec.opts
92
+ - spec/spec_helper.rb
93
+ - spec/spec_rcov.opts
94
+ has_rdoc: true
95
+ homepage: ""
96
+ licenses: []
97
+
98
+ post_install_message:
99
+ rdoc_options: []
100
+
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ none: false
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ hash: 3
109
+ segments:
110
+ - 0
111
+ version: "0"
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ hash: 3
118
+ segments:
119
+ - 0
120
+ version: "0"
121
+ requirements: []
122
+
123
+ rubyforge_project:
124
+ rubygems_version: 1.3.7
125
+ signing_key:
126
+ specification_version: 3
127
+ summary: Music Player Daemon client in Ruby
128
+ test_files: []
129
+