More screens updated to do background fetching
[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(*args):
126             from showartist import ShowArtist
127             track = self.playlist.current()
128             artist = jamaendo.get_artist(int(track.artist_id))
129             wnd = ShowArtist(artist)
130             wnd.show_all()
131         def to_album(*args):
132             from showalbum import ShowAlbum
133             track = self.playlist.current()
134             album = jamaendo.get_album(int(track.album_id))
135             wnd = ShowAlbum(album)
136             wnd.show_all()
137
138         b = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
139         b.set_label("Artist")
140         b.connect("clicked", to_artist)
141         self.menu.append(b)
142
143         b = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
144         b.set_label("Album")
145         b.connect("clicked", to_album)
146         self.menu.append(b)
147
148         b = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
149         b.set_label("Add to playlist")
150         b.connect("clicked", self.on_add_to_playlist)
151         self.menu.append(b)
152
153         self.menu.show_all()
154         self.set_app_menu(self.menu)
155
156     def on_state_changed(self, *args):
157         self.update_state()
158         self.update_play_button()
159
160         if self.player.playing():
161             self.start_position_timer()
162         else:
163             self.stop_position_timer()
164
165
166     def get_album_id(self):
167         if self.player.playlist and self.player.playlist.current():
168             c = self.player.playlist.current()
169             if not c.album_id:
170                 c.load()
171             if c.album_id:
172                 return c.album_id
173         return None
174
175     def on_destroy(self, wnd):
176         PlayerWindow.instance = None
177         self.stop_position_timer()
178         postoffice.disconnect(['album-cover', 'next', 'prev', 'play', 'stop'], self)
179
180     def add_stock_button(self, btns, stock, cb):
181         btn = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
182         btn.set_relief(gtk.RELIEF_NONE)
183         btn.set_focus_on_click(False)
184         sz = gtk.ICON_SIZE_BUTTON
185         btn.set_image(gtk.image_new_from_stock(stock, sz))
186         btn.connect('clicked', cb)
187         btns.add(btn)
188
189     def add_play_button(self, btns):
190         sz = gtk.ICON_SIZE_BUTTON
191         self.playimg = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, sz)
192         self.pauseimg = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PAUSE, sz)
193         btn = hildon.GtkButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
194         btn.set_relief(gtk.RELIEF_NONE)
195         if self.player.playing():
196             btn.set_image(self.pauseimg)
197             btn.set_data('state', 'pause')
198         else:
199             btn.set_image(self.playimg)
200             btn.set_data('state', 'play')
201         btn.connect('clicked', self.on_play)
202         btns.add(btn)
203         self.playbtn = btn
204
205     def update_play_button(self):
206         if self.player.playing():
207             self.playbtn.set_image(self.pauseimg)
208             self.playbtn.set_data('state', 'pause')
209         else:
210             self.playbtn.set_image(self.playimg)
211             self.playbtn.set_data('state', 'play')
212
213     def set_labels(self, track, artist, album, playlist_pos, playlist_size):
214
215         if self.playlist.radio_mode:
216             ppstr = 'Radio: %s'%(cgi.escape(self.playlist.radio_name))
217         elif playlist_size > 0:
218             ppstr = 'Track %s of %s'%(int(playlist_pos)+1, playlist_size)
219         else:
220             ppstr = "No playlist"
221         ppstr = '<span font_desc="%s" foreground="%s">%s</span>'%(colors.SmallSystemFont(), colors.SecondaryTextColor(), ppstr)
222         self.playlist_pos.set_markup(ppstr)
223         self.track.set_markup('<span font_desc="%s">%s</span>'%(colors.LargeSystemFont(), cgi.escape(track)))
224         self.artist.set_markup('%s'%(cgi.escape(artist)))
225         self.album.set_markup('<span foreground="%s">%s</span>'%(colors.SecondaryTextColor(), cgi.escape(album)))
226
227         if not track:
228             txt = '<span font_desc="%s" foreground="%s">%s</span>' % \
229                 (colors.XXLargeSystemFont(),
230                  colors.SecondaryTextColor(),
231                  '00:00'
232                  )
233             self.playtime.set_markup(txt)
234
235     def show_banner(self, message, timeout = 2000):
236         banner = hildon.hildon_banner_show_information(self, '', message)
237         banner.set_timeout(2000)
238
239     def on_add_to_playlist(self, button, user_data=None):
240         track = self.player.playlist.current()
241         from playlists import add_to_playlist
242         add_to_playlist(self, track)
243
244     def volume_changed_hildon(self, widget):
245         settings.volume = widget.get_level()/100.0
246
247     def mute_toggled(self, widget):
248         if widget.get_mute():
249             settings.volume = 0
250         else:
251             settings.volume = widget.get_level()/100.0
252
253     def on_position_timeout(self):
254         if the_player.playing():
255             self.set_song_position(*the_player.get_position_duration())
256         else:
257             log.debug("position timeout, but not playing")
258         return True
259
260     def start_position_timer(self):
261         if self._position_timer is not None:
262             self.stop_position_timer()
263         self._position_timer = gobject.timeout_add(1000, self.on_position_timeout)
264
265     def stop_position_timer(self):
266         if self._position_timer is not None:
267             gobject.source_remove(self._position_timer)
268             self._position_timer = None
269
270     def clear_position(self):
271         self.progress.set_position(0)
272
273     def nanosecs_to_str(self, ns):
274         time_secs = int(float(ns)/1000000000.0)
275         if time_secs <= 59:
276             return "00:%02d"%(time_secs)
277         time_mins = int(time_secs/60.0)
278         time_secs -= time_mins*60
279         if time_mins <= 59:
280             return "%02d:%02d"%(time_mins, time_secs)
281
282         time_hrs = int(time_mins/60.0)
283         time_mins -= time_hrs*60
284         return "%d:%02d:%02d"%(time_hrs, time_secs, time_mins)
285
286     def set_song_position(self, time_elapsed, total_time):
287         value = (float(time_elapsed) / float(total_time)) if total_time else 0
288         self.progress.set_position(value)
289
290
291         txt = '<span font_desc="%s" foreground="%s">%s</span>' % \
292             (colors.XXLargeSystemFont(),
293              colors.SecondaryTextColor(),
294              self.nanosecs_to_str(time_elapsed)
295              )
296         self.playtime.set_markup(txt)
297
298     def set_default_cover(self):
299         tmp = util.find_resource('album.png')
300         if tmp:
301             log.debug("Setting cover to %s", tmp)
302             self.cover.set_from_file(tmp)
303
304     def update_state(self):
305         item = self.playlist.current()
306         if item:
307             hildon.hildon_gtk_window_set_progress_indicator(self, 0)
308             if not item.name:
309                 item.load()
310             self.set_labels(item.name, item.artist_name, item.album_name,
311                             self.playlist.current_index(), self.playlist.size())
312             self.set_default_cover()
313             postoffice.notify('request-album-cover', int(item.album_id), 300)
314         else:
315             self.set_labels('', '', '', 0, 0)
316             self.set_default_cover()
317         state = bool(item)
318         for btn in self.controls:
319             if isinstance(btn, hildon.GtkButton):
320                 btn.set_sensitive(state)
321
322     def set_album_cover(self, albumid, size, cover):
323         if size == 300:
324             playing = self.get_album_id()
325             if playing and albumid and (int(playing) == int(albumid)):
326                 log.debug("Setting cover to %s", cover)
327                 self.cover.set_from_file(cover)
328
329     def play_radio(self, radio_name, radio_id):
330         playlist = Playlist([])
331         playlist.radio_mode = True
332         playlist.radio_name = radio_name
333         playlist.radio_id = radio_id
334         hildon.hildon_gtk_window_set_progress_indicator(self, 1)
335         self.__play_tracks(playlist)
336         #log.debug("Playlist current: %s, playing? %s",
337         #          playlist.current_index(),
338         #          self.player.playing())
339
340     def play_tracks(self, tracks):
341         self.__play_tracks(tracks)
342
343     def __play_tracks(self, tracks):
344         self.clear_position()
345         if isinstance(tracks, Playlist):
346             self.playlist = tracks
347         else:
348             self.playlist = Playlist(tracks)
349         log.debug("Playing: %s", self.playlist)
350         self.player.stop()
351         self.player.play(self.playlist)
352
353     def on_play(self, button):
354         if not self.player.playing():
355             self.player.play(self.playlist)
356         else:
357             self.player.pause()
358     def on_prev(self, button):
359         self.player.prev()
360
361     def on_next(self, button):
362         self.player.next()
363
364     def on_stop(self, button):
365         self.clear_position()
366         self.player.stop()
367
368 def open_playerwindow():
369     if PlayerWindow.instance:
370         player = PlayerWindow.instance
371         stack = player.get_stack()
372         sz = stack.size()
373         windows = stack.pop(sz)
374         windows.remove(player)
375         windows.append(player)
376         stack.push_list(windows)
377     else:
378         player = PlayerWindow()
379         player.show_all()
380     return player