# HG changeset patch # User souliane # Date 1385663039 -3600 # Node ID 539f278bc265355dff4eb014695c7b561aa247c7 # Parent 812dc38c00940bc6082bf127398fe480221539ea plugin room_games, radiocol: send the current queue to new players diff -r 812dc38c0094 -r 539f278bc265 src/plugins/plugin_misc_radiocol.py --- a/src/plugins/plugin_misc_radiocol.py Tue Dec 10 09:02:20 2013 +0100 +++ b/src/plugins/plugin_misc_radiocol.py Thu Nov 28 19:23:59 2013 +0100 @@ -23,6 +23,8 @@ from twisted.words.protocols.jabber import jid import os.path +import copy +import time from os import unlink from mutagen.oggvorbis import OggVorbis, OggVorbisHeaderError @@ -41,6 +43,10 @@ "description": _("""Implementation of radio collective""") } + +# Number of songs needed in the queue before we start playing +QUEUE_TO_START = 2 +# Maximum number of songs in the queue (the song being currently played doesn't count) QUEUE_LIMIT = 2 @@ -61,20 +67,20 @@ host.bridge.addMethod("radiocolCreate", ".plugin", in_sign='sass', out_sign='', method=self.createGame) host.bridge.addMethod("radiocolSongAdded", ".plugin", in_sign='sss', out_sign='', method=self.radiocolSongAdded) host.bridge.addSignal("radiocolPlayers", ".plugin", signature='ssass') # room_jid, referee, players, profile - host.bridge.addSignal("radiocolStarted", ".plugin", signature='ssass') # room_jid, referee, players, profile + host.bridge.addSignal("radiocolStarted", ".plugin", signature='ssasais') # room_jid, referee, players, [QUEUE_TO_START, QUEUE_LIMIT], profile host.bridge.addSignal("radiocolSongRejected", ".plugin", signature='sss') # room_jid, reason, profile - host.bridge.addSignal("radiocolPreload", ".plugin", signature='ssssss') # room_jid, filename, title, artist, album, profile + host.bridge.addSignal("radiocolPreload", ".plugin", signature='sssssss') # room_jid, timestamp, filename, title, artist, album, profile host.bridge.addSignal("radiocolPlay", ".plugin", signature='sss') # room_jid, filename, profile host.bridge.addSignal("radiocolNoUpload", ".plugin", signature='ss') # room_jid, profile host.bridge.addSignal("radiocolUploadOk", ".plugin", signature='ss') # room_jid, profile - def __create_preload_elt(self, sender, filename, title, artist, album): - preload_elt = domish.Element((None, 'preload')) + def __create_preload_elt(self, sender, song_added_elt): + preload_elt = copy.deepcopy(song_added_elt) + preload_elt.name = 'preload' preload_elt['sender'] = sender - preload_elt['filename'] = filename # XXX: the frontend should know the temporary directory where file is put - preload_elt['title'] = title - preload_elt['artist'] = artist - preload_elt['album'] = album + preload_elt['timestamp'] = str(time.time()) + # attributes filename, title, artist, album, length have been copied + # XXX: the frontend should know the temporary directory where file is put return preload_elt def radiocolSongAdded(self, referee, song_path, profile): @@ -94,7 +100,7 @@ song = OggVorbis(song_path) except OggVorbisHeaderError: #this file is not ogg vorbis, we reject it - unlink(song_path) # FIXME: same host trick (see note above) + self.deleteFile(song_path) # FIXME: same host trick (see note above) self.host.bridge.radiocolSongRejected(jid.JID(referee).userhost(), "Uploaded file is not Ogg Vorbis song, only Ogg Vorbis songs are acceptable", profile) """mess = self.createGameElt(jid.JID(referee)) @@ -116,20 +122,25 @@ radio_data['to_delete'][attrs['filename']] = song_path # FIXME: works only because of the same host trick, see the note under the docstring def playNext(self, room_jid, profile): - """"Play next sont in queue if exists, and put a timer + """"Play next song in queue if exists, and put a timer which trigger after the song has been played to play next one""" - #TODO: need to check that there are still peoples in the room - # and clean the datas/stop the playlist if it's not the case #TODO: songs need to be erased once played or found invalids # ==> unlink done the Q&D way with the same host trick (see above) radio_data = self.games[room_jid.userhost()] + if len(radio_data['players']) == 0: + debug(_('No more participants in the radiocol: cleaning data')) + radio_data['queue'] = [] + for filename in radio_data['to_delete']: + self.deleteFile(radio_data, filename) + radio_data['to_delete'] = {} queue = radio_data['queue'] if not queue: #nothing left to play, we need to wait for uploads radio_data['playing'] = False return - filename, length = queue.pop(0) + song = queue.pop(0) + filename, length = song['filename'], float(song['length']) self.send(room_jid, ('', 'play'), {'filename': filename}, profile=profile) if not radio_data['upload'] and len(queue) < QUEUE_LIMIT: @@ -138,17 +149,23 @@ radio_data['upload'] = True reactor.callLater(length, self.playNext, room_jid, profile) + #we wait more than the song length to delete the file, to manage poorly reactive networks/clients + reactor.callLater(length + 90, self.deleteFile, radio_data, filename) # FIXME: same host trick (see above) + + def deleteFile(self, radio_data, filename): try: file_to_delete = radio_data['to_delete'][filename] except KeyError: error(_("INTERNAL ERROR: can't find full path of the song to delete")) - return - - #we wait more than the song length to delete the file, to manage poorly reactive networks/clients - reactor.callLater(length + 90, unlink, file_to_delete) # FIXME: same host trick (see above) + return False + try: + unlink(file_to_delete) + except OSError: + error(_("INTERNAL ERROR: can't find %s on the file system" % file_to_delete)) + return False + return True def room_game_cmd(self, mess_elt, profile): - #FIXME: we should check sender (is it referee ?) here before accepting commands from_jid = jid.JID(mess_elt['from']) room_jid = jid.JID(from_jid.userhost()) radio_elt = mess_elt.firstChildElement() @@ -156,16 +173,20 @@ if 'queue' in radio_data: queue = radio_data['queue'] + from_referee = self.isReferee(room_jid.userhost(), from_jid.resource) + to_referee = self.isReferee(room_jid.userhost(), jid.JID(mess_elt['to']).user) for elt in radio_elt.elements(): + if not from_referee and not (to_referee and elt.name == 'song_added'): + continue # sender must be referee, expect when a song is submitted if elt.name == 'started' or elt.name == 'players': # new game created players = [] for player in elt.elements(): players.append(unicode(player)) signal = self.host.bridge.radiocolStarted if elt.name == 'started' else self.host.bridge.radiocolPlayers - signal(room_jid.userhost(), from_jid.full(), players, profile) + signal(room_jid.userhost(), from_jid.full(), players, [QUEUE_TO_START, QUEUE_LIMIT], profile) elif elt.name == 'preload': # a song is in queue and must be preloaded - self.host.bridge.radiocolPreload(room_jid.userhost(), elt['filename'], elt['title'], elt['artist'], elt['album'], profile) + self.host.bridge.radiocolPreload(room_jid.userhost(), elt['timestamp'], elt['filename'], elt['title'], elt['artist'], elt['album'], profile) elif elt.name == 'play': self.host.bridge.radiocolPlay(room_jid.userhost(), elt['filename'], profile) elif elt.name == 'song_rejected': # a song has been refused @@ -180,32 +201,30 @@ if len(queue) >= QUEUE_LIMIT: #there are already too many songs in queue, we reject this one - attrs = {'sender': from_jid.resource, - 'reason': "Too many songs in queue" - } #FIXME: add an error code - self.send(room_jid, ('', 'song_rejected'), attrs, profile=profile) + self.send(from_jid, ('', 'song_rejected'), {'reason': "Too many songs in queue"}, profile=profile) return #The song is accepted and added in queue - queue.append((elt['filename'], float(elt['length']))) + preload_elt = self.__create_preload_elt(from_jid.resource, elt) + queue.append(preload_elt) if len(queue) >= QUEUE_LIMIT: #We are at the limit, we refuse new upload until next play - #FIXME: add an error code self.send(room_jid, ('', 'no_upload'), profile=profile) radio_data['upload'] = False - preload_elt = self.__create_preload_elt(from_jid.resource, - elt['filename'], - elt['title'], - elt['artist'], - elt['album']) self.send(room_jid, preload_elt, profile=profile) - if not radio_data['playing'] and len(queue) == 2: - #we have not started playing yet, and we have 2 songs in queue - #we can now start the party :) + if not radio_data['playing'] and len(queue) == QUEUE_TO_START: + # We have not started playing yet, and we have QUEUE_TO_START + # songs in queue. We can now start the party :) radio_data['playing'] = True self.playNext(room_jid, profile) else: error(_('Unmanaged game element: %s') % elt.name) + + def getSyncData(self, room_jid_s): + data = self.games[room_jid_s]['queue'] + if len(data) == QUEUE_LIMIT: + data.append(domish.Element((None, 'no_upload'))) + return data diff -r 812dc38c0094 -r 539f278bc265 src/plugins/plugin_misc_room_game.py --- a/src/plugins/plugin_misc_room_game.py Tue Dec 10 09:02:20 2013 +0100 +++ b/src/plugins/plugin_misc_room_game.py Thu Nov 28 19:23:59 2013 +0100 @@ -23,6 +23,7 @@ from time import time from wokkel import disco, iwokkel from zope.interface import implements +import copy try: from twisted.words.protocols.xmlstream import XMPPHandler except ImportError: @@ -30,7 +31,6 @@ # Don't forget to set it to False before you commit _DEBUG = False -_DEBUG_FILE = False PLUGIN_INFO = { "name": "Room game", @@ -123,7 +123,7 @@ """ referee = room_jid_s + '/' + referee_nick self.games[room_jid_s] = {'referee': referee, 'players': [], 'started': False} - self.games[room_jid_s].update(self.game_init) + self.games[room_jid_s].update(copy.deepcopy(self.game_init)) def gameExists(self, room_jid_s, started=False): """Return True if a game has been initialized/started. @@ -166,7 +166,7 @@ return auth def updatePlayers(self, room_jid_s, nicks, profile): - """Signal to the room or to each player that some players joined the game""" + """Signal to the room that some players joined the game""" if nicks == []: return new_nicks = set(nicks).difference(self.games[room_jid_s]['players']) @@ -181,8 +181,19 @@ element = self.createStartElement(self.games[room_jid_s]['players']) else: element = self.createStartElement(self.games[room_jid_s]['players'], name="players") + elements = [(element, None, None)] + for child in self.getSyncData(room_jid_s): + # TODO: sync data may be different and private to each player, + # in that case send a separate message to the new players + elements.append((child, None, None)) for recipient in recipients: - self.send(recipient, element, profile=profile) + self.sendElements(recipient, elements, profile=profile) + + def getSyncData(self, room_jid_s): + """This method may be overwritten by any child class. + @return: a list of child elements to be added for the game to be synchronized. + """ + return [] def invitePlayers(self, room, other_players, nick, profile): """Invite players to a room, associated game may exist or not. @@ -408,7 +419,7 @@ players_data = {} for nick in nicks: # The dict must be COPIED otherwise it is shared between all users - players_data[nick] = self.player_init.copy() + players_data[nick] = copy.deepcopy(self.player_init) status[nick] = "init" self.games[room_jid_s].update({'status': status, 'players_data': players_data}) @@ -429,7 +440,7 @@ players_data = game_data['players_data'] game_data['stage'] = "init" - common_data, msg_elts = data if data is not None else (None, None) + common_data, msg_elts = copy.deepcopy(data) if data is not None else (None, None) if isinstance(msg_elts, dict): for player in players: @@ -467,6 +478,35 @@ started_elt.addChild(player_elt) return started_elt + def sendElements(self, to_jid, data, profile=None): + """ + @param to_jid: recipient JID + @param data: list of (elem, attr, content) with: + - elem: domish.Element, unicode or a couple: + - domish.Element to be directly added as a child to the message + - unicode name or couple (uri, name) to create a new domish.Element + and add it as a child to the message (see domish.Element.addElement) + - attrs: dictionary of attributes for the new child + - content: unicode that is appended to the child content + @param profile: the profile from which the message is sent + """ + if profile is None: + error(_("Message can not be sent without a sender profile")) + return + msg = self.createGameElt(to_jid) + for elem, attrs, content in data: + if elem is not None: + if isinstance(elem, domish.Element): + msg.firstChildElement().addChild(elem) + else: + elem = msg.firstChildElement().addElement(elem) + if attrs is not None: + elem.attributes.update(attrs) + if content is not None: + elem.addContent(content) + self.host.profiles[profile].xmlstream.send(msg) + + def send(self, to_jid, elem=None, attrs=None, content=None, profile=None): """ @param to_jid: recipient JID @@ -478,26 +518,7 @@ @param content: unicode that is appended to the child content @param profile: the profile from which the message is sent """ - if profile is None: - error(_("Message can not be sent without a sender profile")) - return - msg = self.createGameElt(to_jid) - if elem is not None: - if isinstance(elem, domish.Element): - msg.firstChildElement().addChild(elem) - else: - elem = msg.firstChildElement().addElement(elem) - if attrs is not None: - elem.attributes.update(attrs) - if content is not None: - elem.addContent(content) - self.host.profiles[profile].xmlstream.send(msg) - - if _DEBUG_FILE: - # From here you will see all the game messages - file_ = open("/tmp/game_messages", "a") - file_.write("%s from %s to %s: %s\n" % (self.name, profile, "room" if to_jid.resource is None else to_jid.resource, elem.toXml())) - file_.close() + self.sendElements(to_jid, [(elem, attrs, content)], profile) def getHandler(self, profile): return RoomGameHandler(self)