diff --git a/cms/sections.py b/cms/sections.py
index b20074e..7ce4568 100644
--- a/cms/sections.py
+++ b/cms/sections.py
@@ -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
diff --git a/cms/static/aircox/cms/scripts.js b/cms/static/aircox/cms/scripts.js
index 6ecd4f2..0659544 100644
--- a/cms/static/aircox/cms/scripts.js
+++ b/cms/static/aircox/cms/scripts.js
@@ -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 || '') + '';
+ 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.
diff --git a/cms/templates/aircox/cms/list_item.html b/cms/templates/aircox/cms/list_item.html
index dc584ff..b541499 100644
--- a/cms/templates/aircox/cms/list_item.html
+++ b/cms/templates/aircox/cms/list_item.html
@@ -4,63 +4,72 @@
{% with object|downcast as object %}
- {% if object.url %}
+ {% for k, v in object.attrs.items %}
+ {{ k }} = "{{ v|addslashes }}"
+ {% endfor %} >
+ {% if object.url %}
- {% endif %}
- {% if 'image' in list.fields and object.image %}
-
- {% endif %}
+ {% endif %}
+ {% if 'image' in list.fields and object.image %}
+
+ {% endif %}
-
- {% if 'title' in list.fields and object.title %}
-
{{ object.title }}
- {% endif %}
+
+ {% if 'title' in list.fields and object.title %}
+
{{ object.title }}
+ {% endif %}
- {% if 'content' in list.fields and object.content %}
-
- {% if list.truncate %}
- {{ object.content|striptags|truncatewords:list.truncate }}
- {% else %}
- {{ object.content|striptags }}
- {% endif %}
-
- {% endif %}
-
+ {% if 'content' in list.fields and object.content %}
+
+ {% if list.truncate %}
+ {{ object.content|striptags|truncatewords:list.truncate }}
+ {% else %}
+ {{ object.content|striptags }}
+ {% endif %}
+
+ {% endif %}
+
-
- {% if object.url %}
-
- {% endif %}
+ {% if object.actions and 'actions' in list.fields %}
+
+ {% for action_id,action_data in object.actions.items %}
+
+ {% endfor %}
+
+ {% endif %}
+
+ {% if object.url %}
+
+ {% endif %}
{% endwith %}
diff --git a/cms/views.py b/cms/views.py
index b52b386..4b07366 100644
--- a/cms/views.py
+++ b/cms/views.py
@@ -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)
diff --git a/liquidsoap/management/commands/liquidsoap.py b/liquidsoap/management/commands/liquidsoap.py
index 2773d08..0d28203 100644
--- a/liquidsoap/management/commands/liquidsoap.py
+++ b/liquidsoap/management/commands/liquidsoap.py
@@ -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):
diff --git a/liquidsoap/utils.py b/liquidsoap/utils.py
index 88b7141..650dd57 100644
--- a/liquidsoap/utils.py
+++ b/liquidsoap/utils.py
@@ -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', '')
diff --git a/notes.md b/notes.md
index 672ccc2..33ee713 100644
--- a/notes.md
+++ b/notes.md
@@ -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
+
+
+
diff --git a/programs/admin.py b/programs/admin.py
index 2a34ec8..ad4872e 100755
--- a/programs/admin.py
+++ b/programs/admin.py
@@ -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' ] } )
]
diff --git a/programs/models.py b/programs/models.py
index d512035..3a63485 100755
--- a/programs/models.py
+++ b/programs/models.py
@@ -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,
)
diff --git a/website/sections.py b/website/sections.py
index 19167ca..e09629d 100644
--- a/website/sections.py
+++ b/website/sections.py
@@ -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):
diff --git a/website/templates/aircox/website/player.html b/website/templates/aircox/website/player.html
index e4c654e..906a51d 100644
--- a/website/templates/aircox/website/player.html
+++ b/website/templates/aircox/website/player.html
@@ -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 @@
}
-
@@ -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');
+
{% endblock %}