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