viewing source for "rmpc-notify.rb"

last commit

commit 0a8e6c8bf8e3e930786f4e7516b98b70db782845 Author: Andrew Rader <andrew.r.rader@gmail.com> Date: Sun Feb 3 11:15:09 2008 -0800
incremented version number to 0.2

source code

001: #!/usr/bin/ruby
002: #
003: # Rmpc Notify v0.2
004: # Copyright 2007 Andrew Rader (nymb.us)
005: #
006: # This file is part of Rmpc Notify
007: #
008: # Rmpc Notify is free software; you can redistribute it and/or modify
009: # it under the terms of the GNU General Public License as published by
010: # the Free Software Foundation; either version 2 of the License, or
011: # (at your option) any later version.
012: #
013: # Rmpc Notify is distributed in the hope that it will be useful,
014: # but WITHOUT ANY WARRANTY; without even the implied warranty of
015: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016: # GNU General Public License for more details.
017: #
018: # You should have received a copy of the GNU General Public License
019: # along with Rmpc Notify; if not, write to the Free Software
020: # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
021: #
022: 
023: require 'rubygems'
024: require 'librmpd'
025: require 'rnotify'
026: require 'thread'
027: 
028: # this daemon code was heavily inspired by ciconia's snippet
029: # at the following: http://snippets.dzone.com/posts/show/2265
030: module Daemon
031:     HomeDir = File.expand_path( '~' )
032: 
033:     class Base
034:         def self.pid_file
035:             File.join( HomeDir, ".#{name.downcase}.pid" )
036:         end
037: 
038:         def self.usr1_trap
039:             # do nothing, this can be used in your code
040:         end
041: 
042:         def self.daemonize( cmd, *args )
043:             Controller.daemonize( self, cmd, args )
044:         end
045:     end
046: 
047:     module PidFile
048:         def self.store( daemon, pid )
049:             File.open( daemon.pid_file, 'w' ) do |f|
050:                 f << pid
051:             end
052:         end
053: 
054:         def self.load( daemon )
055:             IO.read( daemon.pid_file ).to_i rescue nil
056:         end
057:     end
058: 
059:     module Controller
060:         def self.daemonize( daemon, cmd, args )
061:             case cmd
062:             when 'start'
063:                 start daemon, args
064:             when 'stop'
065:                 stop daemon
066:             when 'restart'
067:                 stop daemon
068:                 sleep 0.5
069:                 start daemon, args
070:             when 'zap'
071:                 File.delete daemon.pid_file if File.file? daemon.pid_file
072:             when 'usr1'
073:                 if not File.file? daemon.pid_file
074:                     puts "Pid File not found, is the daemon running?"
075:                     exit
076:                 end
077:                 pid = PidFile.load daemon
078:                 pid && Process.kill( "USR1", pid )
079:             else
080:                 puts "Invalid Command (valid: start|stop|restart|zap|usr1)"
081:                 exit
082:             end
083:         end
084: 
085:         def self.start( daemon, args )
086:             if File.file? daemon.pid_file
087:                 puts "Pid file already exists, is the daemon running?"
088:                 puts "Use zap to clear old pid files"
089:                 exit
090:             end
091:             exit if fork                    # Parent exists, child runs
092:             Process.setsid                  # Become session leader
093:             exit if fork                    # Zap session leader
094:             PidFile.store daemon, Process.pid
095:             Dir.chdir "/"                   # Release working dir
096:             File.umask 0000                 # Ensure sensible umask
097:             STDIN.reopen "/dev/null"        # Free file descripters
098:             STDOUT.reopen "/dev/null", "a"
099:             STDERR.reopen STDOUT
100:             trap( "TERM" ) do
101:                 daemon.stop
102:                 exit
103:             end
104:             trap( "SIGUSR1" ) do
105:                 daemon.usr1_trap
106:             end
107:             daemon.start args
108:         end
109: 
110:         def self.stop( daemon )
111:             if not File.file? daemon.pid_file
112:                 puts "Pid File not found, is the daemon running?"
113:                 exit
114:             end
115:             pid = PidFile.load daemon
116:             File.delete daemon.pid_file
117:             pid && Process.kill( "TERM", pid )
118:             return pid
119:         end
120:     end
121: end
122: 
123: class RmpcNotify < Daemon::Base
124: 
125:     PLAY_IMG = "media-playback-start"
126:     PAUSE_IMG = "media-playback-pause"
127:     STOP_IMG = "media-playback-stop"
128: 
129:     CONNECT_IMG = "info"
130:     DISCONNECT_IMG = "error"
131: 
132:     def self.start( args )
133:         @host = args[0]
134:         @port = args[1]
135:         @mpd = MPD.new @host, @port
136: 
137:         @mpd.register_callback( self.method('song_cb'), MPD::CURRENT_SONG_CALLBACK )
138:         @mpd.register_callback( self.method('state_cb'), MPD::STATE_CALLBACK )
139:         @mpd.register_callback( self.method('connection_cb'), MPD::CONNECTION_CALLBACK )
140: 
141:         Notify.init "RmpcNotify"
142: 
143:         @state_note = Notify::Notification.new "State","","",nil
144:         @song_note = Notify::Notification.new "Song","","",nil
145:         @conn_note = Notify::Notification.new "Conn","","",nil
146: 
147:         quit = false
148: 
149:         # Set up the trap for Ctrl-C presses
150:         trap "SIGINT" do
151:             quit = true
152:         end
153: 
154:         @mpd.connect true
155: 
156:         loop do
157:             break if quit
158:             sleep 2
159:         end
160: 
161:         if quit
162:             self.stop
163:         end
164:     end
165: 
166:     def self.stop
167:         puts "Shutting down rmpc-notify"
168:         @mpd.disconnect
169:         Notify.uninit
170:     end
171: 
172:     def self.usr1_trap
173:         if @mpd.connected?
174:             state = @mpd.status['state']
175:             if state != 'stop'
176:                 self.song_cb @mpd.current_song
177:             else
178:                 self.state_cb state
179:             end
180:         else
181:             self.connection_cb false
182:         end
183:     end
184: 
185:     def self.output_song( song )
186:         ret = "<b>"
187:         if not song.artist.nil?
188:             ret += "#{song.artist} - "
189:         end
190: 
191:         if not song.title.nil?
192:             ret += "#{song.title}"
193:         end
194: 
195:         if ret.empty?
196:             ret = song.file
197:         end
198: 
199:         status = @mpd.status
200: 
201:         return ret if status.nil?
202: 
203:         size = status['playlistlength']
204:         time = status['time'].split ':'
205: 
206:         e_min = time[0].to_i / 60
207:         e_hrs = e_min / 60
208:         e_min = e_min % 60
209:         e_sec = time[0].to_i % 60
210: 
211:         e_sec = "0#{e_sec}" if e_sec < 10
212: 
213:         hrs = e_hrs == 0 ? "" : "#{e_hrs}:"
214: 
215:         ret += "</b>\n\nTime: #{hrs}#{e_min}:#{e_sec}\n"
216:         ret += "Bitrate: #{status['bitrate']} kbps\n"
217:         ret += "Playlist: #{song.pos} of #{size}"
218: 
219:         return ret
220:     end
221: 
222:     def self.state_cb( newstate )
223:         @conn_note.close
224:         @state_note.close
225:         case newstate
226:             when 'play'
227:                 song = @mpd.current_song
228:                 @state_note.update "MPD: Playing",output_song(song),PLAY_IMG
229:             when 'pause'
230:                 song = @mpd.current_song
231:                 @state_note.update "MPD: Paused",output_song(song),PAUSE_IMG
232:             when 'stop'
233:                 @state_note.update "MPD: Stopped",nil,STOP_IMG
234:             else
235:                 return
236:         end
237:         @state_note.show
238:     end
239: 
240:     def self.song_cb( current )
241:         return if current.nil?
242:         status = @mpd.status
243:         return if status.nil? or status['state'] != 'play'
244:         @conn_note.close
245:         @state_note.close
246:         @song_note.close
247:         @song_note.update "Now Playing:",output_song(current),"audio-x-generic"
248:         @song_note.show
249:     end
250: 
251:     def self.connection_cb( connected )
252:         @conn_note.close
253:         if not connected
254:             @conn_note.update "MPD: Disconnected",
255:                 "Disconnected from MPD at '#{@host}:#{@port}'",
256:                 DISCONNECT_IMG
257:             @conn_note.urgency = Notify::Notification::URGENCY_CRITICAL
258:             @conn_note.show
259:         else
260:             @conn_note.update "MPD: Connected",
261:                 "Successfully connected to MPD at '#{@host}:#{@port}'",
262:                 CONNECT_IMG
263:             @conn_note.urgency = Notify::Notification::URGENCY_NORMAL
264:             @conn_note.show
265:         end
266:     end
267: end
268: 
269: def _rmpc_usage
270:     puts "rmpc-notify.rb usage:\n"
271:     puts "\t-h host - MPD hostname (default: localhost)\n"
272:     puts "\t-p port - MPD port (default: 6600)\n"
273:     puts "\t-d      - enable daemon mode (default: off)\n"
274:     puts "\t-s      - Tells the currently running daemon to\n"
275:     puts "\t          display the current song\n"
276:     puts "\t-k      - stop an already running copy\n"
277:     puts "\t--help  - print this help\n"
278:     puts "\nCopyright 2007 Andrew Rader"
279: end
280: 
281: host = 'localhost'
282: port = 6600
283: daemon = false
284: kill = false
285: 
286: i = 0
287: loop do
288:     break if i >= ARGV.length
289:     case ARGV[i]
290:         when '-p'
291:             i+=1
292:             port = ARGV[i]
293:         when '-h'
294:             i+=1
295:             host = ARGV[i]
296:         when '-d'
297:             daemon = true
298:         when '-k'
299:             pid = RmpcNotify.daemonize 'stop'
300:             puts "Stopped rmpc-notify (pid #{pid})"
301:             exit
302:         when '-z'
303:             RmpcNotify.daemonize 'zap'
304:             puts "Zapped (removed) old pid file"
305:             exit
306:         when '-s'
307:             RmpcNotify.daemonize 'usr1'
308:             exit
309:         when '--help'
310:             _rmpc_usage
311:             exit
312:         else
313:             _rmpc_usage
314:             exit
315:     end
316:     i+=1
317: end
318: 
319: if host.nil? or port.nil?
320:     _rmpc_usage
321:     exit
322: end
323: 
324: if daemon
325:     RmpcNotify.daemonize 'start', host, port
326: else
327:     RmpcNotify.start [host,port]
328: end