00001 from __future__ import with_statement
00002
00003 from plai.playlist import Playlist
00004 from plai.utility import PrettyPrintTime, GreyedOut, ReportError
00005 from plai.collectiontree import CollectionTree
00006 from plai import config
00007
00008 import gtk,gobject,gst
00009 import os,copy,pickle
00010 import gtk.glade
00011
00012 from plai import config
00013
00014
00015 class Player:
00016 """The main player. Handles the collection tree and playlists. Manages
00017 GUI not associated with these."""
00018 def __init__(self):
00019 xml=gtk.glade.XML(config.gladefname)
00020 handlers={'on_mainwindow_destroy' : self._onQuit_onQuit,
00021 'on_SearchBox_changed' : self._onSearch_onSearch,
00022 'on_SearchBox_activate' : self._onSearch_onSearch,
00023 'on_SearchResults_row_activated' : self._onSearchActivate_onSearchActivate,
00024 'on_AddButton_clicked' : self._onAdd_onAdd,
00025 'on_PauseButton_toggled' : self._onPause_onPause,
00026 'on_RemoveButton_clicked' : self._onRemove_onRemove,
00027 'on_VolumeSlider_value_changed' : self._onVolume_onVolume,
00028 'on_SongProgressEventBox_button_press_event' : self._onSeek_onSeek,
00029 'on_ShuffleButton_clicked' : self._onShuffle_onShuffle,
00030 'on_NewButton_clicked' : self._onNew_onNew,
00031 'on_notebook_key_press_event' : self._onNotebookKey_onNotebookKey}
00032 xml.signal_autoconnect(handlers)
00033
00034 self._notebook_notebook=xml.get_widget('notebook')
00035 self._volslider_volslider=xml.get_widget('VolumeSlider')
00036 self._pausebutton_pausebutton=xml.get_widget('PauseButton')
00037 self._searchbox_searchbox=xml.get_widget('SearchBox')
00038 self._songprogress_songprogress=xml.get_widget('SongProgress')
00039
00040 self._searchresults_searchresults=CollectionTree(xml.get_widget('SearchResults'))
00041
00042 gobject.timeout_add(1000,self._onTimer_onTimer,None)
00043
00044 self._playbin_playbin=gst.element_factory_make('playbin','myplayer')
00045 bus=self._playbin_playbin.get_bus()
00046 bus.add_watch(self._onBus_onBus,None)
00047
00048 self._tonotify_tonotify=[]
00049
00050 self._playingplaylist_playingplaylist=None
00051
00052 self._lasttypingtime_lasttypingtime=0
00053
00054 self._mainwin_mainwin=xml.get_widget('MainWindow')
00055 self._mainwin_mainwin.maximize()
00056 self._mainwin_mainwin.show()
00057
00058 self._mainwin_mainwin.set_sensitive(False)
00059
00060
00061 def RegisterForNotifications(self,func):
00062 """Add a function to the list to call when the state changes."""
00063 self._tonotify_tonotify.append(func)
00064
00065
00066 def _SendNotification(self):
00067 """Private. Dispatch notifications. Call me whenever you change
00068 player state and think someone might be interested."""
00069 for f in self._tonotify_tonotify: f()
00070
00071
00072 def GetState(self):
00073 """Returns a representation of the player state suitable for giving
00074 to SetState() thus allowing persistance.
00075 Format is (currenttab,[(label,playliststate)],volume).
00076 TODO - make the bits that peek at private Playlist stuff be a function on Playlist."""
00077 if self._playingplaylist_playingplaylist:
00078 curtab=self._notebook_notebook.page_num(self._playingplaylist_playingplaylist._view.get_data('Scroller'))
00079 else:
00080 curtab=self._notebook_notebook.get_current_page()
00081
00082 playlists=[]
00083 for n in range(self._notebook_notebook.get_n_pages()):
00084 page=self._notebook_notebook.get_nth_page(n)
00085 label=self.GetPageLabelGetPageLabel(page).get_text()
00086 pl=page.get_data('PlaylistObject')
00087 plstate=pl.GetState()
00088 playlists.append((label,plstate))
00089
00090 vol=self._volslider_volslider.get_value()
00091
00092 return (curtab,playlists,vol)
00093
00094
00095 def SetState(self,state):
00096 """Sets the player state to that represented by 'state' as
00097 produced by GetState()."""
00098
00099 def inittab(n):
00100 page=self._notebook_notebook.get_nth_page(n)
00101 pl=page.get_data('PlaylistObject')
00102 (label,plstate)=playlists[n]
00103 pl.SetState(plstate)
00104
00105 (curtab,playlists,vol)=state
00106 self._volslider_volslider.set_value(vol)
00107
00108 for pl in playlists:
00109 self._onNew_onNew(None)
00110 (label,plstate)=pl
00111 page=self._notebook_notebook.get_nth_page(self._notebook_notebook.get_current_page())
00112 self.GetPageLabelGetPageLabel(page).set_text(label)
00113
00114
00115 page.get_data('PlaylistObject')._view.set_sensitive(False)
00116
00117
00118
00119 inittab(curtab)
00120
00121 for n in range(len(playlists)):
00122 if n!=curtab: gobject.idle_add(inittab,n)
00123
00124 self._notebook_notebook.set_current_page(curtab)
00125 page=self._notebook_notebook.get_nth_page(self._notebook_notebook.get_current_page())
00126 self._playingplaylist_playingplaylist=page.get_data('PlaylistObject')
00127 self.StartPlayingStartPlaying()
00128 self.PausePause()
00129
00130 self._mainwin_mainwin.set_sensitive(True)
00131
00132
00133 def GetPlayingPlaylist(self):
00134 """Returns the playlist containing the currently playing song."""
00135 return self._playingplaylist_playingplaylist
00136
00137
00138 def GotoNextTrack(self):
00139 """Move to the next track in the current playlist and start playing it
00140 if possible. Otherwise pause the player."""
00141 if self.GetPlayingPlaylistGetPlayingPlaylist().GotoNextTrack():
00142 self.GetPlayingPlaylistGetPlayingPlaylist().ScrollToCurrent()
00143 self.StartPlayingStartPlaying()
00144 self._SendNotification_SendNotification()
00145 else:
00146 self.PausePause()
00147
00148
00149 def _onBus(self,bus,msg,data):
00150 """Callback. Handles messages from the gstreamer bus.
00151 Responds to EOS by proceeding to next track."""
00152 if msg.type==gst.MESSAGE_EOS:
00153 self.GotoNextTrackGotoNextTrack()
00154 return True
00155 elif msg.type==gst.MESSAGE_ERROR:
00156 ReportError('GStreamer Error',msg.parse_error()[1])
00157
00158 return True
00159 else:
00160
00161 return True
00162
00163
00164 def _onSearch(self,widget):
00165 """Private callback."""
00166
00167 self.RefreshSearchRefreshSearch()
00168
00169
00170 def RefreshSearch(self):
00171 """Refreshes the search results tree using whatever
00172 is in the search box."""
00173 self._searchresults_searchresults.RunQuery(self._searchbox_searchbox.get_text())
00174
00175
00176 def StartPlaying(self):
00177 """Starts playing the current track if it exists."""
00178 self._pausebutton_pausebutton.set_active(False)
00179
00180 self._playbin_playbin.set_state(gst.STATE_NULL)
00181 if self._playingplaylist_playingplaylist.GetCurrentTrack():
00182 self._playbin_playbin.set_property('uri','file://'+self._playingplaylist_playingplaylist.GetCurrentTrack().Filename())
00183 self._playbin_playbin.set_state(gst.STATE_PLAYING)
00184
00185 self._SendNotification_SendNotification()
00186
00187
00188 def _onPlaylistActivate(self,widget,path,column):
00189 """Callback. Skips to the selected item in the current playlist."""
00190 it=widget.get_model().get_iter(path)
00191 self.GetCurrentPlaylistGetCurrentPlaylist().SetCurrentTrack(it)
00192 self._playingplaylist_playingplaylist=self.GetCurrentPlaylistGetCurrentPlaylist()
00193 self.StartPlayingStartPlaying()
00194
00195
00196 def _onSearchActivate(self,widget,path,column):
00197 """Callback. Appends selected search results to the current playlist
00198 and starts playing the first."""
00199 self.GetCurrentPlaylistGetCurrentPlaylist().SetCurrentTrack(self.AppendSearchSelectionToPlaylistAppendSearchSelectionToPlaylist())
00200 self.StartPlayingStartPlaying()
00201
00202
00203 def AppendSearchSelectionToPlaylist(self):
00204 """Appends the selected search results to the current playlist.
00205 Return an iterator to the first track appended."""
00206 tracks=self._searchresults_searchresults.GetSelectedTracks()
00207 tracks=[copy.copy(t) for t in tracks]
00208 return self.GetCurrentPlaylistGetCurrentPlaylist().AppendTracks(tracks)
00209
00210
00211 def _onAdd(self,widget):
00212 """Callback for Add button."""
00213 self.AppendSearchSelectionToPlaylistAppendSearchSelectionToPlaylist()
00214
00215
00216 def Pause(self):
00217 """Pause the currently playing track."""
00218 self._playbin_playbin.set_state(gst.STATE_PAUSED)
00219 self._pausebutton_pausebutton.set_active(True)
00220
00221 self._SendNotification_SendNotification()
00222
00223
00224 def UnPause(self):
00225 """Resume the currently playing track."""
00226 self._playbin_playbin.set_state(gst.STATE_PLAYING)
00227 self._pausebutton_pausebutton.set_active(False)
00228
00229 self._SendNotification_SendNotification()
00230
00231
00232 def IsPaused(self):
00233 """Returns True if the current track is paused."""
00234 return self._pausebutton_pausebutton.get_active()
00235
00236
00237 def _onPause(self,widget):
00238 """Callback for the Pause button. Toggles whether the current song
00239 is paused based on what the button says."""
00240 if widget.get_active():
00241 self.PausePause()
00242 else:
00243 self.UnPauseUnPause()
00244
00245
00246 def _onVolume(self,widget):
00247 """Callback for the volume slider. Sets the gstreamer volume
00248 to what the slider says."""
00249 self._playbin_playbin.set_property('volume',.01*widget.get_value())
00250
00251
00252 def SaveState(self):
00253 """Writes out GetState() to 'config.picklefname'"""
00254 print 'Saving state',
00255 with file(config.picklefname,'w') as f:
00256 pickle.dump(self.GetStateGetState(),f,pickle.HIGHEST_PROTOCOL)
00257 print ' - Done'
00258
00259 def _onQuit(self,widget):
00260 """Callback for the close button. Saves state and calls gtk.main_quit()."""
00261 self.PausePause()
00262 self.SaveStateSaveState()
00263 gtk.main_quit()
00264
00265
00266 def GetSongPosAndLength(self):
00267 """Returns the position in and length of the current track
00268 in seconds as a tuple."""
00269 try:
00270 (pos,junk)=self._playbin_playbin.query_position(gst.FORMAT_TIME)
00271 (length,junk)=self._playbin_playbin.query_duration(gst.FORMAT_TIME)
00272 if pos<0: pos=0
00273 if pos>length: pos=length
00274 return (int(pos/1e9),int(length/1e9))
00275 except:
00276 return (0,1)
00277
00278
00279 def _onTimer(self,data):
00280 """Callback. Every second update the progress bar."""
00281 (pos,length)=self.GetSongPosAndLengthGetSongPosAndLength()
00282
00283 prog=self._songprogress_songprogress
00284 if(prog!=None):
00285 prog.set_fraction(float(pos)/length)
00286 prog.set_text(PrettyPrintTime(pos)+'/'+PrettyPrintTime(length))
00287
00288 self._SendNotification_SendNotification()
00289
00290 return True
00291
00292
00293 def _onRemove(self,widget):
00294 """Callback for the Remove button. Removes the selected tracks
00295 from the current playlist."""
00296 wasplaying=self.GetCurrentPlaylistGetCurrentPlaylist().RemoveSelected()
00297 if wasplaying:
00298 self._playbin_playbin.set_state(gst.STATE_NULL)
00299 self.GetCurrentPlaylistGetCurrentPlaylist().SetCurrentTrack(None)
00300
00301
00302 def _onSeek(self,widget,event):
00303 """Callback for clicking on the song progress bar. Seeks to
00304 the position in the stream that the user clicked."""
00305 (x,y)=widget.window.get_size()
00306 prog=float(event.x)/x;
00307 (length,junk)=self._playbin_playbin.query_duration(gst.FORMAT_TIME)
00308 length=length/1e9
00309 self._playbin_playbin.seek(1.0,gst.FORMAT_TIME,gst.SEEK_FLAG_FLUSH,gst.SEEK_TYPE_SET,prog*length*gst.SECOND,gst.SEEK_TYPE_NONE,0)
00310 self._songprogress_songprogress.set_fraction(prog)
00311
00312
00313 def _onShuffle(self,widget):
00314 """Callback for the Shuffle button. Shuffle the current playlist."""
00315 self.GetCurrentPlaylistGetCurrentPlaylist().Shuffle()
00316
00317
00318 def GetCurrentTrack(self):
00319 """Returns the current track or None if there isn't one."""
00320 pl=self.GetPlayingPlaylistGetPlayingPlaylist()
00321 return pl.GetCurrentTrack() if pl else None
00322
00323
00324 def _onNew(self,widget):
00325 """Callback for the New button. Adds a new notebook page,
00326 and adds the playlist object as data on the page."""
00327 xml=gtk.glade.XML(config.gladeplaylistfname)
00328 handlers={'on_Playlist_row_activated' : self._onPlaylistActivate_onPlaylistActivate,
00329 'on_Close_clicked' : self._onCloseTab_onCloseTab}
00330 xml.signal_autoconnect(handlers)
00331 num=self._notebook_notebook.append_page(xml.get_widget('PlaylistScroller'),
00332 xml.get_widget('PlaylistTitle'))
00333 xml.get_widget('Playlist').set_data('Scroller',xml.get_widget('PlaylistScroller'))
00334 pl=Playlist(xml.get_widget('Playlist'))
00335 xml.get_widget('Close').set_data('ControlledTab',xml.get_widget('PlaylistScroller'))
00336 self._notebook_notebook.get_nth_page(num).set_data('PlaylistObject',pl)
00337 self._notebook_notebook.set_current_page(-1)
00338
00339
00340 def GetCurrentPlaylist(self):
00341 """Returns the Playlist object associated with the currently focussed tab."""
00342 page=self._notebook_notebook.get_nth_page(self._notebook_notebook.get_current_page())
00343 return page.get_data('PlaylistObject') if page else None
00344
00345
00346 def _onCloseTab(self,widget):
00347 """Callback for notebook page close buttons.
00348 Removes the tab in whose title 'widget' is."""
00349 tab=widget.get_data('ControlledTab')
00350
00351
00352
00353 if self._playingplaylist_playingplaylist is tab.get_data('PlaylistObject'):
00354 self._playbin_playbin.set_state(gst.STATE_NULL)
00355
00356 self._notebook_notebook.remove_page(self._notebook_notebook.page_num(tab))
00357
00358
00359 def GetPageLabel(self,page):
00360 """Returns the gtk.Label for 'page' or None."""
00361 return self._notebook_notebook.get_tab_label(page).get_children()[0] if page else None
00362
00363
00364 def _onNotebookKey(self,widget,event):
00365 """Private callback for typing in the notebook. Modified the current
00366 page's title."""
00367 if event.string=='': return
00368 label=self.GetPageLabelGetPageLabel(self._notebook_notebook.get_nth_page(self._notebook_notebook.get_current_page()))
00369 if label:
00370 if event.time-self._lasttypingtime_lasttypingtime>config.notebooktitletimeout:
00371 label.set_text('')
00372 self._lasttypingtime_lasttypingtime=event.time
00373 label.set_text(label.get_text()+event.string)
00374