dhun 0.5.6 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/dhun/player.rb CHANGED
@@ -4,112 +4,137 @@ module Dhun
4
4
  class Player
5
5
  include Singleton
6
6
 
7
- attr_reader :queue
8
- attr_reader :history
9
- attr_reader :status
10
- attr_reader :current
11
-
12
- attr_reader :logger
7
+ attr_accessor :queue,:history,:status,:current,:logger
13
8
 
14
9
  def initialize
15
- @queue = []
16
- @history = []
10
+ @queue,@history = [],[]
17
11
  @logger = Logger.instance
18
12
  @status = :stopped
19
13
  end
20
14
 
21
- def empty_queue
22
- stop
23
- @queue.clear
24
- end
25
-
26
-
27
- def play_files(files)
28
- if files.empty?
29
- logger.log "Empty Queue"
30
- else
31
- stop
32
- empty_queue
33
- files.each { |f| self.queue.push f }
34
- play
35
- end
36
- end
37
-
15
+ # enqueue files and call play.
38
16
  def enqueue(files)
39
- files.each { |f| self.queue.push f }
40
- play
17
+ return false if files.empty?
18
+ files.each { |f| self.queue.push f }; play
19
+ return true
20
+ end
21
+
22
+ # clear the queue and stops playback
23
+ def clear
24
+ stop ; @queue.clear
25
+ return true
41
26
  end
42
27
 
28
+ # commence playback
43
29
  def play
44
- return unless self.status == :stopped
30
+ return :empty if @queue.empty?
31
+ return false if @status == :playing
32
+ return resume if @status == :paused
45
33
  @status = :playing
46
- @player_thread = Thread.new do
47
- while @status == :playing and !queue.empty?
48
- @current = @queue.shift
49
- logger.log "Playing #{@current}"
50
- DhunExt.play_file @current
51
- @history.unshift @current
52
- end
53
- @status = :stopped
54
- @current = nil
55
- end
34
+ @player_thread = play_thread
35
+ return true
56
36
  end
57
37
 
38
+ # pause playback
39
+ # only on :playing
58
40
  def pause
59
41
  if @status == :playing
60
42
  @status = :paused
61
43
  DhunExt.pause
44
+ @logger.debug "pause"
45
+ return true
62
46
  end
47
+ return false
63
48
  end
64
49
 
50
+ # resume playback
51
+ # only on :paused
65
52
  def resume
66
53
  if @status == :paused
67
54
  @status = :playing
68
55
  DhunExt.resume
56
+ @logger.debug "resume"
57
+ return true
69
58
  end
59
+ return false
70
60
  end
71
61
 
62
+ # stops the song
63
+ # unless :stopped
72
64
  def stop
73
- @status = :stopped
74
- DhunExt.stop
75
- # Wait for @player_thread to exit cleanly
76
- @player_thread.join unless @player_thread.nil?
77
- logger.debug "Stopped"
65
+ unless @status == :stopped
66
+ @status = :stopped
67
+ DhunExt.stop
68
+ # Wait for @player_thread to exit cleanly
69
+ @player_thread.join unless @player_thread.nil?
70
+ @logger.debug "Stopped"
71
+ return true
72
+ end
73
+ return false
78
74
  end
79
75
 
76
+ # plays next song on queue.
77
+ # returns next_track or false if invalid
80
78
  def next(skip_length = 1)
81
- logger.debug "Switching to next"
82
- unless @queue.size < skip_length
83
- stop # stops current track
84
- @queue.shift skip_length-1 # Remove skip_length-1 tracks
79
+ unless skip_length > @queue.size
80
+ @logger.debug "next invoked"
81
+ stop
82
+ @queue.shift(skip_length - 1) #skip_length returns starting with first on queue.
85
83
  next_track = @queue.first
86
- play # start playing with the next track
84
+ play
85
+ return next_track
87
86
  end
88
- return next_track
87
+ return false
89
88
  end
90
89
 
90
+ # when :stopped
91
+ # returns the first song in history
92
+ # when :playing
93
+ # returns the second song in history as first song is current song
91
94
  def prev(skip_length = 1)
