actions & action button automatic generation; 'play' & 'listen' button on diffusions work

This commit is contained in:
bkfox 2016-07-07 01:18:39 +02:00
parent 8ff67fe68a
commit e971f3f0b5
11 changed files with 247 additions and 160 deletions

View File

@ -265,7 +265,7 @@ class List(Section):
message_empty = _('nothing')
paginate_by = 4
fields = [ 'date', 'time', 'image', 'title', 'content', 'info' ]
fields = [ 'date', 'time', 'image', 'title', 'content', 'info', 'actions' ]
image_size = '64x64'
truncate = 16

View File

@ -1,3 +1,77 @@
/// Actions manager
/// This class is used to register actions and to execute them when it is
/// triggered by the user.
var Actions = {
registry: {},
/// Add an handler for a given action
register: function(id, symbol, title, handler) {
this.registry[id] = {
symbol: symbol,
title: title,
handler: handler,
}
},
/// Init an existing action HTML element
init_action: function(item, action_id, data, url) {
var action = this.registry[action_id];
if(!action)
return;
item.title = action.title;
item.innerHTML = (action.symbol || '') + '<label>' +
action.title + '</label>';
item.data = data;
item.className = 'action';
if(url)
item.href = url;
item.setAttribute('action', action_id);
item.addEventListener('click', Actions.run, true);
},
/// Add an action to the given item
add_action: function(item, action_id, data, url) {
var actions = item.querySelector('.actions');
if(actions && actions.querySelector('[action="' + action_id + '"]'))
return;
var item = document.createElement('a');
this.init_action(item, action_id, data, url);
actions.appendChild(item);
},
/// Run an action from the given event -- ! this can be undefined
run: function(event) {
var item = event.target;
var action = item.hasAttribute('action') &&
item.getAttribute('action');
if(!action)
return;
event.preventDefault();
event.stopImmediatePropagation();
action = Actions.registry[action];
if(!action)
return
data = item.data || item.dataset;
action.handler(data, item);
return true;
},
};
document.addEventListener('DOMContentLoaded', function(event) {
var items = document.querySelectorAll('.action[action]');
for(var i = 0; i < items.length; i++) {
var item = items[i];
var action_id = item.getAttribute('action');
var data = item.dataset;
Actions.init_action(item, action_id, data);
}
}, false);
/// Small utility used to make XMLHttpRequests, and map results on objects.

View File

@ -4,63 +4,72 @@
{% with object|downcast as object %}
<li {% if object.css_class %}class="{{ object.css_class }}"{% endif %}
{% for k, v in object.attrs.items %}
{{ k }} = "{{ v|addslashes }}"
{% endfor %} >
{% if object.url %}
{% for k, v in object.attrs.items %}
{{ k }} = "{{ v|addslashes }}"
{% endfor %} >
{% if object.url %}
<a class="url" href="{{ object.url }}">
{% endif %}
{% if 'image' in list.fields and object.image %}
<img class="image" src="{% thumbnail object.image list.image_size crop %}">
{% endif %}
{% endif %}
{% if 'image' in list.fields and object.image %}
<img class="image" src="{% thumbnail object.image list.image_size crop %}">
{% endif %}
<div class="body">
{% if 'title' in list.fields and object.title %}
<h2 class="title">{{ object.title }}</h2>
{% endif %}
<div class="body">
{% if 'title' in list.fields and object.title %}
<h2 class="title">{{ object.title }}</h2>
{% endif %}
{% if 'content' in list.fields and object.content %}
<div class="content">
{% if list.truncate %}
{{ object.content|striptags|truncatewords:list.truncate }}
{% else %}
{{ object.content|striptags }}
{% endif %}
</div>
{% endif %}
</div>
{% if 'content' in list.fields and object.content %}
<div class="content">
{% if list.truncate %}
{{ object.content|striptags|truncatewords:list.truncate }}
{% else %}
{{ object.content|striptags }}
{% endif %}
</div>
{% endif %}
</div>
<div class="meta">
{% if object.date and 'date' in list.fields or 'time' in list.fields %}
<time datetime="{{ object.date }}">
{% if 'date' in list.fields %}
<span class="date">
{{ object.date|date:'D. d F' }}
</span>
{% endif %}
{% if 'time' in list.fields %}
<span class="time">
{{ object.date|date:'H:i' }}
</span>
{% endif %}
</time>
{% endif %}
{% if object.author and 'author' in list.fields %}
<span class="author">
{{ object.author }}
</span>
{% endif %}
<div class="meta">
{% if object.date and 'date' in list.fields or 'time' in list.fields %}
<time datetime="{{ object.date }}">
{% if 'date' in list.fields %}
<span class="date">
{{ object.date|date:'D. d F' }}
</span>
{% endif %}
{% if 'time' in list.fields %}
<span class="time">
{{ object.date|date:'H:i' }}
</span>
{% endif %}
</time>
{% endif %}
{% if object.author and 'author' in list.fields %}
<span class="author">
{{ object.author }}
</span>
{% endif %}
{% if object.info and 'info' in list.fields %}
<span class="info">
{{ object.info }}
</span>
{% endif %}
</div>
{% if object.info and 'info' in list.fields %}
<span class="info">
{{ object.info }}
</span>
{% endif %}
</div>
{% if object.url %}
</a>
{% endif %}
{% if object.actions and 'actions' in list.fields %}
<div class="actions">
{% for action_id,action_data in object.actions.items %}
<a class="action" action="{{action_id}}"
{% for k,v in action_data.items %}data-{{ k }}="{{ v }}"{% endfor %}></a>
{% endfor %}
</div>
{% endif %}
{% if object.url %}
</a>
{% endif %}
</li>
{% endwith %}

