diff --git a/bin/ascoltami b/bin/ascoltami new file mode 100644 --- /dev/null +++ b/bin/ascoltami @@ -0,0 +1,331 @@ +#!/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.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 + +def buildseeker(master,player): + seeker=tk.Frame(master,bg='black') + + scl=tk.Scale(seeker, 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') + + 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(seeker,text=txt, + relief='raised',bd=1,font='arial 9', + **appstyle).pack(side='left') + l = tk.Label(seeker,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: ( + 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 1: # new dmcc code + if player.current_time.get() < 0 or left_var.get() < 0: + scl['troughcolor'] = 'blue' + else: + scl['troughcolor'] = 'black' + + + player.total_time.trace("w",fixleft) + player.current_time.trace("w",fixleft) + + # 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) + + return seeker + +############################ + +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') +f3=tk.Frame(f2,bg='black') +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(f3,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()) + statebuttons[txt.lower()] = b + +f3.pack(side='top',fill='x') + +def update_state_buttons(*args): + global statebuttons + state = player.state.get() + print "State", state + + if state in ('stop', 'pause'): + statebuttons['pause']['text'] = 'Play' + else: + 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 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' + +player.state.trace_variable('w', update_state_buttons) +update_state_buttons() + +seeker=buildseeker(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) + +def func_tracer(frame, event, arg): + if event == "call": + co = frame.f_code + if 'twisted' not in co.co_filename and \ + 'python2.3' not in co.co_filename and \ + co.co_filename != '': + print co.co_filename, co.co_name #, co_firstlineno + + return func_tracer + +# sys.settrace(func_tracer) + +reactor.run()