92
- logger.debug "Switching to prev"
93
- unless @history.size < skip_length
94
- unless @status == :stopped
95
- stop
96
- # history has increased by one
97
- skip_length = skip_length + 1
98
- end
95
+ # skip current track if playing
96
+ if @status == :playing
97
+ stop ; skip_length += 1
98
+ end
99
+ unless skip_length > @history.size
100
+ @logger.debug "previous invoked"
99
101
  tracks = @history.shift skip_length
100
- logger.debug tracks
101
- tracks.each { |t| @queue.unshift t }
102
- prev_track = @queue.first
103
- play # start playing with the next track
102
+ tracks.each { |track| @queue.unshift track }
103
+ previous = @queue.first
104
+ play
105
+ return previous
104
106
  end
105
- return prev_track
107
+ return false
106
108
  end
107
109
 
110
+ # shuffle queue if queue is not empty
111
+ # ensures that shuffled queue is not equal to previous queue order
112
+ # NOTE: if they enqueue all the same songs, this will NOT end. should catch that.
108
113
  def shuffle
109
- return if @queue.empty?
110
- s = @queue.size
111
- s.downto(1) { |n| @queue.push @queue.delete_at(rand(n)) }
112
- logger.debug @queue
114
+ return false if @queue.empty? or @queue.uniq.size == 1 # this will catch a playlist of same songs
115
+ q = @queue.clone
116
+ while q == @queue
117
+ @queue.size.downto(1) { |n| @queue.push @queue.delete_at(rand(n)) }
118
+ end
119
+ @logger.debug @queue
120
+ return true
121
+ end
122
+
123
+ private
124
+
125
+ # play method's player thread
126
+ def play_thread
127
+ Thread.new do
128
+ while @status == :playing and !@queue.empty?
129
+ @current = @queue.shift
130
+ @logger.log "Playing #{@current}"
131
+ DhunExt.play_file @current
132
+ @history.unshift @current
133
+ end
134
+ @status = :stopped
135
+ @current = nil
136
+ end
113
137
  end
138
+
114
139
  end
115
140
  end
data/lib/dhun/query.rb CHANGED
@@ -3,64 +3,85 @@ require 'dhun_ext'
3
3
  module Dhun
4
4
  class Query
5
5
 
6
- MD_ITEMS = [:kMDItemAlbum, :kMDItemAuthors, :kMDItemComposer, :kMDItemDisplayName, :kMDItemFSName, :kMDItemTitle, :kMDItemMusicalGenre]
7
- MAPPINGS = { "file" => :kMDItemFSName, "album" => :kMDItemAlbum, "artist" => :kMDItemAuthors, "title" => :kMDItemTitle, "genre" => :kMDItemMusicalGenre }
6
+ MAPPINGS = {
7
+ :file => :kMDItemFSName,
8
+ :album => :kMDItemAlbum,
9
+ :artist => :kMDItemAuthors,
10
+ :title => :kMDItemTitle,
11
+ :genre => :kMDItemMusicalGenre,
12
+ :composer => :kMDItemComposer,
13
+ :display => :kMDItemDisplayName
14
+ }
8
15
 
9
- attr_reader :spotlight_query
16
+ attr_accessor :spotlight_query,:is_valid,:logger,:query_search,:query_fields
10
17
 
11
- def initialize(args)
12
- @query_args = args
13
- parse
18
+ def initialize(search=nil,fields={})
19
+ @logger = Dhun::Logger.instance
20
+ @query_search = search
21
+ @query_fields = fields
22
+ @is_valid = parse!
14
23
  end
15
24
 
16
- def parse
17
- return if @query_args.empty?
18
- filters = []
19
- strings = []
25
+ # parses all search terms and stores query
26
+ # return false if both are empty.
27
+ def parse!
28
+ return false if @query_search.nil? and @query_fields.empty?
20
29
 
21
- @query_args.each do |arg|
22
- # Check if it is a filter
23
- if filter?(arg)
24
- filters.push(arg)
25
- else
26
- strings.push(arg)
27
- end
28
- end
30
+ mappings = MAPPINGS.values #instantiate mappings to be picked off by query methods
31
+ #create the queries
32
+ filter_query = create_filter_query(@query_fields,mappings)
33
+ string_query = create_string_query(@query_search,mappings)
34
+ @spotlight_query = create_spotlight_query(filter_query,string_query)
29
35
 
