fcf9ff3ee1997f908f19744ff5aa5f51658a7059
[jamaendo] / jamaui / playerwindow.py
1 #!/usr/bin/env python
2 #
3 # This file is part of Jamaendo.
4 # Copyright (c) 2010 Kristoffer Gronlund
5 #
6 # Jamaendo is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # Jamaendo is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with Jamaendo.  If not, see <http://www.gnu.org/licenses/>.
18 #
19 # Player code heavily based on http://thpinfo.com/2008/panucci/:
20 #  A resuming media player for Podcasts and Audiobooks
21 #  Copyright (c) 2008-05-26 Thomas Perl <thpinfo.com>
22 #  (based on http://pygstdocs.berlios.de/pygst-tutorial/seeking.html)
23 #
24 import gtk
25 import gobject
26 try:
27     import hildon
28 except:
29     import helldon as hildon
30 import util
31 import datetime
32 import pango
33 import jamaendo
34 from settings import settings
35 from postoffice import postoffice
36 from player import Playlist, the_player
37 import logging
38 import cgi
39
40 from songposition import SongPosition
41 from listbox import ListDialog
42 import colors
43 log = logging.getLogger(__name__)
44
45 class PlayerWindow(hildon.StackableWindow):
46     instance = None
47
48     def __init__(self):
49         hildon.StackableWindow.__init__(self)
50         self.set_title("Jamaendo")
51
52         PlayerWindow.instance = self
53
54         self.connect('destroy', self.on_destroy)
55
56         self.player = the_player
57         self.playlist = the_player.playlist
58
59         vbox = gtk.VBox(False, 8)
60
61         hbox = gtk.HBox(False)
62
63         self.cover = gtk.Image()
64         self.set_default_cover()
65
66         vbox2 = gtk.VBox()
67
68         self.playlist_pos = gtk.Label()
69         self.playlist_pos.set_alignment(1.0,0)
70         self.track = gtk.Label()
71         self.track.set_alignment(0,1)
72         self.track.set_ellipsize(pango.ELLIPSIZE_END)
73         self.artist = gtk.Label()
74         self.artist.set_alignment(0,0.5)
75         self.artist.set_ellipsize(pango.ELLIPSIZE_END)
76         self.album = gtk.Label()
77         self.album.set_alignment(0,0)
78         self.album.set_ellipsize(pango.ELLIPSIZE_END)
79         self.playtime = gtk.Label()
80         self.playtime.set_alignment(1,0)
81         self.playtime.set_ellipsize(pango.ELLIPSIZE_END)
82         self.progress = SongPosition()
83
84         self.set_labels('', '', '', 0, 0)
85
86         self._position_timer = None
87
88         vbox2.pack_start(self.playlist_pos, False)
89         vbox2.pack_start(self.track, True)
90         vbox2.pack_start(self.artist, True)
91         vbox2.pack_start(self.album, True)
92         vbox2.pack_start(self.playtime, False, True)
93         vbox2.pack_start(self.progress, False, True)
94
95         hbox.set_border_width(8)
96         hbox.pack_start(self.cover, False, True, 8)
97         hbox.pack_start(vbox2, True, True, 0)
98
99         vbox.pack_start(hbox, True, True, 0)
100
101         btns = gtk.HButtonBox()
102         btns.set_property('layout-style', gtk.BUTTONBOX_SPREAD)
103
104         vbox.pack_start(btns, False, True, 0)
105
106         self.add_stock_button(btns, gtk.STOCK_MEDIA_PREVIOUS, self.on_prev)
107         self.add_play_button(btns)
108         self.add_stock_button(btns, gtk.STOCK_MEDIA_STOP, self.on_stop)
109         self.add_stock_button(btns, gtk.STOCK_MEDIA_NEXT, self.on_next)
110
111         self.controls = btns
112
113         self.add(vbox)
114
115         postoffice.connect('album-cover', self, self.set_album_cover)
116         postoffice.connect(['next', 'prev', 'play', 'pause', 'stop'], self, self.on_state_changed)
117
118         self.on_state_changed()
119
120         self.create_menu()
121
122     def create_menu(self):
123         self.menu = hildon.AppMenu()
124
125         def to_artist():
126             from showartist import ShowArtist
127             try:
128                 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
129                 track = self.playlist.current()
130                 artist = jamaendo.get_artist(int(track.artist_id))
131                 wnd = ShowArtist(artist)
132                 wnd.show_all()
133             finally:
134                 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
135         def to_album():
136             from showalbum import ShowAlbum
137             try:
138                 hildon.hildon_gtk_window_set_progress_indicator(self, 1)
139                 track = self.playlist.current()
140                 album = jamaendo.get_album(int(track.album_id))
141                 wnd = ShowAlbum(album)
142                 wnd.show_all()
143             finally:
144                 hildon.hildon_gtk_window_set_progress_indicator(self, 0)
145
146         b = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
147         b.set_label("Artist")
148         b.connect("clicked", lambda w: gobject.idle_add(to_artist))
149         self.menu.append(b)
150
151         b = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
152         b.set_label("Album")
153         b.connect("clicked", lambda w: gobject.idle_add(to_album))
154         self.menu.append(b)
155
156         b = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
157         b.set_label("Add to playlist")
158         b.connect("clicked", self.on_add_to_playlist)
159         self.menu.append(b)
160
161         self.menu.show_all()
162         self.set_app_menu(self.menu)
163
164     def on_state_changed(self, *args):
165         self.update_state()
166         self.update_play_button()
167
168         if self.player.playing():
169             self.start_position_timer()
170         else:
171             self.stop_position_timer()
172
173
174     def get_album_id(self):
175         if self.player.playlist and self.player.playlist.current():
176             c = self.player.playlist.current()
177             if not c.album_id:
178                 c.load()
179             if c.album_id:
180                 return c.album_id
181         return None
182
183     def on_destroy(self, wnd):
184         PlayerWindow.instance = None
185         self.stop_position_timer()
186         postoffice.disconnect(['album-cover', 'next', 'prev', 'play', 'stop'], self)
187
188     def add_stock_button(self, btns, stock, cb):
189         btn = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
190         btn.set_relief(gtk.RELIEF_NONE)
191         btn.set_focus_on_click(False)
192         sz = gtk.ICON_SIZE_BUTTON
193         btn.set_image(gtk.image_new_from_stock(stock, sz))
194         btn.connect('clicked', cb)
195         btns.add(btn)
196
197     def add_play_button(self, btns):
198         sz = gtk.ICON_SIZE_BUTTON
199         self.playimg = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, sz)
200         self.pauseimg = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PAUSE, sz)
201         btn = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
202         btn.set_relief(gtk.RELIEF_NONE)
203         if self.player.playing():
204             btn.set_image(self.pauseimg)
205             btn.set_data('state', 'pause')
206         else:
207             btn.set_image(self.playimg)
208             btn.set_data('state', 'play')
209         btn.connect('clicked', self.on_play)
210         btns.add(btn)
211         self.playbtn = btn
212
213     def update_play_button(self):
214         if self.player.playing():
215             self.playbtn.set_image(self.pauseimg)
216             self.playbtn.set_data('state', 'pause')
217         else:
218             self.playbtn.set_image(self.playimg)
219             self.playbtn.set_data('state', 'play')
220
221     def set_labels(self, track, artist, album, playlist_pos, playlist_size):
222
223         if self.playlist.radio_mode:
224             ppstr = 'Radio: %s'%(cgi.escape(self.playlist.radio_name))
225         elif playlist_size > 0:
226             ppstr = 'Track %s of %s'%(int(playlist_pos)+1, playlist_size)
227         else:
228             ppstr = "No playlist"
229         ppstr = '<span font_desc="%s" foreground="%s">%s</span>'%(colors.SmallSystemFont(), colors.SecondaryTextColor(), ppstr)
230         self.playlist_pos.set_markup(ppstr)
231         self.track.set_markup('<span font_desc="%s">%s</span>'%(colors.LargeSystemFont(), cgi.escape(track)))
232         self.artist.set_markup('%s'%(cgi.escape(artist)))
233         self.album.set_markup('<span foreground="%s">%s</span>'%(colors.SecondaryTextColor(), cgi.escape(album)))
234
235         if not track:
236             txt = '<span font_desc="%s" foreground="%s">%s</span>' % \
237                 (colors.XXLargeSystemFont(),
238                  colors.SecondaryTextColor(),
239                  '00:00'
240                  )
241             self.playtime.set_markup(txt)
242
243     def show_banner(self, message, timeout = 2000):
244         banner = hildon.hildon_banner_show_information(self, '', message)
245         banner.set_timeout(2000)
246
247     def on_add_to_playlist(self, button, user_data=None):
248         track = self.player.playlist.current()
249         from playlists import add_to_playlist
250         add_to_playlist(self, track)
251
252     def volume_changed_hildon(self, widget):
253         settings.volume = widget.get_level()/100.0
254
255     def mute_toggled(self, widget):
256         if widget.get_mute():
257             settings.volume = 0
258         else:
259             settings.volume = widget.get_level()/100.0
260
261     def on_position_timeout(self):
262         if the_player.playing():
263             self.set_song_position(*the_player.get_position_duration())
264         else:
265             log.debug("position timeout, but not playing")
266         return True
267
268     def start_position_timer(self):
269         if self._position_timer is not None:
270             self.stop_position_timer()
271         self._position_timer = gobject.timeout_add(1000, self.on_position_timeout)
272
273     def stop_position_timer(self):
274         if self._position_timer is not None:
275             gobject.source_remove(self._position_timer)
276             self._position_timer = None
277
278     def clear_position(self):
279         self.progress.set_position(0)
280
281     def nanosecs_to_str(self, ns):
282         time_secs = int(float(ns)/1000000000.0)
283         if time_secs <= 59:
284             return "00:%02d"%(time_secs)
285         time_mins = int(time_secs/60.0)
286         time_secs -= time_mins*60
287         if time_mins <= 59:
288             return "%02d:%02d"%(time_mins, time_secs)
289
290         time_hrs = int(time_mins/60.0)
291         time_mins -= time_hrs*60
292         return "%d:%02d:%02d"%(time_hrs, time_secs, time_mins)
293
294     def set_song_position(self, time_elapsed, total_time):
295         value = (float(time_elapsed) / float(total_time)) if total_time else 0
296         self.progress.set_position(value)
297
298
299         txt = '<span font_desc="%s" foreground="%s">%s</span>' % \
300             (colors.XXLargeSystemFont(),
301              colors.SecondaryTextColor(),
302              self.nanosecs_to_str(time_elapsed)
303              )
304         self.playtime.set_markup(txt)
305
306     def set_default_cover(self):
307         tmp = util.find_resource('album.png')
308         if tmp:
309             log.debug("Setting cover to %s", tmp)
310             self.cover.set_from_file(tmp)
311
312     def update_state(self):
313         item = self.playlist.current()
314         if item:
315             hildon.hildon_gtk_window_set_progress_indicator(self, 0)
316             if not item.name:
317                 item.load()
318             self.set_labels(item.name, item.artist_name, item.album_name,
319                             self.playlist.current_index(), self.playlist.size())
320             self.set_default_cover()
321             postoffice.notify('request-album-cover', int(item.album_id), 300)
322         else:
323             self.set_labels('', '', '', 0, 0)
324             self.set_default_cover()
325         state = bool(item)
326         for btn in self.controls:
327             if isinstance(btn, hildon.GtkButton):
328                 btn.set_sensitive(state)
329
330     def set_album_cover(self, albumid, size, cover):
331         if size == 300:
332             playing = self.get_album_id()
333             if playing and albumid and (int(playing) == int(albumid)):
334                 log.debug("Setting cover to %s", cover)
335                 self.cover.set_from_file(cover)
336
337     def play_radio(self, radio_name, radio_id):
338         playlist = Playlist([])
339         playlist.radio_mode = True
340         playlist.radio_name = radio_name
341         playlist.radio_id = radio_id
342         hildon.hildon_gtk_window_set_progress_indicator(self, 1)
343         self.__play_tracks(playlist)
344         #log.debug("Playlist current: %s, playing? %s",
345         #          playlist.current_index(),
346         #          self.player.playing())
347
348     def play_tracks(self, tracks):
349         self.__play_tracks(tracks)
350
351     def __play_tracks(self, tracks):
352         self.clear_position()
353         if isinstance(tracks, Playlist):
354             self.playlist = tracks
355         else:
356             self.playlist = Playlist(tracks)
357         log.debug("Playing: %s", self.playlist)
358         self.player.stop()
359         self.player.play(self.playlist)
360
361     def on_play(self, button):
362         if not self.player.playing():
363             self.player.play(self.playlist)
364         else:
365             self.player.pause()
366     def on_prev(self, button):
367         self.player.prev()
368
369     def on_next(self, button):
370         self.player.next()
371
372     def on_stop(self, button):
373         self.clear_position()
374         self.player.stop()
375
376 def open_playerwindow():
377     if PlayerWindow.instance:
378         player = PlayerWindow.instance
379         stack = player.get_stack()
380         sz = stack.size()
381         windows = stack.pop(sz)
382         windows.remove(player)
383         windows.append(player)
384         stack.push_list(windows)
385     else:
386         player = PlayerWindow()
387         player.show_all()
388     return player