view src/browser/radiocol.py @ 449:981ed669d3b3

/!\ reorganize all the file hierarchy, move the code and launching script to src: - browser_side --> src/browser - public --> src/browser_side/public - libervia.py --> src/browser/libervia_main.py - libervia_server --> src/server - libervia_server/libervia.sh --> src/libervia.sh - twisted --> src/twisted - new module src/common - split constants.py in 3 files: - src/common/constants.py - src/browser/constants.py - src/server/constants.py - output --> html (generated by pyjsbuild during the installation) - new option/parameter "data_dir" (-d) to indicates the directory containing html and server_css - setup.py installs libervia to the following paths: - src/common --> <LIB>/libervia/common - src/server --> <LIB>/libervia/server - src/twisted --> <LIB>/twisted - html --> <SHARE>/libervia/html - server_side --> <SHARE>libervia/server_side - LIBERVIA_INSTALL environment variable takes 2 new options with prompt confirmation: - clean: remove previous installation directories - purge: remove building and previous installation directories You may need to update your sat.conf and/or launching script to update the following options/parameters: - ssl_certificate - data_dir
author souliane <souliane@mailoo.org>
date Tue, 20 May 2014 06:41:16 +0200
parents browser_side/radiocol.py@d52f529a6d42
children 1a0cec9b0f1e
line wrap: on
line source

#!/usr/bin/python
# -*- coding: utf-8 -*-

# Libervia: a Salut à Toi frontend
# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org>

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import pyjd  # this is dummy in pyjs
from sat.core.log import getLogger
log = getLogger(__name__)
from sat_frontends.tools.misc import DEFAULT_MUC
from sat.core.i18n import _

from pyjamas.ui.VerticalPanel import VerticalPanel
from pyjamas.ui.HorizontalPanel import HorizontalPanel
from pyjamas.ui.FlexTable import FlexTable
from pyjamas.ui.FormPanel import FormPanel
from pyjamas.ui.Label import Label
from pyjamas.ui.Button import Button
from pyjamas.ui.ClickListener import ClickHandler
from pyjamas.ui.Hidden import Hidden
from pyjamas.ui.CaptionPanel import CaptionPanel
from pyjamas.media.Audio import Audio
from pyjamas import Window
from pyjamas.Timer import Timer

from html_tools import html_sanitize
from file_tools import FilterFileUpload


class MetadataPanel(FlexTable):

    def __init__(self):
        FlexTable.__init__(self)
        title_lbl = Label("title:")
        title_lbl.setStyleName('radiocol_metadata_lbl')
        artist_lbl = Label("artist:")
        artist_lbl.setStyleName('radiocol_metadata_lbl')
        album_lbl = Label("album:")
        album_lbl.setStyleName('radiocol_metadata_lbl')
        self.title = Label("")
        self.title.setStyleName('radiocol_metadata')
        self.artist = Label("")
        self.artist.setStyleName('radiocol_metadata')
        self.album = Label("")
        self.album.setStyleName('radiocol_metadata')
        self.setWidget(0, 0, title_lbl)
        self.setWidget(1, 0, artist_lbl)
        self.setWidget(2, 0, album_lbl)
        self.setWidget(0, 1, self.title)
        self.setWidget(1, 1, self.artist)
        self.setWidget(2, 1, self.album)
        self.setStyleName("radiocol_metadata_pnl")

    def setTitle(self, title):
        self.title.setText(title)

    def setArtist(self, artist):
        self.artist.setText(artist)

    def setAlbum(self, album):
        self.album.setText(album)


