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:
parent
e971f3f0b5
commit
88a5a9556e
126
cms/actions.py
Normal file
126
cms/actions.py
Normal file
|
@ -0,0 +1,126 @@
|
|||
"""
|
||||
Actions are used to add controllers available to the end user.
|
||||
They are attached to models, and tested (+ rendered if it is okay)
|
||||
before rendering each instance of the models.
|
||||
|
||||
For the moment it only can execute javascript code. There is also
|
||||
a javascript mini-framework in order to make it easy. The code of
|
||||
the action is then registered and executable on users actions.
|
||||
"""
|
||||
class Actions(type):
|
||||
"""
|
||||
General class that is used to register and manipulate actions
|
||||
"""
|
||||
registry = []
|
||||
|
||||
def __new__(cls, name, bases, attrs):
|
||||
cl = super().__new__(cls, name, bases, attrs)
|
||||
if name != 'Action':
|
||||
cls.registry.append(cl)
|
||||
return cl
|
||||
|
||||
@classmethod
|
||||
def make(cl, request, object_list = None, object = None):
|
||||
"""
|
||||
Make action on the given object_list or object
|
||||
"""
|
||||
if object_list:
|
||||
in_list = True
|
||||
else:
|
||||
object_list = [object]
|
||||
in_list = False
|
||||
|
||||
for object in object_list:
|
||||
if not hasattr(object, 'actions') or not object.actions:
|
||||
continue
|
||||
object.actions = [
|
||||
action.test(request, object, in_list)
|
||||
if type(action) == cl and issubclass(action, Action) else
|
||||
str(action)
|
||||
for action in object.actions
|
||||
]
|
||||
object.actions = [ code for code in object.actions if code ]
|
||||
|
||||
@classmethod
|
||||
def register_code(cl):
|
||||
"""
|
||||
Render javascript code that can be inserted somewhere to register
|
||||
all actions
|
||||
"""
|
||||
return '\n'.join(action.register_code() for action in cl.registry)
|
||||
|
||||
|
||||
class Action(metaclass=Actions):
|
||||
"""
|
||||
An action available to the end user.
|
||||
Don't forget to note in docstring the needed things.
|
||||
"""
|
||||
id = ''
|
||||
"""
|
||||
Unique ID for the given action
|
||||
"""
|
||||
symbol = ''
|
||||
"""
|
||||
UTF-8 symbol for the given action
|
||||
"""
|
||||
title = ''
|
||||
"""
|
||||
Used to render the correct button for the given action
|
||||
"""
|
||||
code = ''
|
||||
"""
|
||||
If set, used as javascript code executed when the action is
|
||||
activated
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def register_code(cl):
|
||||
"""
|
||||
Render a Javascript code that append the code to the available actions.
|
||||
Used by Actions
|
||||
"""
|
||||
if not cl.code:
|
||||
return ''
|
||||
|
||||
return """
|
||||
Actions.register('{cl.id}', '{cl.symbol}', '{cl.title}', {cl.code})
|
||||
""".format(cl = cl)
|
||||
|
||||
@classmethod
|
||||
def has_me(cl, object):
|
||||
return hasattr(object, 'actions') and cl.id in object.actions
|
||||
|
||||
@classmethod
|
||||
def to_str(cl, object, url = None, **data):
|
||||
"""
|
||||
Utility class to add the action on the object using the
|
||||
given data.
|
||||
"""
|
||||
if cl.has_me(object):
|
||||
return
|
||||
|
||||
code = \
|
||||
'<a class="action" {onclick} action="{cl.id}" {data} title="{cl.title}">' \
|
||||
'{cl.symbol}<label>{cl.title}</label>' \
|
||||
'</a>'.format(
|
||||
href = '' if not url else 'href="' + url + '"',
|
||||
onclick = 'onclick="return Actions.run(event, \'{cl.id}\');"' \
|
||||
.format(cl = cl)
|
||||
if cl.id and cl.code else '',
|
||||
data = ' '.join('data-{k}="{v}"'.format(k=k, v=v)
|
||||
for k,v in data.items()),
|
||||
cl = cl
|
||||
)
|
||||
|
||||
return code
|
||||
|
||||
@classmethod
|
||||
def test(cl, request, object, in_list):
|
||||
"""
|
||||
Test if the given object can have the generated action. If yes, return
|
||||
the generated content, otherwise, return None
|
||||
|
||||
in_list: object is rendered in a list
|
||||
"""
|
||||
|
||||
|
|
@ -171,6 +171,11 @@ class Post (models.Model, Routable):
|
|||
"""
|
||||
Fields on which routes.SearchRoute must run the search
|
||||
"""
|
||||
actions = None
|
||||
"""
|
||||
Actions are a list of actions available to the end user for this model.
|
||||
See aircox.cms.actions for more information
|
||||
"""
|
||||
|
||||
def get_comments(self):
|
||||
"""
|
||||
|
|
|
@ -17,7 +17,8 @@ from django.utils.translation import ugettext as _, ugettext_lazy
|
|||
from honeypot.decorators import check_honeypot
|
||||
|
||||
from aircox.cms.forms import CommentForm
|
||||
import aircox.cms.decorators as decorators
|
||||
from aircox.cms.exposures import expose
|
||||
from aircox.cms.actions import Actions
|
||||
|
||||
|
||||
class Viewable:
|
||||
|
@ -51,7 +52,7 @@ class Viewable:
|
|||
setattr(Sub, k, v)
|
||||
|
||||
if hasattr(cl, '_exposure'):
|
||||
return decorators.expose(Sub)
|
||||
return expose(Sub)
|
||||
return Sub
|
||||
|
||||
|
||||
|
@ -224,6 +225,7 @@ class ListItem:
|
|||
image = None
|
||||
info = None
|
||||
url = None
|
||||
actions = None
|
||||
|
||||
css_class = None
|
||||
attrs = None
|
||||
|
@ -285,7 +287,7 @@ class List(Section):
|
|||
def get_object_list(self):
|
||||
return self.object_list
|
||||
|
||||
def prepare_object_list(self, object_list):
|
||||
def prepare_list(self, object_list):
|
||||
"""
|
||||
Prepare objects before context is sent to the template renderer.
|
||||
Return the object_list that is prepared.
|
||||
|
@ -302,7 +304,7 @@ class List(Section):
|
|||
instances of Post or ListItem.
|
||||
|
||||
If object_list is not given, call `get_object_list` to retrieve it.
|
||||
Prepare the object_list using `self.prepare_object_list`.
|
||||
Prepare the object_list using `self.prepare_list`.
|
||||
|
||||
Set `request`, `object`, `object_list` and `kwargs` in self.
|
||||
"""
|
||||
|
@ -314,10 +316,11 @@ class List(Section):
|
|||
object_list = self.object_list or self.get_object_list()
|
||||
if not object_list and not self.message_empty:
|
||||
return
|
||||
self.object_list = object_list
|
||||
|
||||
self.object_list = object_list
|
||||
if object_list:
|
||||
object_list = self.prepare_object_list(object_list)
|
||||
object_list = self.prepare_list(object_list)
|
||||
Actions.make(request, object_list = object_list)
|
||||
|
||||
context = super().get_context_data(request, object, *args, **kwargs)
|
||||
context.update({
|
||||
|
@ -500,7 +503,7 @@ class Search(Section):
|
|||
)
|
||||
|
||||
|
||||
@decorators.expose
|
||||
@expose
|
||||
class Calendar(Section):
|
||||
model = None
|
||||
template_name = "aircox/cms/calendar.html"
|
||||
|
@ -535,11 +538,12 @@ class Calendar(Section):
|
|||
})
|
||||
return context
|
||||
|
||||
@decorators.expose
|
||||
@expose
|
||||
def render_exp(cl, *args, year, month, **kwargs):
|
||||
year = int(year)
|
||||
month = int(month)
|
||||
return cl.render(*args, year = year, month = month, **kwargs)
|
||||
|
||||
render_exp._exposure.name = 'render'
|
||||
render_exp._exposure.pattern = '(?P<year>[0-9]{4})/(?P<month>[0-1]?[0-9])'
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ var Actions = {
|
|||
}
|
||||
},
|
||||
|
||||
|
||||
/// Init an existing action HTML element
|
||||
init_action: function(item, action_id, data, url) {
|
||||
var action = this.registry[action_id];
|
||||
|
@ -54,15 +53,15 @@ var Actions = {
|
|||
|
||||
action = Actions.registry[action];
|
||||
if(!action)
|
||||
return
|
||||
return;
|
||||
|
||||
data = item.data || item.dataset;
|
||||
action.handler(data, item);
|
||||
action.handler(item.data || item.dataset, item);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
document.addEventListener('DOMContentLoaded', function(event) {
|
||||
var items = document.querySelectorAll('.action[action]');
|
||||
for(var i = 0; i < items.length; i++) {
|
||||
|
@ -72,6 +71,7 @@ document.addEventListener('DOMContentLoaded', function(event) {
|
|||
Actions.init_action(item, action_id, data);
|
||||
}
|
||||
}, false);
|
||||
*/
|
||||
|
||||
|
||||
/// Small utility used to make XMLHttpRequests, and map results on objects.
|
||||
|
|
|
@ -60,10 +60,9 @@
|
|||
|
||||
{% 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 %}
|
||||
{% for action in object.actions %}
|
||||
{{ action|safe }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -14,6 +14,14 @@
|
|||
<link rel="stylesheet" href="{% static website.styles %}" type="text/css">
|
||||
{% endif %}
|
||||
<script src="{% static "aircox/cms/scripts.js" %}"></script>
|
||||
|
||||
{% if actions %}
|
||||
<script>
|
||||
{{ actions|safe }}
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<title>{% if title %}{{ title|striptags }} - {% endif %}{{ website.name }}</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -2,6 +2,7 @@ from django import template
|
|||
from django.core.urlresolvers import reverse
|
||||
|
||||
import aircox.cms.utils as utils
|
||||
import aircox.cms.actions as actions
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
@ -40,8 +41,12 @@ def threads(post, sep = '/'):
|
|||
for post in posts[:-1] if post.published
|
||||
])
|
||||
|
||||
|
||||
@register.filter(name='around')
|
||||
def around(page_num, n):
|
||||
"""
|
||||
Return a range of value around a given number.
|
||||
"""
|
||||
return range(page_num-n, page_num+n+1)
|
||||
|
||||
|
||||
|
|
11
cms/views.py
11
cms/views.py
|
@ -6,8 +6,9 @@ from django.utils.translation import ugettext as _, ugettext_lazy
|
|||
from django.contrib import messages
|
||||
from django.http import Http404
|
||||
|
||||
from aircox.cms.actions import Actions
|
||||
import aircox.cms.sections as sections
|
||||
import aircox.cms.sections as sections_
|
||||
sections_ = sections # used for name clashes
|
||||
|
||||
|
||||
class BaseView:
|
||||
|
@ -101,6 +102,7 @@ class BaseView:
|
|||
for k, v in self.menus.items()
|
||||
if v is not self
|
||||
}
|
||||
context['actions'] = Actions.register_code()
|
||||
context['embed'] = False
|
||||
else:
|
||||
context['embed'] = True
|
||||
|
@ -163,7 +165,7 @@ class PostListView(BaseView, ListView):
|
|||
|
||||
return qs
|
||||
|
||||
def init_list(self):
|
||||
def prepare_list(self):
|
||||
if not self.list:
|
||||
self.list = sections.List(
|
||||
truncate = 32,
|
||||
|
@ -180,8 +182,11 @@ class PostListView(BaseView, ListView):
|
|||
if field in self.list.fields
|
||||
]
|
||||
|
||||
# done in list
|
||||
# Actions.make(self.request, object_list = self.object_list)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
self.init_list()
|
||||
self.prepare_list()
|
||||
self.add_css_class('list')
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
|
17
notes.md
17
notes.md
|
@ -18,34 +18,33 @@
|
|||
- config generation and sound diffusion
|
||||
|
||||
- cms:
|
||||
- empty content -> empty string
|
||||
- empty content/list -> nothing
|
||||
- update documentation:
|
||||
- cms.script
|
||||
- cms.exposure; make it right, see nomenclature, + docstring
|
||||
- cms.actions;
|
||||
- admin cms
|
||||
- sections:
|
||||
- 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
|
||||
- print program's name in lists / clean up that thing also a bit
|
||||
- article list with the focus
|
||||
- player:
|
||||
- mixcloud
|
||||
- seek bar
|
||||
- seek bar + timer
|
||||
- remove from playing playlist -> stop
|
||||
- list of played diffusions and tracks when non-stop;
|
||||
|
||||
# Later todo
|
||||
# Long term 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
|
||||
- actions -> noscript case, think of accessibility
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -56,10 +56,11 @@ class NameableAdmin(admin.ModelAdmin):
|
|||
@admin.register(Sound)
|
||||
class SoundAdmin(NameableAdmin):
|
||||
fields = None
|
||||
list_display = ['id', 'name', 'duration', 'type', 'mtime', 'good_quality', 'removed']
|
||||
list_display = ['id', 'name', 'duration', 'type', 'mtime',
|
||||
'public', 'good_quality', 'removed']
|
||||
fieldsets = [
|
||||
(None, { 'fields': NameableAdmin.fields + ['path', 'type', 'diffusion'] } ),
|
||||
(None, { 'fields': ['embed', 'duration', 'mtime'] }),
|
||||
(None, { 'fields': ['embed', 'duration', 'public', 'mtime'] }),
|
||||
(None, { 'fields': ['removed', 'good_quality' ] } )
|
||||
]
|
||||
readonly_fields = ('path', 'duration',)
|
||||
|
|
|
@ -154,6 +154,11 @@ class Sound(Nameable):
|
|||
default = False,
|
||||
help_text = _('sound\'s quality is okay')
|
||||
)
|
||||
public = models.BooleanField(
|
||||
_('public'),
|
||||
default = False,
|
||||
help_text = _('the sound is accessible to the public')
|
||||
)
|
||||
|
||||
def get_mtime(self):
|
||||
"""
|
||||
|
@ -204,14 +209,40 @@ class Sound(Nameable):
|
|||
return True
|
||||
return old_removed != self.removed
|
||||
|
||||
def save(self, check = True, *args, **kwargs):
|
||||
if check:
|
||||
self.check_on_file()
|
||||
def check_perms(self):
|
||||
"""
|
||||
Check permissions and update them if this is activated
|
||||
"""
|
||||
if not settings.AIRCOX_SOUND_AUTO_CHMOD or \
|
||||
self.removed or not os.path.exists(self.path):
|
||||
return
|
||||
|
||||
flags = settings.AIRCOX_SOUND_CHMOD_FLAGS[self.public]
|
||||
try:
|
||||
os.chmod(self.path, flags)
|
||||
except PermissionError as err:
|
||||
logger.error(
|
||||
'cannot set permissions {} to file {}: {}'.format(
|
||||
self.flags[self.public],
|
||||
self.path, err
|
||||
)
|
||||
)
|
||||
|
||||
def __check_name(self):
|
||||
if not self.name and self.path:
|
||||
# FIXME: later, remove date?
|
||||
self.name = os.path.basename(self.path)
|
||||
self.name = os.path.splitext(self.name)[0]
|
||||
self.name = self.name.replace('_', ' ')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.__check_name()
|
||||
|
||||
def save(self, check = True, *args, **kwargs):
|
||||
if check:
|
||||
self.check_on_file()
|
||||
self.__check_name()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import stat
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
@ -18,6 +19,15 @@ ensure('AIRCOX_SOUND_ARCHIVES_SUBDIR', 'archives')
|
|||
# Sub directory used for the excerpts of the episode
|
||||
ensure('AIRCOX_SOUND_EXCERPTS_SUBDIR', 'excerpts')
|
||||
|
||||
# Change sound perms based on 'public' attribute if True
|
||||
ensure('AIRCOX_SOUND_AUTO_CHMOD', True)
|
||||
# Chmod bits flags as a tuple for (not public, public). Use os.chmod
|
||||
# and stat.*
|
||||
ensure(
|
||||
'AIRCOX_SOUND_CHMOD_FLAGS',
|
||||
(stat.S_IRWXU, stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH )
|
||||
)
|
||||
|
||||
# Quality attributes passed to sound_quality_check from sounds_monitor
|
||||
ensure('AIRCOX_SOUND_QUALITY', {
|
||||
'attribute': 'RMS lev dB',
|
||||
|
|
75
website/actions.py
Normal file
75
website/actions.py
Normal 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);
|
||||
}
|
||||
"""
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
6
website/utils.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
def duration_to_str(duration):
|
||||
return duration.strftime(
|
||||
'%H:%M:%S' if duration.hour else '%M:%S'
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user