changeset 217:1d4e406d19e6

add new ascoltami that uses mpd
author drewp@bigasterisk.com
date Sun, 17 Apr 2005 05:35:01 +0000
parents 233ee9b1e561
children 65ed570b7ba8
files bin/ascoltami
diffstat 1 files changed, 331 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/ascoltami	Sun Apr 17 05:35:01 2005 +0000
@@ -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("<Configure>",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("<Motion>", set_mouse_state)
+    scl.bind("<B1-Motion>",set_mouse_state)
+
+    def b1down(evt):
+        scl.mouse_state = 1
+    def b1up(evt):
+        scl.mouse_state = 0
+    scl.bind("<ButtonPress-1>", b1down)
+    scl.bind("<ButtonRelease-1>", 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, "<Control-s>"),
+                ('Pause', player.play_pause_toggle, "<Control-p>"),
+                ('Skip Intro',lambda: player.seek_to(-.5), "<Control-i>"),
+                ):
+    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("<Destroy>",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 != '<string>':
+            print co.co_filename, co.co_name #, co_firstlineno
+
+    return func_tracer
+
+# sys.settrace(func_tracer)
+
+reactor.run()