class ControlPanel(FormPanel):
    """Panel used to show controls to add a song, or vote for the current one"""

    def __init__(self, parent):
        FormPanel.__init__(self)
        self.setEncoding(FormPanel.ENCODING_MULTIPART)
        self.setMethod(FormPanel.METHOD_POST)
        self.setAction("upload_radiocol")
        self.timer_on = False
        self._parent = parent
        vPanel = VerticalPanel()

        types = [('audio/ogg', '*.ogg', 'Ogg Vorbis Audio'),
                 ('video/ogg', '*.ogv', 'Ogg Vorbis Video'),
                 ('application/ogg', '*.ogx', 'Ogg Vorbis Multiplex'),
                 ('audio/mpeg', '*.mp3', 'MPEG-Layer 3'),
                 ('audio/mp3', '*.mp3', 'MPEG-Layer 3'),
                 ]
        self.file_upload = FilterFileUpload("song", 10, types)
        vPanel.add(self.file_upload)

        hPanel = HorizontalPanel()
        self.upload_btn = Button("Upload song", getattr(self, "onBtnClick"))
        hPanel.add(self.upload_btn)
        self.status = Label()
        self.updateStatus()
        hPanel.add(self.status)
        #We need to know the filename and the referee
        self.filename_field = Hidden('filename', '')
        hPanel.add(self.filename_field)
        referee_field = Hidden('referee', self._parent.referee)
        hPanel.add(self.filename_field)
        hPanel.add(referee_field)
        vPanel.add(hPanel)

        self.add(vPanel)
        self.addFormHandler(self)

    def updateStatus(self):
        if self.timer_on:
            return
        # TODO: the status should be different if a song is being played or not
        queue = self._parent.getQueueSize()
        queue_data = self._parent.queue_data
        if queue < queue_data[0]:
            left = queue_data[0] - queue
            self.status.setText("[we need %d more song%s]" % (left, "s" if left > 1 else ""))
        elif queue < queue_data[1]:
            left = queue_data[1] - queue
            self.status.setText("[%d available spot%s]" % (left, "s" if left > 1 else ""))
        elif queue >= queue_data[1]:
                self.status.setText("[The queue is currently full]")
        self.status.setStyleName('radiocol_status')

    def onBtnClick(self):
        if self.file_upload.check():
            self.status.setText('[Submitting, please wait...]')
            self.filename_field.setValue(self.file_upload.getFilename())
            if self.file_upload.getFilename().lower().endswith('.mp3'):
                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)
            self.submit()
            self.file_upload.setFilename("")

    def onSubmit(self, event):
        pass

    def blockUpload(self):
        self.file_upload.setVisible(False)
        self.upload_btn.setEnabled(False)

    def unblockUpload(self):
        self.file_upload.setVisible(True)
        self.upload_btn.setEnabled(True)

    def setTemporaryStatus(self, text, style):
        self.status.setText(text)
        self.status.setStyleName('radiocol_upload_status_%s' % style)
        self.timer_on = True

        def cb(timer):
            self.timer_on = False
            self.updateStatus()

        Timer(5000, cb)

    def onSubmitComplete(self, event):
        result = event.getResults()
        if result == "OK":
            # the song can still be rejected (not readable, full queue...)
            self.setTemporaryStatus('[Your song has been submitted to the radio]', "ok")
        elif result == "KO":
            self.setTemporaryStatus('[Something went wrong during your song upload]', "ko")
            self._parent.radiocolSongRejected(_("The uploaded file has been rejected, only Ogg Vorbis and MP3 songs are accepted."))
            # TODO: would be great to re-use the original Exception class and message
            # but it is lost in the middle of the traceback and encapsulated within
            # a DBusException instance --> extract the data from the traceback?
        else:
            Window.alert('Submit error: %s' % result)
            self.status.setText('')


class Player(Audio):

    def __init__(self, player_id, metadata_panel):
        Audio.__init__(self)
        self._id = player_id
        self.metadata = metadata_panel
        self.timestamp = ""
        self.title = ""
        self.artist = ""
        self.album = ""
        self.filename = None
        self.played = False  # True when the song is playing/has played, becomes False on preload
        self.setAutobuffer(True)
        self.setAutoplay(False)
        self.setVisible(False)


    def preload(self, timestamp, filename, title, artist, album):
        """preload the song but doesn't play it"""
        self.timestamp = timestamp
        self.filename = filename
        self.title = title
        self.artist = artist
        self.album = album
        self.played = False
        self.setSrc("radiocol/%s" % html_sanitize(filename))
        log.debug("preloading %s in %s" % (title, self._id))

    def play(self, play=True):
        """Play or pause the song
        @param play: set to True to play or to False to pause
        """
        if play:
            self.played = True
            self.metadata.setTitle(self.title)
            self.metadata.setArtist(self.artist)
            self.metadata.setAlbum(self.album)
            Audio.play(self)
        else:
            self.pause()


