cms.actions + website.actions; Sounds section; player: bug fix (ask for restart on live stream), actions; remove website.Sound (not really used): move chmod/public into programs.Sound

This commit is contained in:
bkfox
2016-07-08 01:17:02 +02:00
parent e971f3f0b5
commit 88a5a9556e
19 changed files with 456 additions and 173 deletions

75
website/actions.py Normal file
View File

@ -0,0 +1,75 @@
from django.utils import timezone as tz
from django.utils.translation import ugettext as _, ugettext_lazy
from aircox.cms.actions import Action
import aircox.website.utils as utils
class AddToPlaylist(Action):
"""
Remember a sound and add it into the default playlist. The given
object can be:
- a Diffusion post
- a programs.Sound instance
- an object with an attribute 'sound' used to generate the code
"""
id = 'sound.add'
symbol = ''
title = _('add to the playlist')
code = """
function(sound, item) {
Player.playlist.add(sound);
item.parentNode.removeChild(item)
}
"""
@classmethod
def make_for_diffusions(cl, request, object):
from aircox.website.sections import Player
if object.related.end > tz.make_aware(tz.datetime.now()):
return
archives = object.related.get_archives()
if not archives:
return False
sound = Player.make_sound(object, archives[0])
return cl.to_str(object, **sound)
@classmethod
def make_for_sound(cl, request, object):
from aircox.website.sections import Player
sound = Player.make_sound(None, object)
return cl.to_str(object, **sound)
@classmethod
def test(cl, request, object, in_list):
from aircox.programs.models import Sound
from aircox.website.models import Diffusion
print(object)
if not in_list:
return False
if issubclass(type(object), Diffusion):
return cl.make_for_diffusions(request, object)
if issubclass(type(object), Sound):
return cl.make_for_sound(request, object)
if hasattr(object, 'sound') and object.sound:
return cl.make_for_sound(request, object.sound)
class Play(AddToPlaylist):
"""
Play a sound
"""
id = 'sound.play'
symbol = ''
title = _('listen')
code = """
function(sound) {
sound = Player.playlist.add(sound);
Player.select_playlist(Player.playlist);
Player.select(sound, true);
}
"""

View File

@ -21,5 +21,4 @@ admin.site.register(models.Diffusion, cms.RelatedPostAdmin)
cms.inject_inline(programs.Diffusion, TrackInline, True)
cms.inject_related_inline(models.Program, True)
cms.inject_related_inline(models.Diffusion, True)
cms.inject_related_inline(models.Sound, True)

View File

@ -9,6 +9,7 @@ from django.utils.translation import ugettext as _, ugettext_lazy
import aircox.programs.models as programs
import aircox.cms.models as cms
import aircox.website.actions as actions
class Article (cms.Post):
@ -50,6 +51,8 @@ class Program (cms.RelatedPost):
class Diffusion (cms.RelatedPost):
actions = [actions.Play, actions.AddToPlaylist]
class Relation:
model = programs.Diffusion
bindings = {
@ -78,53 +81,3 @@ class Diffusion (cms.RelatedPost):
'day': self.related.initial.start.strftime('%A %d/%m')
}
class Sound (cms.RelatedPost):
"""
Publication concerning sound. In order to manage access of sound
files in the filesystem, we use permissions -- it is up to the
user to work select the correct groups and permissions.
"""
embed = models.TextField(
_('embedding code'),
blank=True, null=True,
help_text = _('HTML code used to embed a sound from an external '
'plateform'),
)
"""
Embedding code if the file has been published on an external
plateform.
"""
auto_chmod = True
"""
change file permission depending on the "published" attribute.
"""
chmod_flags = (stat.S_IRWXU, stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH )
"""
chmod bit flags, for (not_published, published)
"""
class Relation:
model = programs.Sound
bindings = {
'title': 'name',
'date': 'mtime',
}
rel_to_post = True
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.auto_chmod and not self.related.removed and \
os.path.exists(self.related.path):
try:
os.chmod(self.related.path,
self.chmod_flags[self.published])
except PermissionError as err:
logger.error(
'cannot set permission {} to file {}: {}'.format(
self.chmod_flags[self.published],
self.related.path, err
)
)

View File