30
- mappings = MD_ITEMS.clone
31
- fq = filters.collect do |f|
32
- fltr,query = *(f.split(":"))
33
- md_item = MAPPINGS[fltr]
34
- mappings.delete md_item
35
- "#{md_item} == '#{query.strip}'wc"
36
- end.join(" && ")
37
-
38
-
39
- template = "%s == '%s'wc"
40
- sq = strings.collect do |keyword|
41
- q = mappings.collect { |key| template % [key,keyword] }.join(" || ")
42
- "( #{q} )"
43
- end.join(" && ")
36
+ @logger.debug @spotlight_query
37
+ return true
38
+ end
44
39
 
45
- @spotlight_query = ["kMDItemContentTypeTree == 'public.audio'", fq, sq].select { |s| s.length > 0 }.join(" && ")
46
- Logger.instance.debug @spotlight_query
47
- @is_valid = true
40
+ # create filter queries
41
+ # { :album => 'test' } => "kMDItemAlbum == 'test'wc"
42
+ # ADDITIONALLY, throws out any non matching filters
43
+ # { :album => 'test', :booger => 'one' } => "kMDItemAlbum == 'test'wc"
44
+ def create_filter_query(filters,mappings)
45
+ filters.collect do |field,value|
46
+ md_item = MAPPINGS[field.to_sym]
47
+ next unless md_item # makes sure that field is to sym, or funky stuff happens
48
+ mappings.delete md_item
49
+ "#{md_item} == '#{value}'wc && "
50
+ end.join.chomp(" && ")
48
51
  end
49
52
 
50
- def is_valid?
51
- @is_valid
53
+ # create string queries
54
+ # this sets string to all fields not already matched
55
+ # by create_filter_query
56
+ # 'test' => "( kMDItemTitle == 'holy'wc || kMDItemMusicalGenre == 'holy'wc )"
57
+ # if kMDItemTitle and kMDItemMusicalGenre are the only fields left open.
58
+ # returns "" if given nil
59
+ # if given multiple strings:
60
+ # 'holy','test' =>
61
+ # ( kMDItemTitle == 'holy'wc || kMDItemMusicalGenre == 'holy'wc ) && ( kMDItemTitle == 'test'wc || kMDItemMusicalGenre == 'test'wc )
62
+ def create_string_query(strings,mappings)
63
+ return "" unless strings
64
+ strings.collect do |keyword|
65
+ query = mappings.collect { |key| "%s == '%s'wc" % [key,keyword] }.join(" || ")
66
+ "( #{query} )"
67
+ end.join(" && ")
52
68
  end
53
69
 
54
- def filter?(str)
55
- return false unless str.index ":"
56
- a,b = *(str.split(":"))
57
- # Check if filter is valid
58
- return MAPPINGS.keys.member?(a)
70
+ # create spotlight queries
71
+ # with {:album => 'test'},"" =>
72
+ # "kMDItemContentTypeTree == 'public.audio' && kMDItemAlbum == 'test'wc"
73
+ def create_spotlight_query(filter_query,string_query)
74
+ ["kMDItemContentTypeTree == 'public.audio'", filter_query, string_query].select do |s|
75
+ s.length > 0
76
+ end.join(" && ")
59
77
  end
60
78
 
61
79
  # Use extension to query spotlight
62
80
  def execute_spotlight_query
63
81
  return DhunExt.query_spotlight(@spotlight_query)
64
82
  end
83
+
84
+ def is_valid?; @is_valid; end
85
+
65
86
  end
66
87
  end
data/lib/dhun/result.rb CHANGED
@@ -1,31 +1,31 @@
1
1
  require 'json'
2
2
  module Dhun
3
3
  class Result
4
+ attr_reader :data
4
5
 
5
6
  def initialize(result, message, options = {})
6
- @response = { :result => result,:message => message}
7
- @response.merge!(options)
7
+ @data = { :result => result.to_sym, :message => message }.merge(options)
8
8
  end
9
9
 
10
10
  def success?
11
- @response[:result].to_sym == :success
11
+ @data[:result] == :success
12
12
  end
13
13
 
14
14
  def error?