class RadioColPanel(HorizontalPanel, ClickHandler):

    def __init__(self, parent, referee, player_nick, players, queue_data):
        """
        @param parent
        @param referee
        @param player_nick
        @param players
        @param queue_data: list of integers (queue to start, queue limit)
        """
        # We need to set it here and not in the CSS :(
        HorizontalPanel.__init__(self, Height="90px")
        ClickHandler.__init__(self)
        self._parent = parent
        self.referee = referee
        self.queue_data = queue_data
        self.setStyleName("radiocolPanel")

        # Now we set up the layout
        self.metadata_panel = MetadataPanel()
        self.add(CaptionPanel("Now playing", self.metadata_panel))
        self.playlist_panel = VerticalPanel()
        self.add(CaptionPanel("Songs queue", self.playlist_panel))
        self.control_panel = ControlPanel(self)
        self.add(CaptionPanel("Controls", self.control_panel))

        self.next_songs = []
        self.players = [Player("player_%d" % i, self.metadata_panel) for i in xrange(queue_data[1] + 1)]
        self.current_player = None
        for player in self.players:
            self.add(player)
        self.addClickListener(self)

        help_msg = """Accepted file formats: Ogg Vorbis (recommended), MP3.<br />
        Please do not submit files that are protected by copyright.<br />
        Click <a style="color: red;">here</a> if you need some support :)"""
        link_cb = lambda: self._parent.host.bridge.call('joinMUC', None, DEFAULT_MUC, self._parent.nick)
        self._parent.printInfo(help_msg, type_='link', link_cb=link_cb)

    def pushNextSong(self, title):
        """Add a song to the left panel's next songs queue"""
        next_song = Label(title)
        next_song.setStyleName("radiocol_next_song")
        self.next_songs.append(next_song)
        self.playlist_panel.append(next_song)
        self.control_panel.updateStatus()

    def popNextSong(self):
        """Remove the first song of next songs list
        should be called when the song is played"""
        #FIXME: should check that the song we remove is the one we play
        next_song = self.next_songs.pop(0)
        self.playlist_panel.remove(next_song)
        self.control_panel.updateStatus()

    def getQueueSize(self):
        return len(self.playlist_panel.getChildren())

    def radiocolCheckPreload(self, timestamp):
        for player in self.players:
            if player.timestamp == timestamp:
                return False
        return True

    def radiocolPreload(self, timestamp, filename, title, artist, album, sender):
        if not self.radiocolCheckPreload(timestamp):
            return  # song already preloaded
        preloaded = False
        for player in self.players:
            if not player.filename or \
               (player.played and player != self.current_player):
                #if player has no file loaded, or it has already played its song
                #we use it to preload the next one
                player.preload(timestamp, filename, title, artist, album)
                preloaded = True
                break
        if not preloaded:
            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])
        else:
            self.pushNextSong(title)
            self._parent.printInfo(_('%(user)s uploaded %(artist)s - %(title)s') % {'user': sender, 'artist': artist, 'title': title})

    def radiocolPlay(self, filename):
        found = False
        for player in self.players:
            if not found and player.filename == filename:
                player.play()
                self.popNextSong()
                self.current_player = player
                found = True
            else:
                player.play(False)  # in case the previous player was not sync
        if not found:
            log.error("Song not found in queue, can't play it. This should not happen")

    def radiocolNoUpload(self):
        self.control_panel.blockUpload()

    def radiocolUploadOk(self):
        self.control_panel.unblockUpload()

    def radiocolSongRejected(self, reason):
        Window.alert("Song rejected: %s" % reason)