From e971f3f0b5e7d38e650686003d3e9b7f20f184d2 Mon Sep 17 00:00:00 2001 From: bkfox Date: Thu, 7 Jul 2016 01:18:39 +0200 Subject: [PATCH] actions & action button automatic generation; 'play' & 'listen' button on diffusions work --- cms/sections.py | 2 +- cms/static/aircox/cms/scripts.js | 74 +++++++++++++ cms/templates/aircox/cms/list_item.html | 111 ++++++++++--------- cms/views.py | 1 - liquidsoap/management/commands/liquidsoap.py | 36 +++--- liquidsoap/utils.py | 27 ++--- notes.md | 23 ++-- programs/admin.py | 2 +- programs/models.py | 33 +++++- website/sections.py | 10 ++ website/templates/aircox/website/player.html | 88 +++++---------- 11 files changed, 247 insertions(+), 160 deletions(-) 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.date and 'date' in list.fields or 'time' in list.fields %} - - {% endif %} - {% if object.author and 'author' in list.fields %} - - {{ object.author }} - - {% endif %} +
    + {% if object.date and 'date' in list.fields or 'time' in list.fields %} + + {% endif %} + {% if object.author and 'author' in list.fields %} + + {{ object.author }} + + {% endif %} - {% if object.info and 'info' in list.fields %} - - {{ object.info }} - - {% endif %} -
    + {% if object.info and 'info' in list.fields %} + + {{ object.info }} + + {% 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 @@ }
    -