rmpd 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.
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
+