15
- @response[:result].to_sym == :error
15
+ @data[:result] == :error
16
16
  end
17
-
17
+
18
18
  def [](sym)
19
- @response[sym] || @response[sym.to_s]
19
+ @data[sym.to_sym] || @data[sym.to_s]
20
20
  end
21
21
 
22
22
  def to_json
23
- @response.to_json
23
+ @data.to_json
24
24
  end
25
25
 
26
26
  def self.from_json_str(resp_json)
27
27
  resp = JSON.parse(resp_json)
28
- Result.new(resp["result"],resp["message"],resp)
28
+ Result.new resp.delete('result'), resp.delete('message'), resp
29
29
  end
30
30
 
31
31
  end
data/lib/dhun/runner.rb CHANGED
@@ -1,104 +1,222 @@
1
- require 'optparse'
1
+ require 'thor'
2
+ require 'json'
2
3
 
3
4
  module Dhun
4
5
 
5
- # Heavily lifted from Thin codebase
6
- class Runner
7
- COMMANDS = %w(start query)
8
- CLIENT_COMMANDS = %w(stop play pause resume next prev enqueue status shuffle history)
9
- # Parsed options
10
- attr_accessor :options
11
-
12
- # Name of the command to be runned.
13
- attr_accessor :command
14
-
15
- # Arguments to be passed to the command.
16
- attr_accessor :arguments
17
-
18
- # Return all available commands
19
- def self.commands
20
- commands = COMMANDS + CLIENT_COMMANDS
21
- commands
22
- end
23
-
24
- def initialize(argv)
25
- @argv = argv
26
- # Default options values
27
- @options = {
28
- :socket => "/tmp/dhun.sock",
29
- :default_log => "/tmp/dhun.log"
30
- }
31
- parse!
32
- end
33
-
34
- def parser
35
- # NOTE: If you add an option here make sure the key in the +options+ hash is the
36
- # same as the name of the command line option.
37
- # +option+ keys are used to build the command line to launch other processes,
38
- # see <tt>lib/dhun/command.rb</tt>.
39
- @parser ||= OptionParser.new do |opts|
40
- opts.banner = <<-EOF
41
- Usage:
42
- dhun start
43
- dhun play spirit
44
- dhun pause
45
- dhun resume
46
- dhun enqueue rahman
47
- dhun status
48
- dhun shuffle
49
- dhun stop
50
-
51
- For more details see README at http://github.com/deepakjois/dhun
52
- EOF
53
- opts.separator ""
54
- opts.on("-d", "--daemonize", "Run daemonized in the background") { @options[:daemonize] = true }
55
- opts.on("-l", "--log FILE", "File to redirect output " +
56
- "(default: #{@options[:default_log]})") { |file| @options[:log] = file }
57
-
58
- opts.separator "Common options:"
59
- opts.on_tail("-h", "--help", "Show this message") { puts opts; exit }
60
- opts.on_tail("-D", "--debug", "Set debugging on") { @options[:debug] = true }
61
- opts.on_tail("-h", "--help", "Show this message") { puts opts; exit }
62
- opts.on_tail('-v', '--version', "Show version") { puts "Dhun " + Dhun::VERSION; exit }
6
+ class Runner < Thor
7
+ include Thor::Actions
8
+ include Dhun::Client
63
9
 
10
+ def self.banner(task)
11
+ task.formatted_usage(self).gsub("dhun:runner:","dhun ")
12
+ end
13
+
14
+ desc "start_server","Starts the Dhun Server."
15
+ method_option :socket, :type => :string, :default => "/tmp/dhun.sock", :aliases => '-s'
16
+ method_option :log, :type => :string, :default => "/tmp/dhun.log", :aliases => '-l'
17
+ method_option :foreground, :type => :boolean, :default => false, :aliases => '-f'
18
+ method_option :debug, :type => :boolean, :default => false, :aliases => '-D'
19
+ def start_server
20
+ unless server_running?(options[:socket],:silent)
21
+ server_path = File.join File.dirname(__FILE__), 'server.rb'
22
+ cmd = options[:foreground] ? 'run' : 'start'
23
+ say "Starting Dhun", :green
24
+ system("ruby #{server_path} #{cmd} -- #{options[:socket]} #{options[:log]}")
25
+ else
26
+ say "Dhun Server is already running", :yellow
64
27
  end
