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:
bkfox 2016-07-08 01:17:02 +02:00
parent e971f3f0b5
commit 88a5a9556e
19 changed files with 456 additions and 173 deletions

126
cms/actions.py Normal file
View 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
"""

View File

@ -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):
"""

View File

@ -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])'

View File

@ -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.

View File

@ -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 %}

View File

@ -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>

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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',)

View File

@ -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):

View File

@ -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
View 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);
}
"""

View File

@ -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)

View File

@ -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
)
)

View File

@ -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):

View File

@ -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
View File

@ -0,0 +1,6 @@
def duration_to_str(duration):
return duration.strftime(
'%H:%M:%S' if duration.hour else '%M:%S'
)