View File

@ -168,7 +168,6 @@ class PostListView(BaseView, ListView):
self.list = sections.List(
truncate = 32,
paginate_by = 0,
fields = ['date', 'time', 'image', 'title', 'content'],
)
else:
self.list = self.list(paginate_by = 0)

View File

@ -64,7 +64,8 @@ class Monitor:
playlist = diff.playlist
if played_sounds:
diff.played = [ sound.related_object.path
for sound in sound_logs[0:len(playlist)] ]
for sound in sound_logs[0:len(playlist)]
if sound.type = program.Logs.Type.switch ]
return diff
@classmethod
@ -111,6 +112,13 @@ class Monitor:
# playlist update
if dealer.playlist != playlist:
dealer.playlist = playlist
if next_diff:
cl.log(
type = programs.Log.Type.load,
source = dealer.id,
date = now,
related_object = next_diff
)
# dealer.on when next_diff.start <= now
if next_diff and not dealer.on and next_diff.start <= now:
@ -118,18 +126,16 @@ class Monitor:
for source in controller.streams.values():
source.skip()
cl.log(
type = programs.Log.Type.play,
source = dealer.id,
date = now,
comment = 'trigger diffusion to liquidsoap; '
'skip other streams',
related_object = next_diff,
)
@classmethod
def run_source (cl, source):
"""
Keep trace of played sounds on the given source. For the moment we only
keep track of known sounds.
Keep trace of played sounds on the given source.
"""
# TODO: repetition of the same sound out of an interval of time
last_log = programs.Log.objects.filter(
@ -150,16 +156,16 @@ class Monitor:
return
sound = programs.Sound.objects.filter(path = on_air)
if not sound:
return
sound = sound[0]
cl.log(
source = source.id,
date = tz.make_aware(tz.datetime.now()),
comment = 'sound changed',
related_object = sound or None,
)
kwargs = {
'type': programs.Log.Type.play,
'source': source.id,
'date': tz.make_aware(tz.datetime.now()),
}
if sound:
kwargs['related_object'] = sound[0]
else:
kwargs['comment'] = on_air
cl.log(**kwargs)
class Command (BaseCommand):

View File

@ -2,10 +2,10 @@ import os
import socket
import re
import json
import subprocess
from django.utils.translation import ugettext as _, ugettext_lazy
from django.utils import timezone as tz
from django.utils.translation import ugettext as _, ugettext_lazy
from django.utils.text import slugify
from django.conf import settings as main_settings
from django.template.loader import render_to_string
@ -130,7 +130,6 @@ class BaseSource:
return self.update(metadata = r or {})
source = metadata.get('source') or ''
# FIXME: self.program
if hasattr(self, 'program') and self.program \
and not source.startswith(self.id):
return -1
@ -145,20 +144,19 @@ class Source(BaseSource):
metadata = None
def __init__(self, controller, program = None, is_dealer = None):
station = controller.station
if is_dealer:
id, name = '{}_dealer'.format(station.slug), \
id, name = '{}_dealer'.format(controller.id), \
'Dealer'
self.is_dealer = True
else:
id, name = '{}_stream_{}'.format(station.slug, program.id), \
id, name = '{}_stream_{}'.format(controller.id, program.id), \
program.name
super().__init__(controller, id, name)
self.program = program
self.path = os.path.join(settings.AIRCOX_LIQUIDSOAP_MEDIA,
station.slug,
controller.id,
self.id + '.m3u')
if program:
self.playlist_from_db()
@ -237,10 +235,6 @@ class Master (BaseSource):
"""
A master Source based on a given station
"""
def __init__(self, controller):
station = controller.station
super().__init__(controller, station.slug, station.name)
def update(self, metadata = None):
if metadata is not None:
return super().update(metadata)
@ -259,13 +253,12 @@ class Controller:
path = None
connector = None
station = None # the related station
master = None # master source (station's source)
master = None # master source
dealer = None # dealer source
streams = None # streams streams
# FIXME: used nowhere except in liquidsoap cli to get on air item but is not
# correctly
# correct
@property
def on_air(self):
return self.master
@ -294,10 +287,9 @@ class Controller:
to the given station; We ensure the existence of the controller's
files dir.
"""
self.id = station.slug
self.id = slugify(station)
self.name = station
self.path = os.path.join(settings.AIRCOX_LIQUIDSOAP_MEDIA,
slugify(station))
self.path = os.path.join(settings.AIRCOX_LIQUIDSOAP_MEDIA, self.id)
self.outputs = models.Output.objects.all()
@ -360,6 +352,7 @@ class Controller:
'log_script': log_script,
}
# FIXME: remove this crappy thing
data = render_to_string('aircox/liquidsoap/station.liq', context)
data = re.sub(r'\s*\\\n', r'#\\n#', data)
data = data.replace('\n', '')

View File

@ -1,11 +1,3 @@
- sounds monitor: max_size of path, take in account
- logs: archive functionnality + track stats for diffusions
- debug/prod configuration
# TODO ajd
- website/sections Diffusions/prepare\_object\_list -> sounds
- players' buttons
# TODO:
- general:
@ -14,6 +6,7 @@
- programs:
- schedule changes -> update later diffusions according to the new schedule
- stream disable -> remote control on liquidsoap
- tests:
- sound_monitor
@ -25,26 +18,34 @@
- config generation and sound diffusion
- cms:
- switch to abstract class and remove qcombine (or keep it smw else)?
- empty content -> empty string
- update documentation:
- cms.script
- cms.exposure; make it right, see nomenclature, + docstring
- admin cms
- sections:
- calendar title update
- article list with the focus
-> set html attribute based on values that are public
- website:
- render schedule does not get the correct list
-> postlistview has not the same queryset as website/sections/schedule
- diffusions:
- filter sounds for undiffused diffusions
- print sounds of diffusions
- print program's name in lists
- player:
- "listen" + "favorite" buttons made easy + automated
- mixcloud
- seek bar
- list of played diffusions and tracks when non-stop;
# Later todo
- sounds monitor: max_size of path, take in account
- logs: archive functionnality
- track stats for diffusions
- debug/prod configuration
- player support diffusions with multiple archive files
- view as grid

View File

@ -58,7 +58,7 @@ class SoundAdmin(NameableAdmin):
fields = None
list_display = ['id', 'name', 'duration', 'type', 'mtime', 'good_quality', 'removed']
fieldsets = [
(None, { 'fields': NameableAdmin.fields + ['path', 'type'] } ),
(None, { 'fields': NameableAdmin.fields + ['path', 'type', 'diffusion'] } ),
(None, { 'fields': ['embed', 'duration', 'mtime'] }),
(None, { 'fields': ['removed', 'good_quality' ] } )
]

View File

@ -81,8 +81,8 @@ class Track(Nameable):
_('artist'),
max_length = 128,
)
# position can be used to specify a position in seconds for non-
# stop programs or a position in the playlist
# position can be used to specify a position in seconds for stream
# programs or a position in the playlist
position = models.SmallIntegerField(
default = 0,
help_text=_('position in the playlist'),
@ -172,7 +172,7 @@ class Sound(Nameable):
# path = self._meta.get_field('path').path
path = self.path.replace(main_settings.MEDIA_ROOT, '', 1)
#path = self.path.replace(path, '', 1)
return path
return main_settings.MEDIA_URL + '/' + path
def file_exists(self):
"""
@ -684,8 +684,30 @@ class Diffusion(models.Model):
class Log(models.Model):
"""
Log a played sound start and stop, or a single message
Log sounds and diffusions that are played in the streamer. It
can also be used for other purposes.
"""
class Type(IntEnum):
stop = 0x00
"""
Source has been stopped (only when there is no more sound)
"""
play = 0x01
"""
Source has been started/changed and is running related_object
If no related_object is available, comment is used to designate
the sound.
"""
load = 0x02
"""
Source starts to be preload related_object
"""
type = models.SmallIntegerField(
verbose_name = _('type'),
choices = [ (int(y), _(x)) for x,y in Type.__members__.items() ],
blank = True, null = True,
)
source = models.CharField(
_('source'),
max_length = 64,
@ -693,10 +715,11 @@ class Log(models.Model):
blank = True, null = True,
)
date = models.DateTimeField(
'date',
_('date'),
auto_now_add=True,
)
comment = models.CharField(
_('comment'),
max_length = 512,
blank = True, null = True,
)

View File

@ -115,7 +115,17 @@ 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):