65
28
  end
66
29
 
67
- def parse!
68
- parser.parse! @argv
69
- @command = @argv.shift
70
- @arguments = @argv
30
+ desc "stop_server","Stop the Dhun Server"
31
+ def stop_server
32
+ server_path = File.join File.dirname(__FILE__), 'server.rb'
33
+ system("ruby #{server_path} stop")
34
+ say "Stopping Dhun", :green
71
35
  end
72
36
 
73
- # Parse the current shell arguments and run the command.
74
- # Exits on error.
75
- def run!
76
- logger = Logger.instance
77
- logger.log_level = :debug if @options[:debug]
78
- if self.class.commands.include?(@command)
79
- if CLIENT_COMMANDS.include?(@command)
80
- unless DhunClient.is_dhun_server_running?(@options[:socket])
81
- puts "Please start Dhun server first with : dhun start"
82
- exit 1
83
- end
37
+ desc "query SEARCH","Show files matching query. ex: dhun query bob,'the marley'
38
+ \t\t\t See docs for details on query syntax"
39
+ method_option :artist, :type => :string, :aliases => '-a'
40
+ method_option :album, :type => :string, :aliases => '-l'
41
+ method_option :genre, :type => :string, :aliases => '-g'
42
+ method_option :file, :type => :string, :aliases => '-f'
43
+ method_option :title, :type => :string, :aliases => '-t'
44
+ def query(search=nil)
45
+ search = search.nil? ? nil : search.split(',')
46
+ query = Dhun::Query.new(search,options)
47
+ if query.is_valid?
48
+
49
+ #make the prompt pretty. i think.
50
+ opts = options.collect {|field,value| "#{field}:#{value}" }.join(" ")
51
+ term = search.nil? ? '[nil]' : search.join(",")
52
+ say "Querying: #{term} | #{opts}", :cyan
53
+
54
+ # commence the query, and respond as so.
55
+ files = query.execute_spotlight_query
56
+ if files.empty?
57
+ say "No Results Found", :red
58
+ else
59
+ say "#{files.size} Results", :green
60
+ say_list files
84
61
  end
85
- run_command
86
- elsif @command.nil?
87
- puts "Command required"
88
- puts @parser
89
- exit 1
90
62
  else
91
- abort "Unknown command: #{@command}. Use one of #{self.class.commands.join(', ')}"
63
+ say "Invalid Query Syntax. See docs for correct syntax", :yellow
64
+ end
65
+ files
66
+ end
67
+
68
+ desc "play SEARCH","Play songs matching query. ex: dhun play bob,'the marley'
69
+ \t\t\t See docs for details on query syntax"
70
+ method_option :artist, :type => :string, :aliases => '-a'
71
+ method_option :album, :type => :string, :aliases => '-l'
72
+ method_option :genre, :type => :string, :aliases => '-g'
73
+ method_option :file, :type => :string, :aliases => '-f'
74
+ method_option :title, :type => :string, :aliases => '-t'
75
+ def play(search=nil)
76
+ return return_response(:play,[]) if search.nil? and options.empty?
77
+ return_response(:clear,[])
78
+ invoke :enqueue, [search], options
79
+ end
80
+
81
+ desc "enqueue SEARCH","Enqueue songs matching query. ex: dhun enqueue bob,'the marley'
82
+ \t\t\t See docs for details on query syntax"
83
+ method_option :artist, :type => :string, :aliases => '-a'
84
+ method_option :album, :type => :string, :aliases => '-l'
85
+ method_option :genre, :type => :string, :aliases => '-g'
86
+ method_option :file, :type => :string, :aliases => '-f'
87
+ method_option :title, :type => :string, :aliases => '-t'
88
+ def enqueue(search=nil)
89
+
90
+ # invoke query command and return us all the files found.
91
+ files = invoke :query, [search], options
92
+ if files and !files.empty?
93
+ #prompt for index of song to play and return it in pretty format. cough.
94
+ if files.size == 1 # Dont prompt if result size is 1
95
+ indexes = [0]
96
+ else
97
+ answer = ask "Enter index to queue (ENTER to select all): ",:yellow
98
+
99
+ indexes ||=
100
+ case
101
+ when answer.include?(',') then answer.split(',')
102
+ when answer.include?(' ') then answer.split(' ')
103
+ when answer.size >= 1 then answer.to_a
104
+ else
105
+ 0..(files.size - 1)
106
+ end
107
+ end
108
+ selected = indexes.map { |index| files[index.to_i] }
109
+ say "selected:",:green
110
+ say_list selected
111
+
112
+ return_response(:enqueue,nil,selected)
92
113
  end
