dhun 0.5.6 → 0.6.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/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