forked from rc/aircox
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:
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'
|
||||
)
|
||||
|
Reference in New Issue
Block a user