93
114
  end
94
115
 
95
- def run_command
96
- controller = Controller.new(@options)
97
- begin
98
- controller.send(@command,*@arguments)
99
- rescue ArgumentError
100
- abort "Illegal arguments passed to #{@command}"
116
+ desc "next COUNT", "Skips to next song by COUNT"
117
+ def next(count=1)
118
+ return_response(:next,[],count.to_i)
119
+ end
120
+
121
+ desc "prev COUNT", "Skips to previous song by COUNT"
122
+ def prev(count=1)
123
+ return_response(:prev,[],count.to_i)
124
+ end
125
+
126
+ desc "status", "Shows the status"
127
+ def status
128
+ return unless server_running?
129
+ response = return_response(:status,[:current,:queue])
130
+ say "Currently Playing:",:magenta
131
+ say response[:current],:white
132
+ say "Queue:",:cyan
133
+ say_list response[:queue]
134
+ end
135
+
136
+ desc "history", "Shows the previously played songs"
137
+ def history
138
+ response = return_response(:history,[:history])
139
+ if response[:history]
140
+ say "History:",:cyan
141
+ say_list response[:history]
142
+ end
143
+ end
144
+
145
+ desc "shuffle", "Shuffles the queue"
146
+ def shuffle
147
+ response = return_response(:shuffle,[:queue])
148
+ if response[:queue]
149
+ say "Queue:",:cyan
150
+ say_list response[:queue]
101
151
  end
102
152
  end
153
+
154
+ desc "pause", "Pauses playing"
155
+ def pause
156
+ return_response(:pause,[])
157
+ end
158
+
159
+ desc "resume", "Resumes playing"
160
+ def resume
161
+ return_response(:resume,[])
162
+ end
163
+
164
+ desc "stop", "Stops playing"
165
+ def stop
166
+ return_response(:stop,[])
167
+ end
168
+
169
+
170
+ private
171
+
172
+ # sends command to dhun client
173
+ def send_command(command,arguments=[])
174
+ cmd = { "command" => command.to_s, "arguments" => arguments }.to_json
175
+ send_message(cmd,"/tmp/dhun.sock")
176
+ end
177
+
178
+ # send command to the server and retrieve response.
179
+ def get_response(command,arguments=[])
180
+ if server_running?
181
+ resp = send_command(command,arguments)
182
+ return Dhun::Result.from_json_str(resp)
183
+ end
184
+ end
185
+
186
+ # prints out list with each index value
187
+ # in pretty format! (contrasting colors)
188
+ def say_list(list)
189
+ list.each_with_index do |item,index|
190
+ color = index.even? ? :white : :cyan
191
+ say("#{index} : #{item}",color)
192
+ end
193
+ end
194
+
195
+ # check to see if Dhun Server is running.
196
+ # asks to start Dhun server if not
197
+ # takes argument :silent to quiet its output.
198
+ # need to make the socket choices more flexible
199
+ def server_running?(socket = "/tmp/dhun.sock",verbose = :verbose)
200
+ socket ||= "/tmp/dhun.sock"
201
+ if is_server?(socket)
202
+ return true
203
+ else
204
+ say("Please start Dhun server first with : dhun start_server", :red) unless verbose == :silent
205
+ return false
206
+ end
207
+ end
208
+
209
+ #send out the command to server and see what it has to say.
210
+ def return_response(action,keys,argument=[])
211
+ response = get_response(action,argument)
212
+ if response
213
+ color = response.success? ? :red : :cyan
214
+ say response[:message], color
215
+ if keys
216
+ return keys.inject({}) {|base,key| base[key.to_sym] = response[key.to_sym] ; base}
217
+ end
218
+ end
219
+ end
220
+
103
221
  end
104
222
  end