@ -7,12 +7,16 @@ import aircox.programs.models as programs
import aircox.cms.models as cms
import aircox.cms.routes as routes
import aircox.cms.sections as sections
import aircox.cms.decorators as decorators
from aircox.cms.exposures import expose
from aircox.cms.actions import Action
import aircox.website.models as models
import aircox.website.actions as actions
import aircox.website.utils as utils
@decorators.expose
@expose
class Player(sections.Section):
"""
Display a player that is cool.
@ -24,7 +28,7 @@ class Player(sections.Section):
"""
#default_sounds
@decorators.expose
@expose
def on_air(cl, request):
qs = programs.Diffusion.get(
now = True,
@ -46,17 +50,56 @@ class Player(sections.Section):
'item': post,
'list': sections.List,
}
on_air._exposure.template_name = 'aircox/cms/list_item.html'
@staticmethod
def make_sound(post = None, sound = None):
"""
Return a standard item from a sound that can be used as a
player's item
"""
r = {
'title': post.title if post else sound.name,
'url': post.url() if post else None,
'info': utils.duration_to_str(sound.duration),
}
if sound.embed:
r['embed'] = sound.embed
else:
r['stream'] = sound.url()
return r
@classmethod
def get_recents(cl, count):
"""
Return a list of count recent published diffusions that have sounds,
as item usable in the playlist.
"""
qs = models.Diffusion.objects \
.filter(published = True) \
.filter(related__end__lte = tz.datetime.now()) \
.order_by('-related__end')
recents = []
for post in qs:
archives = post.related.get_archives()
if not archives:
continue
archives = archives[0]
recents.append(cl.make_sound(post, archives))
if len(recents) >= count:
break
return recents
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context.update({
'base_template': 'aircox/cms/section.html',
'live_streams': self.live_streams,
'last_sounds': models.Sound.objects. \
filter(published = True). \
order_by('-pk')[:10],
'recents': self.get_recents(10),
})
return context
@ -99,7 +142,7 @@ class Diffusions(sections.List):
# .order_by('-start')[:self.prev_count])
#return r
def prepare_object_list(self, object_list):
def prepare_list(self, object_list):
"""
This function just prepare the list of object, in order to:
- have a good title
@ -115,17 +158,6 @@ class Diffusions(sections.List):
post.title = ': ' + post.title if post.title else \
' // ' + post.related.start.strftime('%A %d %B')
post.title = name + post.title
# sounds
pl = post.related.get_archives()
if pl:
item = { 'title': post.title, 'stream': pl[0].url,
'url': post.url() }
post.actions = {
'sound.play': item,
'sound.mark': item,
}
return object_list
def get_object_list(self):
@ -205,7 +237,24 @@ class Playlist(sections.List):
class Sounds(sections.List):
pass
title = _('Podcasts')
def get_object_list(self):
if self.object.related.end > tz.make_aware(tz.datetime.now()):
return
sounds = programs.Sound.objects.filter(
diffusion = self.object.related
# public = True
).order_by('type')
return [
sections.ListItem(
title=sound.name,
info=utils.duration_to_str(sound.duration),
sound = sound,
actions = [ actions.AddToPlaylist, actions.Play ],
) for sound in sounds
]
class Schedule(Diffusions):

View File

@ -102,7 +102,7 @@
.playlist .actions label,
#playlist-live .actions,
#playlist-recents .actions a.action[action="remove"],
#playlist-marked .actions a.action[action="sound.mark"],
#playlist-favorites .actions a.action[action="sound.mark"],
.playlist .actions a.action[action="sound.play"],
.playlist .actions a.url:not([href]),
.playlist .actions a.url[href=""] {
@ -118,8 +118,11 @@
<h2 class="title"></h2>
<div class="info"></div>
<div class="actions">
<a class="action" action="sound.mark"
title="{% trans "add to my favorites" %}"></a>
<a class="url action" title="{% trans "more informations" %}"></a>
<a class="action" action="remove" title="{% trans "remove from the playlist" %}"></a>
<a class="action" action="sound.remove"
title="{% trans "remove from the playlist" %}"></a>
</div>
</li>
<div class="player-box">
@ -130,7 +133,7 @@
Your browser does not support the <code>audio</code> element.
</audio>
<span class="player-button" onclick="player.play()"
<span class="player-button" onclick="Player.play()"
title="{% trans "play/pause" %}"></span>
<h3 class="title"></h3>
@ -149,7 +152,7 @@
</div>
<script>
playerStore = {
PlayerStore = {
// save data to localstorage, or remove it if data is null
set: function(name, data) {
name = 'player.' + name;
@ -194,8 +197,7 @@ playerStore = {
// * tab: text to put in the tab
// * items: list of items to append
// * store: store the playlist in localStorage
function Playlist(player, name, tab, items, store = false) {
this.player = player;
function Playlist(name, tab, items, store = false) {
this.name = name;
this.store = store;
@ -206,14 +208,14 @@ function Playlist(player, name, tab, items, store = false) {
var self = this;
this.tab = document.createElement('a');
this.tab.addEventListener('click', function(event) {
player.select_playlist(self);
Player.select_playlist(self);
event.preventDefault();
}, true);
this.tab.className = 'tab';
this.tab.innerHTML = tab;
player.playlists.appendChild(this.playlist);
player.playlists.querySelector('nav').appendChild(this.tab);
Player.playlists.appendChild(this.playlist);
Player.playlists.querySelector('nav').appendChild(this.tab);
this.items = [];
if(store)
@ -237,15 +239,30 @@ Playlist.prototype = {
});
},
/// add an item to the playlist or container, if not in this playlist.
/// add sound actions to a given element
add_actions: function(item, container) {
Actions.add_action(container, 'sound.mark', item);
Actions.add_action(container, 'sound.play', item, item.stream);
var elm = container.querySelector('.actions a[action="sound.mark"]');
elm.addEventListener('click', function(event) {
Player.favorites.add(item);
}, true);
var elm = container.querySelector('.actions a[action="sound.remove"]');
elm.addEventListener('click', function() {
item.playlist.remove(item);
}, true);
},
/// add an item to the playlist or container, if not in this.playlist.
/// return the existing item or the newly created item.
add: function(item, container) {
var item_ = this.find(item);
if(item_)
return item_;
var player = this.player;
var elm = player.player.querySelector('.item').cloneNode(true);
var elm = Player.player.querySelector('.item').cloneNode(true);
elm.removeAttribute('style');
if(!container)
@ -255,10 +272,18 @@ Playlist.prototype = {
else
container.appendChild(elm);
item.elm = elm;
item.playlist = this;
elm.item = item;
item = {
title: item.title,
url: item.url,
stream: item.stream,
info: item.info,
seekable: 'seekable' in item ? item.seekable : true,
elm: elm,
playlist: this,
}
elm.item = item;
elm.querySelector('.title').innerHTML = item.title || '';
elm.querySelector('.url').href = item.url || '';
elm.querySelector('.info').innerHTML = item.info || '';
@ -273,13 +298,13 @@ Playlist.prototype = {
var item = event.currentTarget.item;
if(item.stream || item.embed)
player.select(item);
Player.select(item);
event.stopPropagation();
return true;
}, false);
if(item.embed || item.stream)
player.add_actions(item, elm);
this.add_actions(item, elm);
this.items.push(item);
if(container == this.playlist && this.store)
@ -322,32 +347,32 @@ Playlist.prototype = {
delete item.playlist;
pl.push(item);
}
playerStore.set('playlist.' + this.name, pl)
PlayerStore.set('playlist.' + this.name, pl)
},
/// Load playlist from local storage
load: function() {
var pl = playerStore.get('playlist.' + this.name);
var pl = PlayerStore.get('playlist.' + this.name);
if(pl)
this.add_list(pl);
},
/// called by the player when the given item is unselected
unselect: function(player, item) {
/// called by Player when the given item is unselected
unselect: function(item) {
this.tab.removeAttribute('active');
if(item.elm)
item.elm.removeAttribute('selected');
var audio = this.player.audio;
var audio = Player.audio;
if(this.store && !audio.ended) {
item.currentTime = audio.currentTime;
this.save();
}
},
/// called by the player when the given item is selected, in order to
/// called by Player when the given item is selected, in order to
/// prepare it.
select: function(player, item) {
select: function(item) {
this.tab.setAttribute('active', 'true');
if(item.elm)
item.elm.setAttribute('selected', 'true');
@ -355,15 +380,15 @@ Playlist.prototype = {
}
player = {
/// main container of the player
Player = {
/// main container of the Player
player: undefined,
/// <audio> container
audio: undefined,
/// controls
controls: undefined,
/// init player
/// init Player
init: function(id) {
this.player = document.getElementById(id);
this.audio = this.player.querySelector('audio');
@ -398,12 +423,12 @@ player = {
this.audio.addEventListener('timeupdate', function() {
if(self.audio.seekable.length)
playerStore.set('stream.' + self.item.stream + '.pos',
PlayerStore.set('stream.' + self.item.stream + '.pos',
self.audio.currentTime)
}, false);
this.audio.addEventListener('ended', function() {
playerStore.set('streams.' + self.item.stream + '.pos')
PlayerStore.set('streams.' + self.item.stream + '.pos')
single = self.player.querySelector('input.single');
if(!single.checked)
@ -413,7 +438,7 @@ player = {
__init_playlists: function() {
this.playlists = this.player.querySelector('.playlists');
this.live = new Playlist(this,
this.live = new Playlist(
'live',
" {% trans "live" %}",
[ {% for sound in live_streams %}
@ -421,25 +446,28 @@ player = {
url: "{{ sound.url }}",
stream: "{{ sound.url }}",
info: "{{ sound.info }}",
seekable: false,
}, {% endfor %} ]
);
this.recents = new Playlist(this,
this.recents = new Playlist(
'recents', '{% trans "recents" %}',
[ {% for sound in last_sounds %}
[ {% for sound in recents %}
{ title: "{{ sound.title }}",
url: "{{ sound.url }}",
{% if sound.related.embed %}
embed: "{{ sound.related.embed }}",
{% else %}
stream: "{{ MEDIA_URL }}{{ sound.related.url|safe }}",
stream: "{{ sound.related.url|safe }}",
{% endif %}
info: "{{ sound.related.duration|date:"i:s" }}",
info: "{{ sound.related.duration|date:"H:i:s" }}",
}, {% endfor %} ]
);
this.marked = new Playlist(this,
'marked', '★ {% trans "marked" %}', null, true);
this.playlist = new Playlist(this,
'playlist', '☰ {% trans "playlist" %}', null, true);
this.favorites = new Playlist(
'favorites', '★ {% trans "favorites" %}', null, true
);
this.playlist = new Playlist(
'playlist', '☰ {% trans "playlist" %}', null, true
);
this.select(this.live.items[0], false);
this.select_playlist(this.recents);
@ -447,7 +475,7 @@ player = {
},
load: function() {
var data = playerStore.get('player');
var data = PlayerStore.get('Player');
if(!data)
return;
@ -461,19 +489,17 @@ player = {
},
save: function() {
playerStore.set('player', {
PlayerStore.set('player', {
'selected_playlist': this.__playlist && this.__playlist.name,
'stream': this.item && this.item.stream,
'single': this.controls.single.checked,
});
},
/** player actions **/
/** Player actions **/
/// play a given item { title, src }
play: function() {
var player = this.player;
var audio = this.audio;
if(audio.paused)
audio.play();
else
@ -481,13 +507,16 @@ player = {
},
__ask_to_seek(item) {
if(!item.seekable)
return;
var key = 'stream.' + item.stream + '.pos'
var pos = playerStore.get(key);
var pos = PlayerStore.get(key);
if(!pos)
return
if(confirm("{% trans "restart from the last position?" %}"))
this.audio.currentTime = Math.max(pos - 5, 0);
playerStore.set(key);
PlayerStore.set(key);
},
/// select the current track to play, and start playing it
@ -496,7 +525,7 @@ player = {
var player = this.player;
if(this.item && this.item.playlist)
this.item.playlist.unselect(this, this.item);
this.item.playlist.unselect(this.item);
audio.pause();
audio.src = item.stream;
@ -504,7 +533,7 @@ player = {
this.item = item;
if(this.item && this.item.playlist)
this.item.playlist.select(this, this.item);
this.item.playlist.select(this.item);
player.querySelectorAll('#simple-player .title')[0]
.innerHTML = item.title;
@ -563,33 +592,12 @@ player = {
.send();
window.setTimeout(function() {
player.update_on_air();
Player.update_on_air();
}, 60000*5);
},
/// add sound actions to a given element
add_actions: function(item, container) {
Actions.add_action(container, 'sound.mark', item);
Actions.add_action(container, 'sound.play', item, item.stream);
// TODO: remove from playlist
},
}
Actions.register('sound.mark', '★', '{% trans "add to my playlist" %}',
function(item) {
player.marked.add(item);
}
);
Actions.register('sound.play', '▶', '{% trans "listen" %}',
function(item) {
item = player.playlist.add(item);
player.select_playlist(player.playlist);
player.select(item, true);
}
);
player.init('player');
Player.init('player');
</script>
{% endblock %}

6
website/utils.py Normal file
View File

@ -0,0 +1,6 @@
def duration_to_str(duration):
return duration.strftime(
'%H:%M:%S' if duration.hour else '%M:%S'
)