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
|
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):
|
def get_comments(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -17,7 +17,8 @@ from django.utils.translation import ugettext as _, ugettext_lazy
|
||||||
from honeypot.decorators import check_honeypot
|
from honeypot.decorators import check_honeypot
|
||||||
|
|
||||||
from aircox.cms.forms import CommentForm
|
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:
|
class Viewable:
|
||||||
|
@ -51,7 +52,7 @@ class Viewable:
|
||||||
setattr(Sub, k, v)
|
setattr(Sub, k, v)
|
||||||
|
|
||||||
if hasattr(cl, '_exposure'):
|
if hasattr(cl, '_exposure'):
|
||||||
return decorators.expose(Sub)
|
return expose(Sub)
|
||||||
return Sub
|
return Sub
|
||||||
|
|
||||||
|
|
||||||
|
@ -224,6 +225,7 @@ class ListItem:
|
||||||
image = None
|
image = None
|
||||||
info = None
|
info = None
|
||||||
url = None
|
url = None
|
||||||
|
actions = None
|
||||||
|
|
||||||
css_class = None
|
css_class = None
|
||||||
attrs = None
|
attrs = None
|
||||||
|
@ -285,7 +287,7 @@ class List(Section):
|
||||||
def get_object_list(self):
|
def get_object_list(self):
|
||||||
return self.object_list
|
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.
|
Prepare objects before context is sent to the template renderer.
|
||||||
Return the object_list that is prepared.
|
Return the object_list that is prepared.
|
||||||
|
@ -302,7 +304,7 @@ class List(Section):
|
||||||
instances of Post or ListItem.
|
instances of Post or ListItem.
|
||||||
|
|
||||||
If object_list is not given, call `get_object_list` to retrieve it.
|
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.
|
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()
|
object_list = self.object_list or self.get_object_list()
|
||||||
if not object_list and not self.message_empty:
|
if not object_list and not self.message_empty:
|
||||||
return
|
return
|
||||||
self.object_list = object_list
|
|
||||||
|
|
||||||
|
self.object_list = object_list
|
||||||
if 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 = super().get_context_data(request, object, *args, **kwargs)
|
||||||
context.update({
|
context.update({
|
||||||
|
@ -500,7 +503,7 @@ class Search(Section):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@decorators.expose
|
@expose
|
||||||
class Calendar(Section):
|
class Calendar(Section):
|
||||||
model = None
|
model = None
|
||||||
template_name = "aircox/cms/calendar.html"
|
template_name = "aircox/cms/calendar.html"
|
||||||
|
@ -535,11 +538,12 @@ class Calendar(Section):
|
||||||
})
|
})
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@decorators.expose
|
@expose
|
||||||
def render_exp(cl, *args, year, month, **kwargs):
|
def render_exp(cl, *args, year, month, **kwargs):
|
||||||
year = int(year)
|
year = int(year)
|
||||||
month = int(month)
|
month = int(month)
|
||||||
return cl.render(*args, year = year, month = month, **kwargs)
|
return cl.render(*args, year = year, month = month, **kwargs)
|
||||||
|
|
||||||
render_exp._exposure.name = 'render'
|
render_exp._exposure.name = 'render'
|
||||||
render_exp._exposure.pattern = '(?P<year>[0-9]{4})/(?P<month>[0-1]?[0-9])'
|
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 an existing action HTML element
|
||||||
init_action: function(item, action_id, data, url) {
|
init_action: function(item, action_id, data, url) {
|
||||||
var action = this.registry[action_id];
|
var action = this.registry[action_id];
|
||||||
|
@ -54,15 +53,15 @@ var Actions = {
|
||||||
|
|
||||||
action = Actions.registry[action];
|
action = Actions.registry[action];
|
||||||
if(!action)
|
if(!action)
|
||||||
return
|
return;
|
||||||
|
|
||||||
data = item.data || item.dataset;
|
action.handler(item.data || item.dataset, item);
|
||||||
action.handler(data, item);
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
document.addEventListener('DOMContentLoaded', function(event) {
|
document.addEventListener('DOMContentLoaded', function(event) {
|
||||||
var items = document.querySelectorAll('.action[action]');
|
var items = document.querySelectorAll('.action[action]');
|
||||||
for(var i = 0; i < items.length; i++) {
|
for(var i = 0; i < items.length; i++) {
|
||||||
|
@ -72,6 +71,7 @@ document.addEventListener('DOMContentLoaded', function(event) {
|
||||||
Actions.init_action(item, action_id, data);
|
Actions.init_action(item, action_id, data);
|
||||||
}
|
}
|
||||||
}, false);
|
}, false);
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
/// Small utility used to make XMLHttpRequests, and map results on objects.
|
/// Small utility used to make XMLHttpRequests, and map results on objects.
|
||||||
|
|
|
@ -60,10 +60,9 @@
|
||||||
|
|
||||||
{% if object.actions and 'actions' in list.fields %}
|
{% if object.actions and 'actions' in list.fields %}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
{% for action_id,action_data in object.actions.items %}
|
{% for action in object.actions %}
|
||||||
<a class="action" action="{{action_id}}"
|
{{ action|safe }}
|
||||||
{% for k,v in action_data.items %}data-{{ k }}="{{ v }}"{% endfor %}></a>
|
{% endfor %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,14 @@
|
||||||
<link rel="stylesheet" href="{% static website.styles %}" type="text/css">
|
<link rel="stylesheet" href="{% static website.styles %}" type="text/css">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<script src="{% static "aircox/cms/scripts.js" %}"></script>
|
<script src="{% static "aircox/cms/scripts.js" %}"></script>
|
||||||
|
|
||||||
|
{% if actions %}
|
||||||
|
<script>
|
||||||
|
{{ actions|safe }}
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<title>{% if title %}{{ title|striptags }} - {% endif %}{{ website.name }}</title>
|
<title>{% if title %}{{ title|striptags }} - {% endif %}{{ website.name }}</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -2,6 +2,7 @@ from django import template
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
import aircox.cms.utils as utils
|
import aircox.cms.utils as utils
|
||||||
|
import aircox.cms.actions as actions
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
@ -40,8 +41,12 @@ def threads(post, sep = '/'):
|
||||||
for post in posts[:-1] if post.published
|
for post in posts[:-1] if post.published
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name='around')
|
@register.filter(name='around')
|
||||||
def around(page_num, n):
|
def around(page_num, n):
|
||||||
|
"""
|
||||||
|
Return a range of value around a given number.
|
||||||
|
"""
|
||||||
return range(page_num-n, page_num+n+1)
|
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.contrib import messages
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
|
|
||||||
|
from aircox.cms.actions import Actions
|
||||||
import aircox.cms.sections as sections
|
import aircox.cms.sections as sections
|
||||||
import aircox.cms.sections as sections_
|
sections_ = sections # used for name clashes
|
||||||
|
|
||||||
|
|
||||||
class BaseView:
|
class BaseView:
|
||||||
|
@ -101,6 +102,7 @@ class BaseView:
|
||||||
for k, v in self.menus.items()
|
for k, v in self.menus.items()
|
||||||
if v is not self
|
if v is not self
|
||||||
}
|
}
|
||||||
|
context['actions'] = Actions.register_code()
|
||||||
context['embed'] = False
|
context['embed'] = False
|
||||||
else:
|
else:
|
||||||
context['embed'] = True
|
context['embed'] = True
|
||||||
|
@ -163,7 +165,7 @@ class PostListView(BaseView, ListView):
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def init_list(self):
|
def prepare_list(self):
|
||||||
if not self.list:
|
if not self.list:
|
||||||
self.list = sections.List(
|
self.list = sections.List(
|
||||||
truncate = 32,
|
truncate = 32,
|
||||||
|
@ -180,8 +182,11 @@ class PostListView(BaseView, ListView):
|
||||||
if field in self.list.fields
|
if field in self.list.fields
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# done in list
|
||||||
|
# Actions.make(self.request, object_list = self.object_list)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
self.init_list()
|
self.prepare_list()
|
||||||
self.add_css_class('list')
|
self.add_css_class('list')
|
||||||
|
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
17
notes.md
17
notes.md
|
@ -18,34 +18,33 @@
|
||||||
- config generation and sound diffusion
|
- config generation and sound diffusion
|
||||||
|
|
||||||
- cms:
|
- cms:
|
||||||
- empty content -> empty string
|
- empty content/list -> nothing
|
||||||
- update documentation:
|
- update documentation:
|
||||||
- cms.script
|
- cms.script
|
||||||
- cms.exposure; make it right, see nomenclature, + docstring
|
- cms.exposure; make it right, see nomenclature, + docstring
|
||||||
|
- cms.actions;
|
||||||
- admin cms
|
- admin cms
|
||||||
- sections:
|
|
||||||
- article list with the focus
|
|
||||||
-> set html attribute based on values that are public
|
|
||||||
|
|
||||||
- website:
|
- website:
|
||||||
- render schedule does not get the correct list
|
- render schedule does not get the correct list
|
||||||
-> postlistview has not the same queryset as website/sections/schedule
|
-> postlistview has not the same queryset as website/sections/schedule
|
||||||
- diffusions:
|
- diffusions:
|
||||||
- filter sounds for undiffused diffusions
|
- print program's name in lists / clean up that thing also a bit
|
||||||
- print sounds of diffusions
|
- article list with the focus
|
||||||
- print program's name in lists
|
|
||||||
- player:
|
- player:
|
||||||
- mixcloud
|
- mixcloud
|
||||||
- seek bar
|
- seek bar + timer
|
||||||
|
- remove from playing playlist -> stop
|
||||||
- list of played diffusions and tracks when non-stop;
|
- list of played diffusions and tracks when non-stop;
|
||||||
|
|
||||||
# Later todo
|
# Long term TODO
|
||||||
- sounds monitor: max_size of path, take in account
|
- sounds monitor: max_size of path, take in account
|
||||||
- logs: archive functionnality
|
- logs: archive functionnality
|
||||||
- track stats for diffusions
|
- track stats for diffusions
|
||||||
- debug/prod configuration
|
- debug/prod configuration
|
||||||
- player support diffusions with multiple archive files
|
- player support diffusions with multiple archive files
|
||||||
- view as grid
|
- view as grid
|
||||||
|
- actions -> noscript case, think of accessibility
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -56,10 +56,11 @@ class NameableAdmin(admin.ModelAdmin):
|
||||||
@admin.register(Sound)
|
@admin.register(Sound)
|
||||||
class SoundAdmin(NameableAdmin):
|
class SoundAdmin(NameableAdmin):
|
||||||
fields = None
|
fields = None
|
||||||
list_display = ['id', 'name', 'duration', 'type', 'mtime', 'good_quality', 'removed']
|
list_display = ['id', 'name', 'duration', 'type', 'mtime',
|
||||||
|
'public', 'good_quality', 'removed']
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
(None, { 'fields': NameableAdmin.fields + ['path', 'type', 'diffusion'] } ),
|
(None, { 'fields': NameableAdmin.fields + ['path', 'type', 'diffusion'] } ),
|
||||||
(None, { 'fields': ['embed', 'duration', 'mtime'] }),
|
(None, { 'fields': ['embed', 'duration', 'public', 'mtime'] }),
|
||||||
(None, { 'fields': ['removed', 'good_quality' ] } )
|
(None, { 'fields': ['removed', 'good_quality' ] } )
|
||||||
]
|
]
|
||||||
readonly_fields = ('path', 'duration',)
|
readonly_fields = ('path', 'duration',)
|
||||||
|
|
|
@ -154,6 +154,11 @@ class Sound(Nameable):
|
||||||
default = False,
|
default = False,
|
||||||
help_text = _('sound\'s quality is okay')
|
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):
|
def get_mtime(self):
|
||||||
"""
|
"""
|
||||||
|
@ -204,14 +209,40 @@ class Sound(Nameable):
|
||||||
return True
|
return True
|
||||||
return old_removed != self.removed
|
return old_removed != self.removed
|
||||||
|
|
||||||
def save(self, check = True, *args, **kwargs):
|
def check_perms(self):
|
||||||
if check:
|
"""
|
||||||
self.check_on_file()
|
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:
|
if not self.name and self.path:
|
||||||
|
# FIXME: later, remove date?
|
||||||
self.name = os.path.basename(self.path)
|
self.name = os.path.basename(self.path)
|
||||||
self.name = os.path.splitext(self.name)[0]
|
self.name = os.path.splitext(self.name)[0]
|
||||||
self.name = self.name.replace('_', ' ')
|
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)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import os
|
import os
|
||||||
|
import stat
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
@ -18,6 +19,15 @@ ensure('AIRCOX_SOUND_ARCHIVES_SUBDIR', 'archives')
|
||||||
# Sub directory used for the excerpts of the episode
|
# Sub directory used for the excerpts of the episode
|
||||||
ensure('AIRCOX_SOUND_EXCERPTS_SUBDIR', 'excerpts')
|
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
|
# Quality attributes passed to sound_quality_check from sounds_monitor
|
||||||
ensure('AIRCOX_SOUND_QUALITY', {
|
ensure('AIRCOX_SOUND_QUALITY', {
|
||||||
'attribute': 'RMS lev dB',
|
'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_inline(programs.Diffusion, TrackInline, True)
|
||||||
cms.inject_related_inline(models.Program, True)
|
cms.inject_related_inline(models.Program, True)
|
||||||
cms.inject_related_inline(models.Diffusion, 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.programs.models as programs
|
||||||
import aircox.cms.models as cms
|
import aircox.cms.models as cms
|
||||||
|
import aircox.website.actions as actions
|
||||||
|
|
||||||
|
|
||||||
class Article (cms.Post):
|
class Article (cms.Post):
|
||||||
|
@ -50,6 +51,8 @@ class Program (cms.RelatedPost):
|
||||||
|
|
||||||
|
|
||||||
class Diffusion (cms.RelatedPost):
|
class Diffusion (cms.RelatedPost):
|
||||||
|
actions = [actions.Play, actions.AddToPlaylist]
|
||||||
|
|
||||||
class Relation:
|
class Relation:
|
||||||
model = programs.Diffusion
|
model = programs.Diffusion
|
||||||
bindings = {
|
bindings = {
|
||||||
|
@ -78,53 +81,3 @@ class Diffusion (cms.RelatedPost):
|
||||||
'day': self.related.initial.start.strftime('%A %d/%m')
|
'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.models as cms
|
||||||
import aircox.cms.routes as routes
|
import aircox.cms.routes as routes
|
||||||
import aircox.cms.sections as sections
|
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.models as models
|
||||||
|
import aircox.website.actions as actions
|
||||||
|
import aircox.website.utils as utils
|
||||||
|
|
||||||
|
|
||||||
@decorators.expose
|
@expose
|
||||||
class Player(sections.Section):
|
class Player(sections.Section):
|
||||||
"""
|
"""
|
||||||
Display a player that is cool.
|
Display a player that is cool.
|
||||||
|
@ -24,7 +28,7 @@ class Player(sections.Section):
|
||||||
"""
|
"""
|
||||||
#default_sounds
|
#default_sounds
|
||||||
|
|
||||||
@decorators.expose
|
@expose
|
||||||
def on_air(cl, request):
|
def on_air(cl, request):
|
||||||
qs = programs.Diffusion.get(
|
qs = programs.Diffusion.get(
|
||||||
now = True,
|
now = True,
|
||||||
|
@ -46,17 +50,56 @@ class Player(sections.Section):
|
||||||
'item': post,
|
'item': post,
|
||||||
'list': sections.List,
|
'list': sections.List,
|
||||||
}
|
}
|
||||||
|
|
||||||
on_air._exposure.template_name = 'aircox/cms/list_item.html'
|
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):
|
def get_context_data(self, *args, **kwargs):
|
||||||
context = super().get_context_data(*args, **kwargs)
|
context = super().get_context_data(*args, **kwargs)
|
||||||
|
|
||||||
context.update({
|
context.update({
|
||||||
'base_template': 'aircox/cms/section.html',
|
'base_template': 'aircox/cms/section.html',
|
||||||
'live_streams': self.live_streams,
|
'live_streams': self.live_streams,
|
||||||
'last_sounds': models.Sound.objects. \
|
'recents': self.get_recents(10),
|
||||||
filter(published = True). \
|
|
||||||
order_by('-pk')[:10],
|
|
||||||
})
|
})
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
@ -99,7 +142,7 @@ class Diffusions(sections.List):
|
||||||
# .order_by('-start')[:self.prev_count])
|
# .order_by('-start')[:self.prev_count])
|
||||||
#return r
|
#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:
|
This function just prepare the list of object, in order to:
|
||||||
- have a good title
|
- have a good title
|
||||||
|
@ -115,17 +158,6 @@ class Diffusions(sections.List):
|
||||||
post.title = ': ' + post.title if post.title else \
|
post.title = ': ' + post.title if post.title else \
|
||||||
' // ' + post.related.start.strftime('%A %d %B')
|
' // ' + post.related.start.strftime('%A %d %B')
|
||||||
post.title = name + post.title
|
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
|
return object_list
|
||||||
|
|
||||||
def get_object_list(self):
|
def get_object_list(self):
|
||||||
|
@ -205,7 +237,24 @@ class Playlist(sections.List):
|
||||||
|
|
||||||
|
|
||||||
class Sounds(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):
|
class Schedule(Diffusions):
|
||||||
|
|
|
@ -102,7 +102,7 @@
|
||||||
.playlist .actions label,
|
.playlist .actions label,
|
||||||
#playlist-live .actions,
|
#playlist-live .actions,
|
||||||
#playlist-recents .actions a.action[action="remove"],
|
#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.action[action="sound.play"],
|
||||||
.playlist .actions a.url:not([href]),
|
.playlist .actions a.url:not([href]),
|
||||||
.playlist .actions a.url[href=""] {
|
.playlist .actions a.url[href=""] {
|
||||||
|
@ -118,8 +118,11 @@
|
||||||
<h2 class="title"></h2>
|
<h2 class="title"></h2>
|
||||||
<div class="info"></div>
|
<div class="info"></div>
|
||||||
<div class="actions">
|
<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="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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<div class="player-box">
|
<div class="player-box">
|
||||||
|
@ -130,7 +133,7 @@
|
||||||
Your browser does not support the <code>audio</code> element.
|
Your browser does not support the <code>audio</code> element.
|
||||||
</audio>
|
</audio>
|
||||||
|
|
||||||
<span class="player-button" onclick="player.play()"
|
<span class="player-button" onclick="Player.play()"
|
||||||
title="{% trans "play/pause" %}"></span>
|
title="{% trans "play/pause" %}"></span>
|
||||||
|
|
||||||
<h3 class="title"></h3>
|
<h3 class="title"></h3>
|
||||||
|
@ -149,7 +152,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
playerStore = {
|
PlayerStore = {
|
||||||
// save data to localstorage, or remove it if data is null
|
// save data to localstorage, or remove it if data is null
|
||||||
set: function(name, data) {
|
set: function(name, data) {
|
||||||
name = 'player.' + name;
|
name = 'player.' + name;
|
||||||
|
@ -194,8 +197,7 @@ playerStore = {
|
||||||
// * tab: text to put in the tab
|
// * tab: text to put in the tab
|
||||||
// * items: list of items to append
|
// * items: list of items to append
|
||||||
// * store: store the playlist in localStorage
|
// * store: store the playlist in localStorage
|
||||||
function Playlist(player, name, tab, items, store = false) {
|
function Playlist(name, tab, items, store = false) {
|
||||||
this.player = player;
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.store = store;
|
this.store = store;
|
||||||
|
|
||||||
|
@ -206,14 +208,14 @@ function Playlist(player, name, tab, items, store = false) {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.tab = document.createElement('a');
|
this.tab = document.createElement('a');
|
||||||
this.tab.addEventListener('click', function(event) {
|
this.tab.addEventListener('click', function(event) {
|
||||||
player.select_playlist(self);
|
Player.select_playlist(self);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}, true);
|
}, true);
|
||||||
this.tab.className = 'tab';
|
this.tab.className = 'tab';
|
||||||
this.tab.innerHTML = tab;
|
this.tab.innerHTML = tab;
|
||||||
|
|
||||||
player.playlists.appendChild(this.playlist);
|
Player.playlists.appendChild(this.playlist);
|
||||||
player.playlists.querySelector('nav').appendChild(this.tab);
|
Player.playlists.querySelector('nav').appendChild(this.tab);
|
||||||
|
|
||||||
this.items = [];
|
this.items = [];
|
||||||
if(store)
|
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.
|
/// return the existing item or the newly created item.
|
||||||
add: function(item, container) {
|
add: function(item, container) {
|
||||||
var item_ = this.find(item);
|
var item_ = this.find(item);
|
||||||
if(item_)
|
if(item_)
|
||||||
return 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');
|
elm.removeAttribute('style');
|
||||||
|
|
||||||
if(!container)
|
if(!container)
|
||||||
|
@ -255,10 +272,18 @@ Playlist.prototype = {
|
||||||
else
|
else
|
||||||
container.appendChild(elm);
|
container.appendChild(elm);
|
||||||
|
|
||||||
item.elm = elm;
|
item = {
|
||||||
item.playlist = this;
|
title: item.title,
|
||||||
elm.item = item;
|
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('.title').innerHTML = item.title || '';
|
||||||
elm.querySelector('.url').href = item.url || '';
|
elm.querySelector('.url').href = item.url || '';
|
||||||
elm.querySelector('.info').innerHTML = item.info || '';
|
elm.querySelector('.info').innerHTML = item.info || '';
|
||||||
|
@ -273,13 +298,13 @@ Playlist.prototype = {
|
||||||
|
|
||||||
var item = event.currentTarget.item;
|
var item = event.currentTarget.item;
|
||||||
if(item.stream || item.embed)
|
if(item.stream || item.embed)
|
||||||
player.select(item);
|
Player.select(item);
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
return true;
|
return true;
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
if(item.embed || item.stream)
|
if(item.embed || item.stream)
|
||||||
player.add_actions(item, elm);
|
this.add_actions(item, elm);
|
||||||
this.items.push(item);
|
this.items.push(item);
|
||||||
|
|
||||||
if(container == this.playlist && this.store)
|
if(container == this.playlist && this.store)
|
||||||
|
@ -322,32 +347,32 @@ Playlist.prototype = {
|
||||||
delete item.playlist;
|
delete item.playlist;
|
||||||
pl.push(item);
|
pl.push(item);
|
||||||
}
|
}
|
||||||
playerStore.set('playlist.' + this.name, pl)
|
PlayerStore.set('playlist.' + this.name, pl)
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Load playlist from local storage
|
/// Load playlist from local storage
|
||||||
load: function() {
|
load: function() {
|
||||||
var pl = playerStore.get('playlist.' + this.name);
|
var pl = PlayerStore.get('playlist.' + this.name);
|
||||||
if(pl)
|
if(pl)
|
||||||
this.add_list(pl);
|
this.add_list(pl);
|
||||||
},
|
},
|
||||||
|
|
||||||
/// called by the player when the given item is unselected
|
/// called by Player when the given item is unselected
|
||||||
unselect: function(player, item) {
|
unselect: function(item) {
|
||||||
this.tab.removeAttribute('active');
|
this.tab.removeAttribute('active');
|
||||||
if(item.elm)
|
if(item.elm)
|
||||||
item.elm.removeAttribute('selected');
|
item.elm.removeAttribute('selected');
|
||||||
|
|
||||||
var audio = this.player.audio;
|
var audio = Player.audio;
|
||||||
if(this.store && !audio.ended) {
|
if(this.store && !audio.ended) {
|
||||||
item.currentTime = audio.currentTime;
|
item.currentTime = audio.currentTime;
|
||||||
this.save();
|
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.
|
/// prepare it.
|
||||||
select: function(player, item) {
|
select: function(item) {
|
||||||
this.tab.setAttribute('active', 'true');
|
this.tab.setAttribute('active', 'true');
|
||||||
if(item.elm)
|
if(item.elm)
|
||||||
item.elm.setAttribute('selected', 'true');
|
item.elm.setAttribute('selected', 'true');
|
||||||
|
@ -355,15 +380,15 @@ Playlist.prototype = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
player = {
|
Player = {
|
||||||
/// main container of the player
|
/// main container of the Player
|
||||||
player: undefined,
|
player: undefined,
|
||||||
/// <audio> container
|
/// <audio> container
|
||||||
audio: undefined,
|
audio: undefined,
|
||||||
/// controls
|
/// controls
|
||||||
controls: undefined,
|
controls: undefined,
|
||||||
|
|
||||||
/// init player
|
/// init Player
|
||||||
init: function(id) {
|
init: function(id) {
|
||||||
this.player = document.getElementById(id);
|
this.player = document.getElementById(id);
|
||||||
this.audio = this.player.querySelector('audio');
|
this.audio = this.player.querySelector('audio');
|
||||||
|
@ -398,12 +423,12 @@ player = {
|
||||||
|
|
||||||
this.audio.addEventListener('timeupdate', function() {
|
this.audio.addEventListener('timeupdate', function() {
|
||||||
if(self.audio.seekable.length)
|
if(self.audio.seekable.length)
|
||||||
playerStore.set('stream.' + self.item.stream + '.pos',
|
PlayerStore.set('stream.' + self.item.stream + '.pos',
|
||||||
self.audio.currentTime)
|
self.audio.currentTime)
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
this.audio.addEventListener('ended', function() {
|
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');
|
single = self.player.querySelector('input.single');
|
||||||
if(!single.checked)
|
if(!single.checked)
|
||||||
|
@ -413,7 +438,7 @@ player = {
|
||||||
|
|
||||||
__init_playlists: function() {
|
__init_playlists: function() {
|
||||||
this.playlists = this.player.querySelector('.playlists');
|
this.playlists = this.player.querySelector('.playlists');
|
||||||
this.live = new Playlist(this,
|
this.live = new Playlist(
|
||||||
'live',
|
'live',
|
||||||
" {% trans "live" %}",
|
" {% trans "live" %}",
|
||||||
[ {% for sound in live_streams %}
|
[ {% for sound in live_streams %}
|
||||||
|
@ -421,25 +446,28 @@ player = {
|
||||||
url: "{{ sound.url }}",
|
url: "{{ sound.url }}",
|
||||||
stream: "{{ sound.url }}",
|
stream: "{{ sound.url }}",
|
||||||
info: "{{ sound.info }}",
|
info: "{{ sound.info }}",
|
||||||
|
seekable: false,
|
||||||
}, {% endfor %} ]
|
}, {% endfor %} ]
|
||||||
);
|
);
|
||||||
this.recents = new Playlist(this,
|
this.recents = new Playlist(
|
||||||
'recents', '{% trans "recents" %}',
|
'recents', '{% trans "recents" %}',
|
||||||
[ {% for sound in last_sounds %}
|
[ {% for sound in recents %}
|
||||||
{ title: "{{ sound.title }}",
|
{ title: "{{ sound.title }}",
|
||||||
url: "{{ sound.url }}",
|
url: "{{ sound.url }}",
|
||||||
{% if sound.related.embed %}
|
{% if sound.related.embed %}
|
||||||
embed: "{{ sound.related.embed }}",
|
embed: "{{ sound.related.embed }}",
|
||||||
{% else %}
|
{% else %}
|
||||||
stream: "{{ MEDIA_URL }}{{ sound.related.url|safe }}",
|
stream: "{{ sound.related.url|safe }}",
|
||||||
{% endif %}
|
{% endif %}
|
||||||
info: "{{ sound.related.duration|date:"i:s" }}",
|
info: "{{ sound.related.duration|date:"H:i:s" }}",
|
||||||
}, {% endfor %} ]
|
}, {% endfor %} ]
|
||||||
);
|
);
|
||||||
this.marked = new Playlist(this,
|
this.favorites = new Playlist(
|
||||||
'marked', '★ {% trans "marked" %}', null, true);
|
'favorites', '★ {% trans "favorites" %}', null, true
|
||||||
this.playlist = new Playlist(this,
|
);
|
||||||
'playlist', '☰ {% trans "playlist" %}', null, true);
|
this.playlist = new Playlist(
|
||||||
|
'playlist', '☰ {% trans "playlist" %}', null, true
|
||||||
|
);
|
||||||
|
|
||||||
this.select(this.live.items[0], false);
|
this.select(this.live.items[0], false);
|
||||||
this.select_playlist(this.recents);
|
this.select_playlist(this.recents);
|
||||||
|
@ -447,7 +475,7 @@ player = {
|
||||||
},
|
},
|
||||||
|
|
||||||
load: function() {
|
load: function() {
|
||||||
var data = playerStore.get('player');
|
var data = PlayerStore.get('Player');
|
||||||
if(!data)
|
if(!data)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -461,19 +489,17 @@ player = {
|
||||||
},
|
},
|
||||||
|
|
||||||
save: function() {
|
save: function() {
|
||||||
playerStore.set('player', {
|
PlayerStore.set('player', {
|
||||||
'selected_playlist': this.__playlist && this.__playlist.name,
|
'selected_playlist': this.__playlist && this.__playlist.name,
|
||||||
'stream': this.item && this.item.stream,
|
'stream': this.item && this.item.stream,
|
||||||
'single': this.controls.single.checked,
|
'single': this.controls.single.checked,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/** player actions **/
|
/** Player actions **/
|
||||||
/// play a given item { title, src }
|
/// play a given item { title, src }
|
||||||
play: function() {
|
play: function() {
|
||||||
var player = this.player;
|
|
||||||
var audio = this.audio;
|
var audio = this.audio;
|
||||||
|
|
||||||
if(audio.paused)
|
if(audio.paused)
|
||||||
audio.play();
|
audio.play();
|
||||||
else
|
else
|
||||||
|
@ -481,13 +507,16 @@ player = {
|
||||||
},
|
},
|
||||||
|
|
||||||
__ask_to_seek(item) {
|
__ask_to_seek(item) {
|
||||||
|
if(!item.seekable)
|
||||||
|
return;
|
||||||
|
|
||||||
var key = 'stream.' + item.stream + '.pos'
|
var key = 'stream.' + item.stream + '.pos'
|
||||||
var pos = playerStore.get(key);
|
var pos = PlayerStore.get(key);
|
||||||
if(!pos)
|
if(!pos)
|
||||||
return
|
return
|
||||||
if(confirm("{% trans "restart from the last position?" %}"))
|
if(confirm("{% trans "restart from the last position?" %}"))
|
||||||
this.audio.currentTime = Math.max(pos - 5, 0);
|
this.audio.currentTime = Math.max(pos - 5, 0);
|
||||||
playerStore.set(key);
|
PlayerStore.set(key);
|
||||||
},
|
},
|
||||||
|
|
||||||
/// select the current track to play, and start playing it
|
/// select the current track to play, and start playing it
|
||||||
|
@ -496,7 +525,7 @@ player = {
|
||||||
var player = this.player;
|
var player = this.player;
|
||||||
|
|
||||||
if(this.item && this.item.playlist)
|
if(this.item && this.item.playlist)
|
||||||
this.item.playlist.unselect(this, this.item);
|
this.item.playlist.unselect(this.item);
|
||||||
|
|
||||||
audio.pause();
|
audio.pause();
|
||||||
audio.src = item.stream;
|
audio.src = item.stream;
|
||||||
|
@ -504,7 +533,7 @@ player = {
|
||||||
|
|
||||||
this.item = item;
|
this.item = item;
|
||||||
if(this.item && this.item.playlist)
|
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]
|
player.querySelectorAll('#simple-player .title')[0]
|
||||||
.innerHTML = item.title;
|
.innerHTML = item.title;
|
||||||
|
@ -563,33 +592,12 @@ player = {
|
||||||
.send();
|
.send();
|
||||||
|
|
||||||
window.setTimeout(function() {
|
window.setTimeout(function() {
|
||||||
player.update_on_air();
|
Player.update_on_air();
|
||||||
}, 60000*5);
|
}, 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" %}',
|
Player.init('player');
|
||||||
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>
|
</script>
|
||||||
{% endblock %}
|
{% 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