-
Notifications
You must be signed in to change notification settings - Fork 32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Command Lists (Automatic Result Types) #76
Open
Phrogz
wants to merge
3
commits into
archseer:master
Choose a base branch
from
Phrogz:command-lists-magic
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,8 @@ def convert_command(command, *params) | |
end | ||
when MPD::Song | ||
quotable_param param.file | ||
when MPD::Playlist | ||
quotable_param param.name | ||
when Hash # normally a search query | ||
param.each_with_object("") do |(type, what), query| | ||
query << "#{type} #{quotable_param what} " | ||
|
@@ -55,12 +57,19 @@ def quotable_param(value) | |
FLOAT_KEYS = Set[:mixrampdb, :elapsed] | ||
BOOL_KEYS = Set[:repeat, :random, :single, :consume, :outputenabled] | ||
|
||
# Commands, where it makes sense to always explicitly return an array. | ||
# Commands where it makes sense to always explicitly return an array. | ||
RETURN_ARRAY = Set[:channels, :outputs, :readmessages, :list, | ||
:listallinfo, :find, :search, :listplaylists, :listplaylist, :playlistfind, | ||
:playlistsearch, :plchanges, :tagtypes, :commands, :notcommands, :urlhandlers, | ||
:decoders, :listplaylistinfo, :playlistinfo] | ||
|
||
# Commands that should always return MPD::Song instances | ||
SONG_COMMANDS = Set[:listallinfo,:playlistinfo,:find,:findadd,:search, | ||
:searchadd,:playlistfind,:playlistsearch,:plchanges,:listplaylistinfo] | ||
|
||
# Commands that should always return MPD::Playlist instances | ||
PLAYLIST_COMMANDS = Set[:listplaylists] | ||
|
||
# Parses key-value pairs into correct class. | ||
def parse_key(key, value) | ||
if INT_KEYS.include? key | ||
|
@@ -105,25 +114,26 @@ def parse_line(line) | |
# The end result is a hash containing the proper key/value pairs | ||
def build_hash(string) | ||
return {} if string.nil? | ||
array_keys = {} | ||
|
||
string.lines.each_with_object({}) do |line, hash| | ||
key, object = parse_line(line) | ||
|
||
# if val appears more than once, make an array of vals. | ||
if hash.include? key | ||
hash[key] = Array(hash[key]) << object | ||
# cannot use Array(hash[key]) or [*hash[key]] because Time instances get splatted | ||
# cannot check for is_a?(Array) because some values (time) are already arrays | ||
unless array_keys[key] | ||
hash[key] = [hash[key]] | ||
array_keys[key] = true | ||
end | ||
hash[key] << object | ||
else # val hasn't appeared yet, map it. | ||
hash[key] = object # map obj to key | ||
end | ||
end | ||
end | ||
|
||
# Converts the response to MPD::Song objects. | ||
# @return [Array<MPD::Song>] An array of songs. | ||
def build_songs_list(array) | ||
return array.map { |hash| Song.new(self, hash) } | ||
end | ||
|
||
# Make chunks from string. | ||
# @return [Array<String>] | ||
def make_chunks(string) | ||
|
@@ -150,17 +160,39 @@ def parse_response(command, string) | |
build_response(command, string) | ||
end | ||
|
||
def parse_command_list(commands, string) | ||
[].tap do |results| | ||
string.split("list_OK\n").each do |str| | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can get rid of |
||
command = commands.shift | ||
results << parse_response(command, str) unless str.empty? | ||
end | ||
end | ||
end | ||
|
||
# Parses the response into appropriate objects (either a single object, | ||
# or an array of objects or an array of hashes). | ||
# | ||
# @return [Array<Hash>, Array<String>, String, Integer] Parsed response. | ||
def build_response(command, string) | ||
def build_response(command, string, force_hash=nil) | ||
chunks = make_chunks(string) | ||
# if there are any new lines (more than one data piece), it's a hash, else an object. | ||
is_hash = chunks.any? { |chunk| chunk.include? "\n" } | ||
|
||
make_song = SONG_COMMANDS.include?(command) | ||
make_plist = PLAYLIST_COMMANDS.include?(command) | ||
make_hash = force_hash || make_song || make_plist || chunks.any?{ |chunk| chunk.include? "\n" } | ||
|
||
list = chunks.inject([]) do |result, chunk| | ||
result << (is_hash ? build_hash(chunk) : parse_line(chunk)[1]) # parse_line(chunk)[1] is object | ||
result << (make_hash ? build_hash(chunk) : parse_line(chunk)[1]) # parse_line(chunk)[1] is object | ||
end | ||
|
||
if make_song | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can use a splat with a when statement: case command
when *SONG_COMMANDS
when *PLAYLIST_COMMANDS
end |
||
list.map! do |opts| | ||
if opts[:file] && opts[:file] =~ %r{^https?://}i | ||
opts = { file:opts[:file], time:[0] } | ||
end | ||
Song.new(@mpd, opts) | ||
end | ||
elsif make_plist | ||
list.map!{ |opts| Playlist.new(self,opts) } | ||
end | ||
|
||
# if list has only one element and not set to explicit array, return it, else return array | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
module MPD::Plugins | ||
|
||
# Batch send multiple commands at once for speed. | ||
module CommandList | ||
# Send multiple commands at once. | ||
# | ||
# By default, any response from the server is ignored (for speed). | ||
# To get results, pass +{results:true}+ to the method. | ||
# | ||
# Note that each supported command has no return value inside the block. | ||
# Instead, the block itself returns the array of results. | ||
# | ||
# @param [Symbol] response_type the type of responses desired. | ||
# @return [nil] default behavior. | ||
# @return [Array] if +results+ is +true+. | ||
# | ||
# @example Simple batched control commands | ||
# @mpd.command_list do | ||
# stop | ||
# shuffle | ||
# save "shuffled" | ||
# end | ||
# | ||
# @example Adding songs to the queue, ignoring the response | ||
# @mpd.command_list do | ||
# my_songs.each do |song| | ||
# add(song) | ||
# end | ||
# end | ||
# | ||
# @example Adding songs to the queue and getting the song ids | ||
# ids = @mpd.command_list(results:true){ my_songs.each{ |song| addid(song) } } | ||
# #=> [799,800,801,802,803] | ||
# | ||
# @example Finding songs matching various genres | ||
# results = @mpd.command_list(results:true) do | ||
# where genre:'Alternative Rock' | ||
# where genre:'Alt. Rock' | ||
# where genre:'alt-rock' | ||
# end | ||
# p results.class #=> Array (One result for each command that gives results) | ||
# p results.length #=> 3 (One for each command that returns results) | ||
# p results.first.class #=> Array (Each `where` command returns its own results) | ||
# | ||
# | ||
# @example Using playlists inside a command list | ||
# def shuffle_playlist( playlist ) | ||
# song_count = @mpd.send_command(:listplaylist, playlist.name).length | ||
# @mpd.command_list do | ||
# (song_count-1).downto(1){ |i| playlist.move i, rand(i+1) } | ||
# end | ||
# end | ||
# | ||
# | ||
# @see CommandList::Commands CommandList::Commands for a list of supported commands. | ||
def command_list(opts={},&commands) | ||
@mutex.synchronize do | ||
begin | ||
@command_list_commands = [] | ||
socket.puts( opts[:results] ? "command_list_ok_begin" : "command_list_begin" ) | ||
@command_list_active = true | ||
CommandList::Commands.new(self).instance_eval(&commands) | ||
@command_list_active = false | ||
socket.puts "command_list_end" | ||
|
||
# clear the response from the socket, even if we will not parse it | ||
response = handle_server_response || "" | ||
|
||
parse_command_list( @command_list_commands, response ) if opts[:results] | ||
rescue Errno::EPIPE | ||
reconnect | ||
retry | ||
ensure | ||
@command_list_commands = nil | ||
@command_list_active = false | ||
end | ||
end | ||
end | ||
|
||
end | ||
|
||
class CommandList::Commands | ||
def initialize(mpd) | ||
@mpd = mpd | ||
end | ||
|
||
include MPD::Plugins::Controls | ||
include MPD::Plugins::PlaybackOptions | ||
include MPD::Plugins::Queue | ||
include MPD::Plugins::Stickers | ||
include MPD::Plugins::Database | ||
include MPD::Plugins::Playlists | ||
|
||
private | ||
def send_command(command,*args) | ||
@mpd.send_command(command,*args) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you try using
Array.wrap
? I think that one should safely work with Time objects as well, since it usesto_ary
instead ofto_a
. http://apidock.com/rails/Array/wrap/class