#!/usr/bin/env python # this is a fork from semprini/ascotalmi to use mpd """ a separate player program from Semprini.py. name means 'listen to me' in italian. features and limitations: xmlrpc interface for: getting the current time in the playing song requesting what song is playing saying what song should play todo: presong and postsong silence never continue through playlist. maybe use 1-song mpd playlists? install pympd, (pypi too) """ from __future__ import division,nested_scopes from optparse import OptionParser import os,math import Tkinter as tk from twisted.internet import reactor,tksupport from twisted.internet.error import CannotListenError from twisted.web import xmlrpc, server import run_local from light9 import networking, showconfig import sys sys.path.append("/home/drewp/projects/pympd") from pympd import Mpd appstyle={'fg':'white','bg':'black'} class XMLRPCServe(xmlrpc.XMLRPC): def __init__(self,player): xmlrpc.XMLRPC.__init__(self) self.player=player def xmlrpc_echo(self,x): return x def xmlrpc_playfile(self,musicfilename): self.player.play(musicfilename) return 'ok' def xmlrpc_stop(self): self.player.state.set('stop') return 'ok' def xmlrpc_gettime(self): """returns seconds from start of song""" return float(self.player.current_time.get()) def xmlrpc_songlength(self): """song length, in seconds""" return float(self.player.total_time.get()) def xmlrpc_songname(self): """song filename, or None""" return self.player.filename or "No song" class Player: """semprini-style access to mpd""" def __init__(self, app, playlist, media=None): self.mpd = Mpd() reactor.connectTCP('dash',6600,self.mpd) self.state = tk.StringVar() self.state.set("stop") # 'stop' 'pause' 'play' self.current_time = tk.DoubleVar() self.total_time = tk.DoubleVar() self.filename_var = tk.StringVar() self.pollStatus() def pollStatus(self): self.mpd.status().addCallback(self.pollStatus2) def pollStatus2(self,stat): for attr1,attr2 in [('state','state'), ('time_elapsed','current_time'), ('time_total','total_time')]: if not hasattr(stat,attr1): continue v = getattr(stat,attr1) if getattr(self,attr2).get() != v: getattr(self,attr2).set(v) self.mpd.currentsong().addCallback(self.pollStatus3) def pollStatus3(self,song): if hasattr(song,'file'): self.filename_var.set(song.file) # if we're stopped, there will be no file, and the UI will # show a stale filename. that means Play might not play the # indicated file (if another mpd client has changed the song # choice). another method is needed to get the file that is # about to play reactor.callLater(.05, self.pollStatus) def play(self, song_path): self.mpd.clear() p = showconfig.songInMpd(song_path) self.mpd.add(p) self.mpd.play() def stop(self): self.mpd.stop() def seek_to(self, time): self.mpd.seek(seconds=time) def play_pause_toggle(self): def finish(status): if status.state == 'play': self.mpd.pause() else: self.mpd.play() self.mpd.status().addCallback(finish) def buildsonglist(root,songfiles,player): songlist=tk.Frame(root,bd=2,relief='raised',bg='black') prefixlen=len(os.path.commonprefix(songfiles)) # include to the last os.sep- dont crop path elements in the middle prefixlen=songfiles[0].rfind(os.sep)+1 maxsfwidth=max([len(x[prefixlen:]) for x in songfiles]) for i,sf in enumerate(songfiles): b=tk.Button(songlist,text=sf[prefixlen:],width=maxsfwidth, anchor='w',pady=0,bd=0,relief='flat', font="arial 17 bold") b.bind("",lambda ev,b=b: b.config(font="arial %d bold" % min(15,int((ev.height-3)*.8)))) try: # rainbow colors frac=i/len(songfiles) b.config(bg='black', fg="#%02x%02x%02x" % tuple([int(255*(.7+.3* math.sin(frac*4+x)) ) for x in 1,2,3])) except Exception,e: print "rainbow failed: %s"%e b.config(command=lambda sf=sf: player.play(sf)) b.pack(side='top',fill='both',exp=1,padx=0,pady=0,ipadx=0,ipady=0) def color_buttons(x, y, z, sf=sf, b=b): name = player.filename_var.get() if name == sf[prefixlen:]: b['bg'] = 'grey50' else: b['bg'] = 'black' player.filename_var.trace("w", color_buttons) return songlist class Seeker(tk.Frame): """scale AND labels below it""" def __init__(self,master,player): tk.Frame.__init__(self,master,bg='black') self.scl = scl = tk.Scale(self, orient="horiz", from_=-4,to_=100, sliderlen=20,width=20, res=0.001, showvalue=0, variable=player.current_time, troughcolor='black', bg='lightblue3', ) scl.pack(fill='x',side='top') self.buildstatus(player) # dragging the scl changes the player time (which updates the scl) # due to mpd having to seemingly play a whole block at every new # seek point, we may want a mode that pauses playback while the # mouse is down (or is moving too fast; or we've sent a seek too # recently) scl.mouse_state=0 def set_mouse_state(evt): scl.mouse_state = evt.state def seeker_cb(time): if scl.mouse_state: player.seek_to(float(time)) scl.config(command=seeker_cb) scl.bind("", set_mouse_state) scl.bind("",set_mouse_state) def b1down(evt): scl.mouse_state = 1 def b1up(evt): scl.mouse_state = 0 scl.bind("", b1down) scl.bind("", b1up) def buildstatus(self,player): left_var=tk.DoubleVar() for txt,var,fmt in (('Current',player.current_time,"%.2f"), ('Song len',player.total_time,"%.2f",), ('Left',left_var, "%.2f"), ('Song',player.filename_var, "%s"), ('State', player.state, "%s")): tk.Label(self,text=txt, relief='raised',bd=1,font='arial 9', **appstyle).pack(side='left') l = tk.Label(self,width=7, anchor='w', text=var.get(), relief='sunken',bd=1,font='arial 12 bold', bg='#800000',fg='white') if txt == 'Song': l.config(anchor='e') l.pack(side='left',expand=1, fill='x') var.trace_variable('w', lambda a,b,c,l=l,fmt=fmt,var=var: l.config(text=fmt % var.get())) # update end time as the song changes player.total_time.trace("w",lambda *args: ( self.scl.config(to_=player.total_time.get()+15))) def fixleft(*args): # update time-left variable left_var.set(player.total_time.get()-player.current_time.get()) if player.current_time.get() < 0 or left_var.get() < 0: self.scl['troughcolor'] = 'blue' else: self.scl['troughcolor'] = 'black' player.total_time.trace("w",fixleft) player.current_time.trace("w",fixleft) class ControlButtons(tk.Frame): def __init__(self,master): tk.Frame.__init__(self,master,bg='black') self.statebuttons = {} # lowercased name : Button for txt,cmd,key in (('Stop', player.stop, ""), ('Pause', player.play_pause_toggle, ""), ('Skip Intro',lambda: player.seek_to(-.5), ""), ): b = tk.Button(self, text=txt, command=cmd, font='arial 16 bold', height=3,**appstyle) b.pack(side='left', fill='x', expand=1) # keyboard bindings root.bind(key, lambda evt, cmd=cmd: cmd()) self.statebuttons[txt.lower()] = b def update_state_buttons(self,*args): state = player.state.get() print "State", state if state in ('stop', 'pause'): self.statebuttons['pause']['text'] = 'Play' else: self.statebuttons['pause']['text'] = 'Pause' colors = {'stop' : 'red', 'play' : 'blue', 'pause' : 'green'} # very confusing -- play and pause supply colors # for each other! for name, button in self.statebuttons.items(): if name == 'pause' and state not in ('stop', 'pause'): name = 'play' if state == name: # name gets changed sometimes for 'pause' -- see right above button['bg'] = colors.get(name, 'black') else: button['bg'] = 'black' ############################ parser=OptionParser() (options,songfiles)=parser.parse_args() if len(songfiles)<1: songfiles = [f for f in os.listdir(showconfig.musicDir()) if f.endswith('wav')] root=tk.Tk() root.wm_title("ascoltami") root.wm_geometry("656x736+263+0") root.config(bg="black") player=Player(None,None) songlist = buildsonglist(root,songfiles,player) songlist.pack(fill='both',exp=1) f2 = tk.Frame(bg='black') buts = ControlButtons(f2) buts.pack(side='top',fill='x') player.state.trace_variable('w', buts.update_state_buttons) buts.update_state_buttons() seeker = Seeker(f2,player) seeker.pack(fill='x',exp=1) f2.pack(side='bottom',fill='x') tksupport.install(root,ms=10) try: reactor.listenTCP(networking.musicPort(),server.Site(XMLRPCServe(player))) print "started server on %s" % networking.musicPort() except CannotListenError: print "no server started- %s is in use" % networking.musicPort() root.bind("",lambda ev: reactor.stop) root.protocol('WM_DELETE_WINDOW', reactor.stop) reactor.run()