comparison browser/sat_browser/game_radiocol.py @ 1124:28e3eb3bb217

files reorganisation and installation rework: - files have been reorganised to follow other SàT projects and usual Python organisation (no more "/src" directory) - VERSION file is now used, as for other SàT projects - replace the overcomplicated setup.py be a more sane one. Pyjamas part is not compiled anymore by setup.py, it must be done separatly - removed check for data_dir if it's empty - installation tested working in virtual env - libervia launching script is now in bin/libervia
author Goffi <goffi@goffi.org>
date Sat, 25 Aug 2018 17:59:48 +0200
parents src/browser/sat_browser/game_radiocol.py@f2170536ba23
children 2af117bfe6cc
comparison
equal deleted inserted replaced
1123:63a4b8fe9782 1124:28e3eb3bb217
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 # Libervia: a Salut à Toi frontend
5 # Copyright (C) 2011-2018 Jérôme Poisson <goffi@goffi.org>
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 import pyjd # this is dummy in pyjs
21 from sat.core.log import getLogger
22 log = getLogger(__name__)
23
24 from sat.core.i18n import _, D_
25 from sat_frontends.tools import host_listener
26 from constants import Const as C
27
28 from pyjamas.ui.VerticalPanel import VerticalPanel
29 from pyjamas.ui.HorizontalPanel import HorizontalPanel
30 from pyjamas.ui.FlexTable import FlexTable
31 from pyjamas.ui.FormPanel import FormPanel
32 from pyjamas.ui.Label import Label
33 from pyjamas.ui.Button import Button
34 from pyjamas.ui.ClickListener import ClickHandler
35 from pyjamas.ui.Hidden import Hidden
36 from pyjamas.ui.CaptionPanel import CaptionPanel
37 from pyjamas.media.Audio import Audio
38 from pyjamas import Window
39 from pyjamas.Timer import Timer
40
41 import html_tools
42 import file_tools
43 import dialog
44
45
46 unicode = str # XXX: pyjama doesn't manage unicode
47
48
49 class MetadataPanel(FlexTable):
50
51 def __init__(self):
52 FlexTable.__init__(self)
53 title_lbl = Label("title:")
54 title_lbl.setStyleName('radiocol_metadata_lbl')
55 artist_lbl = Label("artist:")
56 artist_lbl.setStyleName('radiocol_metadata_lbl')
57 album_lbl = Label("album:")
58 album_lbl.setStyleName('radiocol_metadata_lbl')
59 self.title = Label("")
60 self.title.setStyleName('radiocol_metadata')
61 self.artist = Label("")
62 self.artist.setStyleName('radiocol_metadata')
63 self.album = Label("")
64 self.album.setStyleName('radiocol_metadata')
65 self.setWidget(0, 0, title_lbl)
66 self.setWidget(1, 0, artist_lbl)
67 self.setWidget(2, 0, album_lbl)
68 self.setWidget(0, 1, self.title)
69 self.setWidget(1, 1, self.artist)
70 self.setWidget(2, 1, self.album)
71 self.setStyleName("radiocol_metadata_pnl")
72
73 def setTitle(self, title):
74 self.title.setText(title)
75
76 def setArtist(self, artist):
77 self.artist.setText(artist)
78
79 def setAlbum(self, album):
80 self.album.setText(album)
81
82
83 class ControlPanel(FormPanel):
84 """Panel used to show controls to add a song, or vote for the current one"""
85
86 def __init__(self, parent):
87 FormPanel.__init__(self)
88 self.setEncoding(FormPanel.ENCODING_MULTIPART)
89 self.setMethod(FormPanel.METHOD_POST)
90 self.setAction("upload_radiocol")
91 self.timer_on = False
92 self._parent = parent
93 vPanel = VerticalPanel()
94
95 types = [('audio/ogg', '*.ogg', 'Ogg Vorbis Audio'),
96 ('video/ogg', '*.ogv', 'Ogg Vorbis Video'),
97 ('application/ogg', '*.ogx', 'Ogg Vorbis Multiplex'),
98 ('audio/mpeg', '*.mp3', 'MPEG-Layer 3'),
99 ('audio/mp3', '*.mp3', 'MPEG-Layer 3'),
100 ]
101 self.file_upload = file_tools.FilterFileUpload("song", 10, types)
102 vPanel.add(self.file_upload)
103
104 hPanel = HorizontalPanel()
105 self.upload_btn = Button("Upload song", getattr(self, "onBtnClick"))
106 hPanel.add(self.upload_btn)
107 self.status = Label()
108 self.updateStatus()
109 hPanel.add(self.status)
110 #We need to know the filename and the referee
111 self.filename_field = Hidden('filename', '')
112 hPanel.add(self.filename_field)
113 referee_field = Hidden('referee', self._parent.referee)
114 hPanel.add(self.filename_field)
115 hPanel.add(referee_field)
116 vPanel.add(hPanel)
117
118 self.add(vPanel)
119 self.addFormHandler(self)
120
121 def updateStatus(self):
122 if self.timer_on:
123 return
124 # TODO: the status should be different if a song is being played or not
125 queue = self._parent.getQueueSize()
126 queue_data = self._parent.queue_data
127 if queue < queue_data[0]:
128 left = queue_data[0] - queue
129 self.status.setText("[we need %d more song%s]" % (left, "s" if left > 1 else ""))
130 elif queue < queue_data[1]:
131 left = queue_data[1] - queue
132 self.status.setText("[%d available spot%s]" % (left, "s" if left > 1 else ""))
133 elif queue >= queue_data[1]:
134 self.status.setText("[The queue is currently full]")
135 self.status.setStyleName('radiocol_status')
136
137 def onBtnClick(self):
138 if self.file_upload.check():
139 self.status.setText('[Submitting, please wait...]')
140 self.filename_field.setValue(self.file_upload.getFilename())
141 if self.file_upload.getFilename().lower().endswith('.mp3'):
142 self._parent._parent.host.showWarning('STATUS', 'For a better support, it is recommended to submit Ogg Vorbis file instead of MP3. You can convert your files easily, ask for help if needed!', 5000)
143 self.submit()
144 self.file_upload.setFilename("")
145
146 def onSubmit(self, event):
147 pass
148
149 def blockUpload(self):
150 self.file_upload.setVisible(False)
151 self.upload_btn.setEnabled(False)
152
153 def unblockUpload(self):
154 self.file_upload.setVisible(True)
155 self.upload_btn.setEnabled(True)
156
157 def setTemporaryStatus(self, text, style):
158 self.status.setText(text)
159 self.status.setStyleName('radiocol_upload_status_%s' % style)
160 self.timer_on = True
161
162 def cb(timer):
163 self.timer_on = False
164 self.updateStatus()
165
166 Timer(5000, cb)
167
168 def onSubmitComplete(self, event):
169 result = event.getResults()
170 if result == C.UPLOAD_OK:
171 # the song can still be rejected (not readable, full queue...)
172 self.setTemporaryStatus('[Your song has been submitted to the radio]', "ok")
173 elif result == C.UPLOAD_KO:
174 self.setTemporaryStatus('[Something went wrong during your song upload]', "ko")
175 self._parent.radiocolSongRejectedHandler(_("The uploaded file has been rejected, only Ogg Vorbis and MP3 songs are accepted."))
176 # TODO: would be great to re-use the original Exception class and message
177 # but it is lost in the middle of the traceback and encapsulated within
178 # a DBusException instance --> extract the data from the traceback?
179 else:
180 Window.alert(_('Submit error: %s' % result))
181 self.status.setText('')
182
183
184 class Player(Audio):
185
186 def __init__(self, player_id, metadata_panel):
187 Audio.__init__(self)
188 self._id = player_id
189 self.metadata = metadata_panel
190 self.timestamp = ""
191 self.title = ""
192 self.artist = ""
193 self.album = ""
194 self.filename = None
195 self.played = False # True when the song is playing/has played, becomes False on preload
196 self.setAutobuffer(True)
197 self.setAutoplay(False)
198 self.setVisible(False)
199
200 def preload(self, timestamp, filename, title, artist, album):
201 """preload the song but doesn't play it"""
202 self.timestamp = timestamp
203 self.filename = filename
204 self.title = title
205 self.artist = artist
206 self.album = album
207 self.played = False
208 self.setSrc(u"radiocol/%s" % html_tools.html_sanitize(filename))
209 log.debug(u"preloading %s in %s" % (title, self._id))
210
211 def play(self, play=True):
212 """Play or pause the song
213 @param play: set to True to play or to False to pause
214 """
215 if play:
216 self.played = True
217 self.metadata.setTitle(self.title)
218 self.metadata.setArtist(self.artist)
219 self.metadata.setAlbum(self.album)
220 Audio.play(self)
221 else:
222 self.pause()
223
224
225 class RadioColPanel(HorizontalPanel, ClickHandler):
226
227 def __init__(self, parent, referee, players, queue_data):
228 """
229 @param parent
230 @param referee
231 @param players
232 @param queue_data: list of integers (queue to start, queue limit)
233 """
234 # We need to set it here and not in the CSS :(
235 HorizontalPanel.__init__(self, Height="90px")
236 ClickHandler.__init__(self)
237 self._parent = parent
238 self.referee = referee
239 self.queue_data = queue_data
240 self.setStyleName("radiocolPanel")
241
242 # Now we set up the layout
243 self.metadata_panel = MetadataPanel()
244 self.add(CaptionPanel("Now playing", self.metadata_panel))
245 self.playlist_panel = VerticalPanel()
246 self.add(CaptionPanel("Songs queue", self.playlist_panel))
247 self.control_panel = ControlPanel(self)
248 self.add(CaptionPanel("Controls", self.control_panel))
249
250 self.next_songs = []
251 self.players = [Player("player_%d" % i, self.metadata_panel) for i in xrange(queue_data[1] + 1)]
252 self.current_player = None
253 for player in self.players:
254 self.add(player)
255 self.addClickListener(self)
256
257 help_msg = """Accepted file formats: Ogg Vorbis (recommended), MP3.<br />
258 Please do not submit files that are protected by copyright.<br />
259 Click <a style="color: red;">here</a> if you need some support :)"""
260 link_cb = lambda: self._parent.host.bridge.joinMUC(self._parent.host.default_muc, self._parent.nick, profile=C.PROF_KEY_NONE, callback=lambda dummy: None, errback=self._parent.host.onJoinMUCFailure)
261 # FIXME: printInfo disabled after refactoring
262 # self._parent.printInfo(help_msg, type_='link', link_cb=link_cb)
263
264 def pushNextSong(self, title):
265 """Add a song to the left panel's next songs queue"""
266 next_song = Label(title)
267 next_song.setStyleName("radiocol_next_song")
268 self.next_songs.append(next_song)
269 self.playlist_panel.append(next_song)
270 self.control_panel.updateStatus()
271
272 def popNextSong(self):
273 """Remove the first song of next songs list
274 should be called when the song is played"""
275 #FIXME: should check that the song we remove is the one we play
276 next_song = self.next_songs.pop(0)
277 self.playlist_panel.remove(next_song)
278 self.control_panel.updateStatus()
279
280 def getQueueSize(self):
281 return len(self.playlist_panel.getChildren())
282
283 def radiocolCheckPreload(self, timestamp):
284 for player in self.players:
285 if player.timestamp == timestamp:
286 return False
287 return True
288
289 def radiocolPreloadHandler(self, timestamp, filename, title, artist, album, sender):
290 if not self.radiocolCheckPreload(timestamp):
291 return # song already preloaded
292 preloaded = False
293 for player in self.players:
294 if not player.filename or \
295 (player.played and player != self.current_player):
296 #if player has no file loaded, or it has already played its song
297 #we use it to preload the next one
298 player.preload(timestamp, filename, title, artist, album)
299 preloaded = True
300 break
301 if not preloaded:
302 log.warning("Can't preload song, we are getting too many songs to preload, we shouldn't have more than %d at once" % self.queue_data[1])
303 else:
304 self.pushNextSong(title)
305 # FIXME: printInfo disabled after refactoring
306 # self._parent.printInfo(_('%(user)s uploaded %(artist)s - %(title)s') % {'user': sender, 'artist': artist, 'title': title})
307
308 def radiocolPlayHandler(self, filename):
309 found = False
310 for player in self.players:
311 if not found and player.filename == filename:
312 player.play()
313 self.popNextSong()
314 self.current_player = player
315 found = True
316 else:
317 player.play(False) # in case the previous player was not sync
318 if not found:
319 log.error("Song not found in queue, can't play it. This should not happen")
320
321 def radiocolNoUploadHandler(self):
322 self.control_panel.blockUpload()
323
324 def radiocolUploadOkHandler(self):
325 self.control_panel.unblockUpload()
326
327 def radiocolSongRejectedHandler(self, reason):
328 Window.alert("Song rejected: %s" % reason)
329
330
331 ## Menu
332
333 def hostReady(host):
334 def onCollectiveRadio(self):
335 def callback(room_jid, contacts):
336 contacts = [unicode(contact) for contact in contacts]
337 room_jid_s = unicode(room_jid) if room_jid else ''
338 host.bridge.launchRadioCollective(contacts, room_jid_s, profile=C.PROF_KEY_NONE, callback=lambda dummy: None, errback=host.onJoinMUCFailure)
339 dialog.RoomAndContactsChooser(host, callback, ok_button="Choose", title="Collective Radio", visible=(False, True))
340
341
342 def gotMenus():
343 host.menus.addMenu(C.MENU_GLOBAL, (D_(u"Groups"), D_(u"Collective radio")), callback=onCollectiveRadio)
344
345 host.addListener('gotMenus', gotMenus)
346
347 host_listener.addListener(hostReady)