View File

@ -99,10 +99,11 @@
display: inline;
}
.playlist .actions label,
#playlist-live .actions,
#playlist-recents .actions a.action[action="remove"],
#playlist-marked .actions a.action[action="mark"],
.playlist .actions a.action[action="play"],
#playlist-marked .actions a.action[action="sound.mark"],
.playlist .actions a.action[action="sound.play"],
.playlist .actions a.url:not([href]),
.playlist .actions a.url[href=""] {
display: none;
@ -113,10 +114,6 @@
}
</style>
<div id="player">
<div class="actions sounds" style="display: none;">
<a class="action" action="mark" title="{% trans "mark this sound" %}"></a>
<a class="action" action="play" title="{% trans "play this sound" %}"></a>
</div>
<li class='item' style="display: none;">
<h2 class="title"></h2>
<div class="info"></div>
@ -455,7 +452,7 @@ player = {
return;
if(data.playlist)
this.select_playlist(this[data.playlist]);
this.select_playlist(this[data.selected_playlist]);
if(data.stream) {
item = this.playlist.find(data.stream, true);
item && this.select(item, false);
@ -465,7 +462,7 @@ player = {
save: function() {
playerStore.set('player', {
'playlist': this.playlist.name,
'selected_playlist': this.__playlist && this.__playlist.name,
'stream': this.item && this.item.stream,
'single': this.controls.single.checked,
});
@ -477,12 +474,10 @@ player = {
var player = this.player;
var audio = this.audio;
if(audio.paused) {
if(audio.paused)
audio.play();
}
else {
else
audio.pause();
}
},
__ask_to_seek(item) {
@ -523,17 +518,17 @@ player = {
/// Select the next track in the current playlist, eventually play it
next: function(play = true) {
var playlist = this.playlist;
var playlist = this.__playlist;
if(playlist == this.live)
return
var index = this.playlist.items.indexOf(this.item);
var index = this.__playlist.items.indexOf(this.item);
if(index == -1)
return;
index--;
if(index >= 0)
this.select(this.playlist.items[index], play);
this.select(this.__playlist.items[index], play);
},
/// remove selection using the given selector.
@ -549,7 +544,7 @@ player = {
this.__unselect('.playlists nav .tab[selected]');
this.__unselect('.playlists .playlist[selected]');
this.playlist = playlist;
this.__playlist = playlist;
if(playlist) {
playlist.playlist.setAttribute('selected', 'true');
playlist.tab.setAttribute('selected', 'true');
@ -574,51 +569,28 @@ player = {
/// add sound actions to a given element
add_actions: function(item, container) {
var player = this;
var actions = player.player.querySelector('.actions')
var elm = container.querySelector('.actions');
if(elm) {
var actions = actions.childNodes;
for(var i = 0; i < actions.length; i++)
elm.appendChild(actions[i].cloneNode(true));
}
else {
elm = elm.cloneNode(true);
elm.removeAttribute('style');
}
elm.addEventListener('click', function(event) {
var action = event.target.getAttribute('action');
if(!action)
return;
event.preventDefault();
event.stopImmediatePropagation();
switch(action) {
case 'mark':
player.marked.add(item);
break;
case 'play':
item = player.playlist.add(item);
player.select_playlist(player.playlist);
player.select(item, true);
break;
case 'remove':
item.playlist.remove(item);
break;
default:
return;
}
}, true);
if(!elm.parentNode)
container.appendChild(elm);
Actions.add_action(container, 'sound.mark', item);
Actions.add_action(container, 'sound.play', item, item.stream);
// TODO: remove from playlist
},
}
player.init('player')
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');
</script>
{% endblock %}