redesign streamer, make it sexy
This commit is contained in:
parent
8e1d2b6769
commit
432467ec8d
|
@ -1,3 +0,0 @@
|
||||||
include LICENSE
|
|
||||||
include README.md
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ class StationAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
@admin.register(Log)
|
@admin.register(Log)
|
||||||
class LogAdmin(admin.ModelAdmin):
|
class LogAdmin(admin.ModelAdmin):
|
||||||
list_display = ['id', 'date', 'station', 'source', 'type', 'diffusion', 'sound', 'track']
|
list_display = ['id', 'date', 'station', 'source', 'type', 'comment']
|
||||||
list_filter = ['date', 'source', 'station']
|
list_filter = ['date', 'source', 'station']
|
||||||
|
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -17,6 +17,10 @@ from .models import Port, Station, Sound
|
||||||
from .connector import Connector
|
from .connector import Connector
|
||||||
|
|
||||||
|
|
||||||
|
# FIXME liquidsoap does not manage timezones -- we have to convert
|
||||||
|
# 'on_air' metadata we get from it into utc one in order to work
|
||||||
|
# correctly.
|
||||||
|
|
||||||
local_tz = tzlocal.get_localzone()
|
local_tz = tzlocal.get_localzone()
|
||||||
logger = logging.getLogger('aircox')
|
logger = logging.getLogger('aircox')
|
||||||
|
|
||||||
|
@ -32,7 +36,11 @@ class Streamer:
|
||||||
sources = None
|
sources = None
|
||||||
""" List of all monitored sources """
|
""" List of all monitored sources """
|
||||||
source = None
|
source = None
|
||||||
""" Current on air source """
|
""" Current source being played on air """
|
||||||
|
# note: we disable on_air rids since we don't have use of it for the
|
||||||
|
# moment
|
||||||
|
# on_air = None
|
||||||
|
# """ On-air request ids (rid) """
|
||||||
|
|
||||||
def __init__(self, station):
|
def __init__(self, station):
|
||||||
self.station = station
|
self.station = station
|
||||||
|
@ -46,6 +54,27 @@ class Streamer:
|
||||||
""" Path to Unix socket file """
|
""" Path to Unix socket file """
|
||||||
return self.connector.address
|
return self.connector.address
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_ready(self):
|
||||||
|
"""
|
||||||
|
If external program is ready to use, returns True
|
||||||
|
"""
|
||||||
|
return self.send('list') != ''
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_running(self):
|
||||||
|
if self.process is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
returncode = self.process.poll()
|
||||||
|
if returncode is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.process = None
|
||||||
|
logger.debug('process died with return code %s' % returncode)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# FIXME: is it really needed as property?
|
||||||
@property
|
@property
|
||||||
def inputs(self):
|
def inputs(self):
|
||||||
""" Return input ports of the station """
|
""" Return input ports of the station """
|
||||||
|
@ -62,13 +91,6 @@ class Streamer:
|
||||||
active=True,
|
active=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def is_ready(self):
|
|
||||||
"""
|
|
||||||
If external program is ready to use, returns True
|
|
||||||
"""
|
|
||||||
return self.send('list') != ''
|
|
||||||
|
|
||||||
# Sources and config ###############################################
|
# Sources and config ###############################################
|
||||||
def send(self, *args, **kwargs):
|
def send(self, *args, **kwargs):
|
||||||
return self.connector.send(*args, **kwargs) or ''
|
return self.connector.send(*args, **kwargs) or ''
|
||||||
|
@ -98,26 +120,27 @@ class Streamer:
|
||||||
|
|
||||||
def sync(self):
|
def sync(self):
|
||||||
""" Sync all sources. """
|
""" Sync all sources. """
|
||||||
|
if self.process is None:
|
||||||
|
return
|
||||||
|
|
||||||
for source in self.sources:
|
for source in self.sources:
|
||||||
source.sync()
|
source.sync()
|
||||||
|
|
||||||
def fetch(self):
|
def fetch(self):
|
||||||
""" Fetch data from liquidsoap """
|
""" Fetch data from liquidsoap """
|
||||||
|
if self.process is None:
|
||||||
|
return
|
||||||
|
|
||||||
for source in self.sources:
|
for source in self.sources:
|
||||||
source.fetch()
|
source.fetch()
|
||||||
|
|
||||||
rid = self.send('request.on_air').split(' ')
|
# request.on_air is not ordered: we need to do it manually
|
||||||
if rid:
|
if self.dealer.is_playing:
|
||||||
rid = rid[-1]
|
self.source = self.dealer
|
||||||
# data = self._send('request.metadata ', rid, parse=True)
|
return
|
||||||
# if not data:
|
|
||||||
# return
|
|
||||||
pred = lambda s: s.rid == rid
|
|
||||||
else:
|
|
||||||
pred = lambda s: s.is_playing
|
|
||||||
|
|
||||||
self.source = next((source for source in self.sources if pred(source)),
|
self.source = next((source for source in self.sources
|
||||||
self.source)
|
if source.is_playing), None)
|
||||||
|
|
||||||
# Process ##########################################################
|
# Process ##########################################################
|
||||||
def get_process_args(self):
|
def get_process_args(self):
|
||||||
|
@ -152,10 +175,8 @@ class Streamer:
|
||||||
|
|
||||||
def kill_process(self):
|
def kill_process(self):
|
||||||
if self.process:
|
if self.process:
|
||||||
logger.info("kill process {pid}: {info}".format(
|
logger.debug("kill process %s: %s", self.process.pid,
|
||||||
pid=self.process.pid,
|
' '.join(self.get_process_args()))
|
||||||
info=' '.join(self.get_process_args())
|
|
||||||
))
|
|
||||||
self.process.kill()
|
self.process.kill()
|
||||||
self.process = None
|
self.process = None
|
||||||
|
|
||||||
|
@ -170,12 +191,19 @@ class Streamer:
|
||||||
|
|
||||||
class Source:
|
class Source:
|
||||||
controller = None
|
controller = None
|
||||||
|
""" parent controller """
|
||||||
id = None
|
id = None
|
||||||
|
""" source id """
|
||||||
uri = ''
|
uri = ''
|
||||||
|
""" source uri """
|
||||||
rid = None
|
rid = None
|
||||||
|
""" request id """
|
||||||
air_time = None
|
air_time = None
|
||||||
|
""" on air time """
|
||||||
status = None
|
status = None
|
||||||
|
""" source status """
|
||||||
|
remaining = 0.0
|
||||||
|
""" remaining time """
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def station(self):
|
def station(self):
|
||||||
|
@ -185,6 +213,10 @@ class Source:
|
||||||
def is_playing(self):
|
def is_playing(self):
|
||||||
return self.status == 'playing'
|
return self.status == 'playing'
|
||||||
|
|
||||||
|
#@property
|
||||||
|
#def is_on_air(self):
|
||||||
|
# return self.rid is not None and self.rid in self.controller.on_air
|
||||||
|
|
||||||
def __init__(self, controller, id=None):
|
def __init__(self, controller, id=None):
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.id = id
|
self.id = id
|
||||||
|
@ -194,14 +226,17 @@ class Source:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def fetch(self):
|
def fetch(self):
|
||||||
|
data = self.controller.send(self.id, '.remaining')
|
||||||
|
self.remaining = float(data)
|
||||||
|
|
||||||
data = self.controller.send(self.id, '.get', parse=True)
|
data = self.controller.send(self.id, '.get', parse=True)
|
||||||
self.on_metadata(data if data and isinstance(data, dict) else {})
|
self.on_metadata(data if data and isinstance(data, dict) else {})
|
||||||
|
|
||||||
def on_metadata(self, data):
|
def on_metadata(self, data):
|
||||||
""" Update source info from provided request metadata """
|
""" Update source info from provided request metadata """
|
||||||
self.rid = data.get('rid')
|
self.rid = data.get('rid') or None
|
||||||
self.uri = data.get('initial_uri')
|
self.uri = data.get('initial_uri') or None
|
||||||
self.status = data.get('status')
|
self.status = data.get('status') or None
|
||||||
|
|
||||||
air_time = data.get('on_air')
|
air_time = data.get('on_air')
|
||||||
if air_time:
|
if air_time:
|
||||||
|
@ -277,8 +312,17 @@ class PlaylistSource(Source):
|
||||||
|
|
||||||
|
|
||||||
class QueueSource(Source):
|
class QueueSource(Source):
|
||||||
def queue(self, *paths):
|
queue = None
|
||||||
|
""" Source's queue (excluded on_air request) """
|
||||||
|
|
||||||
|
def append(self, *paths):
|
||||||
""" Add the provided paths to source's play queue """
|
""" Add the provided paths to source's play queue """
|
||||||
for path in paths:
|
for path in paths:
|
||||||
print(self.controller.send(self.id, '_queue.push ', path))
|
self.controller.send(self.id, '_queue.push ', path)
|
||||||
|
|
||||||
|
def fetch(self):
|
||||||
|
super().fetch()
|
||||||
|
queue = self.controller.send(self.id, '_queue.queue').split(' ')
|
||||||
|
self.queue = queue
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ from django.utils import timezone as tz
|
||||||
import aircox.settings as settings
|
import aircox.settings as settings
|
||||||
from aircox.models import Log, Station
|
from aircox.models import Log, Station
|
||||||
|
|
||||||
logger = logging.getLogger('aircox.tools')
|
logger = logging.getLogger('aircox.commands')
|
||||||
|
|
||||||
|
|
||||||
class Command (BaseCommand):
|
class Command (BaseCommand):
|
||||||
|
|
|
@ -25,7 +25,7 @@ from django.utils import timezone as tz
|
||||||
|
|
||||||
from aircox.models import Schedule, Diffusion
|
from aircox.models import Schedule, Diffusion
|
||||||
|
|
||||||
logger = logging.getLogger('aircox.tools')
|
logger = logging.getLogger('aircox.commands')
|
||||||
|
|
||||||
|
|
||||||
class Actions:
|
class Actions:
|
||||||
|
|
|
@ -23,7 +23,7 @@ from aircox.models import *
|
||||||
|
|
||||||
__doc__ = __doc__.format(settings=settings)
|
__doc__ = __doc__.format(settings=settings)
|
||||||
|
|
||||||
logger = logging.getLogger('aircox.tools')
|
logger = logging.getLogger('aircox.commands')
|
||||||
|
|
||||||
|
|
||||||
class PlaylistImport:
|
class PlaylistImport:
|
||||||
|
|
|
@ -42,7 +42,7 @@ from aircox import settings, utils
|
||||||
from aircox.models import Diffusion, Program, Sound
|
from aircox.models import Diffusion, Program, Sound
|
||||||
from .import_playlist import PlaylistImport
|
from .import_playlist import PlaylistImport
|
||||||
|
|
||||||
logger = logging.getLogger('aircox.tools')
|
logger = logging.getLogger('aircox.commands')
|
||||||
|
|
||||||
|
|
||||||
sound_path_re = re.compile(
|
sound_path_re = re.compile(
|
||||||
|
|
|
@ -9,7 +9,7 @@ from argparse import RawTextHelpFormatter
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
|
||||||
logger = logging.getLogger('aircox.tools')
|
logger = logging.getLogger('aircox.commands')
|
||||||
|
|
||||||
|
|
||||||
class Stats:
|
class Stats:
|
||||||
|
|
|
@ -6,27 +6,31 @@ used to:
|
||||||
- cancels Diffusions that have an archive but could not have been played;
|
- cancels Diffusions that have an archive but could not have been played;
|
||||||
- run Liquidsoap
|
- run Liquidsoap
|
||||||
"""
|
"""
|
||||||
|
# TODO:
|
||||||
|
# x controllers: remaining
|
||||||
|
# x diffusion conflicts
|
||||||
|
# x cancel
|
||||||
|
# x when liquidsoap fails to start/exists: exit
|
||||||
|
# - handle restart after failure
|
||||||
|
# - file in queue without sound not logged?
|
||||||
|
# - is stream restart after live ok?
|
||||||
from argparse import RawTextHelpFormatter
|
from argparse import RawTextHelpFormatter
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
import tzlocal
|
from django.db.models import Q
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.utils import timezone as tz
|
from django.utils import timezone as tz
|
||||||
|
|
||||||
from aircox.models import Station, Episode, Diffusion, Track, Sound, Log
|
|
||||||
from aircox.controllers import Streamer, PlaylistSource
|
from aircox.controllers import Streamer, PlaylistSource
|
||||||
|
from aircox.models import Station, Episode, Diffusion, Track, Sound, Log
|
||||||
|
from aircox.utils import date_range
|
||||||
|
|
||||||
|
|
||||||
# force using UTC
|
# force using UTC
|
||||||
tz.activate(pytz.UTC)
|
tz.activate(pytz.UTC)
|
||||||
|
|
||||||
|
|
||||||
# FIXME liquidsoap does not manage timezones -- we have to convert
|
|
||||||
# 'on_air' metadata we get from it into utc one in order to work
|
|
||||||
# correctly.
|
|
||||||
local_tz = tzlocal.get_localzone()
|
|
||||||
|
|
||||||
|
|
||||||
class Monitor:
|
class Monitor:
|
||||||
"""
|
"""
|
||||||
Log and launch diffusions for the given station.
|
Log and launch diffusions for the given station.
|
||||||
|
@ -42,6 +46,8 @@ class Monitor:
|
||||||
"""
|
"""
|
||||||
streamer = None
|
streamer = None
|
||||||
""" Streamer controller """
|
""" Streamer controller """
|
||||||
|
delay = None
|
||||||
|
""" Timedelta: minimal delay between two call of monitor. """
|
||||||
logs = None
|
logs = None
|
||||||
""" Queryset to station's logs (ordered by -pk) """
|
""" Queryset to station's logs (ordered by -pk) """
|
||||||
cancel_timeout = 20
|
cancel_timeout = 20
|
||||||
|
@ -65,8 +71,11 @@ class Monitor:
|
||||||
""" Log of last triggered item (sound or diffusion). """
|
""" Log of last triggered item (sound or diffusion). """
|
||||||
return self.logs.start().with_diff().first()
|
return self.logs.start().with_diff().first()
|
||||||
|
|
||||||
def __init__(self, streamer, **kwargs):
|
def __init__(self, streamer, delay, cancel_timeout, **kwargs):
|
||||||
self.streamer = streamer
|
self.streamer = streamer
|
||||||
|
# adding time ensure all calculation have a margin
|
||||||
|
self.delay = delay + tz.timedelta(seconds=5)
|
||||||
|
self.cancel_timeout = cancel_timeout
|
||||||
self.__dict__.update(kwargs)
|
self.__dict__.update(kwargs)
|
||||||
self.logs = self.get_logs_queryset()
|
self.logs = self.get_logs_queryset()
|
||||||
|
|
||||||
|
@ -81,6 +90,27 @@ class Monitor:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.streamer.fetch()
|
self.streamer.fetch()
|
||||||
|
|
||||||
|
# Skip tracing - analyzis:
|
||||||
|
# Reason: multiple database request every x seconds, reducing it.
|
||||||
|
# We could skip this part when remaining time is higher than a minimal
|
||||||
|
# value (which should be derived from Command's delay). Problems:
|
||||||
|
# - How to trace tracks? (+ Source can change: caching log might sucks)
|
||||||
|
# - if liquidsoap's source/request changes: remaining time goes higher,
|
||||||
|
# thus avoiding fetch
|
||||||
|
#
|
||||||
|
# Approach: something like having a mean time, such as:
|
||||||
|
#
|
||||||
|
# ```
|
||||||
|
# source = stream.source
|
||||||
|
# mean_time = source.air_time
|
||||||
|
# + min(next_track.timestamp, source.remaining)
|
||||||
|
# - (command.delay + 1)
|
||||||
|
# trace_required = \/ source' != source
|
||||||
|
# \/ source.uri' != source.uri
|
||||||
|
# \/ now < mean_time
|
||||||
|
# ```
|
||||||
|
#
|
||||||
source = self.streamer.source
|
source = self.streamer.source
|
||||||
if source and source.uri:
|
if source and source.uri:
|
||||||
log = self.trace_sound(source)
|
log = self.trace_sound(source)
|
||||||
|
@ -102,21 +132,22 @@ class Monitor:
|
||||||
|
|
||||||
def trace_sound(self, source):
|
def trace_sound(self, source):
|
||||||
""" Return on air sound log (create if not present). """
|
""" Return on air sound log (create if not present). """
|
||||||
sound_path, air_time = source.uri, source.air_time
|
air_uri, air_time = source.uri, source.air_time
|
||||||
|
|
||||||
# check if there is yet a log for this sound on the source
|
# check if there is yet a log for this sound on the source
|
||||||
delta = tz.timedelta(seconds=5)
|
log = self.logs.on_air().filter(
|
||||||
air_times = (air_time - delta, air_time + delta)
|
Q(sound__path=air_uri) |
|
||||||
|
# sound can be null when arbitrary sound file is played
|
||||||
log = self.logs.on_air().filter(source=source.id,
|
Q(sound__isnull=True, track__isnull=True, comment=air_uri),
|
||||||
sound__path=sound_path,
|
source=source.id,
|
||||||
date__range=air_times).first()
|
date__range=date_range(air_time, self.delay),
|
||||||
|
).first()
|
||||||
if log:
|
if log:
|
||||||
return log
|
return log
|
||||||
|
|
||||||
# get sound
|
# get sound
|
||||||
diff = None
|
diff = None
|
||||||
sound = Sound.objects.filter(path=sound_path).first()
|
sound = Sound.objects.filter(path=air_uri).first()
|
||||||
if sound and sound.episode_id is not None:
|
if sound and sound.episode_id is not None:
|
||||||
diff = Diffusion.objects.episode(id=sound.episode_id).on_air() \
|
diff = Diffusion.objects.episode(id=sound.episode_id).on_air() \
|
||||||
.now(air_time).first()
|
.now(air_time).first()
|
||||||
|
@ -124,7 +155,7 @@ class Monitor:
|
||||||
# log sound on air
|
# log sound on air
|
||||||
return self.log(type=Log.Type.on_air, date=source.air_time,
|
return self.log(type=Log.Type.on_air, date=source.air_time,
|
||||||
source=source.id, sound=sound, diffusion=diff,
|
source=source.id, sound=sound, diffusion=diff,
|
||||||
comment=sound_path)
|
comment=air_uri)
|
||||||
|
|
||||||
def trace_tracks(self, log):
|
def trace_tracks(self, log):
|
||||||
"""
|
"""
|
||||||
|
@ -155,19 +186,56 @@ class Monitor:
|
||||||
Handle scheduled diffusion, trigger if needed, preload playlists
|
Handle scheduled diffusion, trigger if needed, preload playlists
|
||||||
and so on.
|
and so on.
|
||||||
"""
|
"""
|
||||||
# TODO: restart
|
# TODO: program restart
|
||||||
# TODO: handle conflict + cancel
|
|
||||||
diff = Diffusion.objects.station(self.station).on_air().now() \
|
# Diffusion conflicts are handled by the way a diffusion is defined
|
||||||
|
# as candidate for the next dealer's start.
|
||||||
|
#
|
||||||
|
# ```
|
||||||
|
# logged_diff: /\ \A diff in diffs: \E log: /\ log.type = START
|
||||||
|
# /\ log.diff = diff
|
||||||
|
# /\ log.date = diff.start
|
||||||
|
# queue_empty: /\ dealer.queue is empty
|
||||||
|
# /\ \/ ~dealer.on_air
|
||||||
|
# \/ dealer.remaining < delay
|
||||||
|
#
|
||||||
|
# start_allowed: /\ diff not in logged_diff
|
||||||
|
# /\ queue_empty
|
||||||
|
#
|
||||||
|
# start_canceled: /\ diff not in logged diff
|
||||||
|
# /\ ~queue_empty
|
||||||
|
# /\ diff.start < now + cancel_timeout
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
now = tz.now()
|
||||||
|
diff = Diffusion.objects.station(self.station).on_air().now(now) \
|
||||||
.filter(episode__sound__type=Sound.Type.archive) \
|
.filter(episode__sound__type=Sound.Type.archive) \
|
||||||
.first()
|
.first()
|
||||||
log = self.logs.start().filter(diffusion=diff) if diff else None
|
# Can't use delay: diffusion may start later than its assigned start.
|
||||||
|
log = None if not diff else self.logs.start().filter(diffusion=diff)
|
||||||
if not diff or log:
|
if not diff or log:
|
||||||
return
|
return
|
||||||
|
|
||||||
playlist = Sound.objects.episode(id=diff.episode_id).paths()
|
|
||||||
dealer = self.streamer.dealer
|
dealer = self.streamer.dealer
|
||||||
dealer.queue(*playlist)
|
# start
|
||||||
self.log(type=Log.Type.start, source=dealer.id, diffusion=diff,
|
if not dealer.queue and dealer.rid is None or \
|
||||||
|
dealer.remaining < self.delay.total_seconds():
|
||||||
|
self.start_diff(dealer, diff)
|
||||||
|
|
||||||
|
# cancel
|
||||||
|
if diff.start < now - self.cancel_timeout:
|
||||||
|
self.cancel_diff(dealer, diff)
|
||||||
|
|
||||||
|
def start_diff(self, source, diff):
|
||||||
|
playlist = Sound.objects.episode(id=diff.episode_id).paths()
|
||||||
|
source.append(*playlist)
|
||||||
|
self.log(type=Log.Type.start, source=source.id, diffusion=diff,
|
||||||
|
comment=str(diff))
|
||||||
|
|
||||||
|
def cancel_diff(self, source, diff):
|
||||||
|
diff.type = Diffusion.Type.cancel
|
||||||
|
diff.save()
|
||||||
|
self.log(type=Log.Type.cancel, source=source.id, diffusion=diff,
|
||||||
comment=str(diff))
|
comment=str(diff))
|
||||||
|
|
||||||
def sync(self):
|
def sync(self):
|
||||||
|
@ -207,7 +275,8 @@ class Command (BaseCommand):
|
||||||
'-d', '--delay', type=int,
|
'-d', '--delay', type=int,
|
||||||
default=1000,
|
default=1000,
|
||||||
help='time to sleep in MILLISECONDS between two updates when we '
|
help='time to sleep in MILLISECONDS between two updates when we '
|
||||||
'monitor'
|
'monitor. This influence the delay before a diffusion is '
|
||||||
|
'launched.'
|
||||||
)
|
)
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
'-s', '--station', type=str, action='append',
|
'-s', '--station', type=str, action='append',
|
||||||
|
@ -215,7 +284,7 @@ class Command (BaseCommand):
|
||||||
'all stations'
|
'all stations'
|
||||||
)
|
)
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
'-t', '--timeout', type=int,
|
'-t', '--timeout', type=float,
|
||||||
default=Monitor.cancel_timeout,
|
default=Monitor.cancel_timeout,
|
||||||
help='time to wait in MINUTES before canceling a diffusion that '
|
help='time to wait in MINUTES before canceling a diffusion that '
|
||||||
'should have ran but did not. '
|
'should have ran but did not. '
|
||||||
|
@ -226,7 +295,6 @@ class Command (BaseCommand):
|
||||||
config=None, run=None, monitor=None,
|
config=None, run=None, monitor=None,
|
||||||
station=[], delay=1000, timeout=600,
|
station=[], delay=1000, timeout=600,
|
||||||
**options):
|
**options):
|
||||||
|
|
||||||
stations = Station.objects.filter(name__in=station) if station else \
|
stations = Station.objects.filter(name__in=station) if station else \
|
||||||
Station.objects.all()
|
Station.objects.all()
|
||||||
streamers = [Streamer(station) for station in stations]
|
streamers = [Streamer(station) for station in stations]
|
||||||
|
@ -238,14 +306,15 @@ class Command (BaseCommand):
|
||||||
streamer.run_process()
|
streamer.run_process()
|
||||||
|
|
||||||
if monitor:
|
if monitor:
|
||||||
monitors = [Monitor(streamer, cancel_timeout=timeout)
|
delay = tz.timedelta(milliseconds=delay)
|
||||||
|
timeout = tz.timedelta(minutes=timeout)
|
||||||
|
monitors = [Monitor(streamer, delay, timeout)
|
||||||
for streamer in streamers]
|
for streamer in streamers]
|
||||||
|
|
||||||
delay = delay / 1000
|
while not run or streamer.is_running:
|
||||||
while True:
|
|
||||||
for monitor in monitors:
|
for monitor in monitors:
|
||||||
monitor.monitor()
|
monitor.monitor()
|
||||||
time.sleep(delay)
|
time.sleep(delay.total_seconds())
|
||||||
|
|
||||||
if run:
|
if run:
|
||||||
for streamer in streamers:
|
for streamer in streamers:
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -122,7 +122,7 @@ class Diffusion(BaseRerun):
|
||||||
class Type(IntEnum):
|
class Type(IntEnum):
|
||||||
on_air = 0x00
|
on_air = 0x00
|
||||||
unconfirmed = 0x01
|
unconfirmed = 0x01
|
||||||
canceled = 0x02
|
cancel = 0x02
|
||||||
|
|
||||||
episode = models.ForeignKey(
|
episode = models.ForeignKey(
|
||||||
Episode, models.CASCADE,
|
Episode, models.CASCADE,
|
||||||
|
|
|
@ -167,18 +167,14 @@ class Log(models.Model):
|
||||||
"""
|
"""
|
||||||
Source has been stopped, e.g. manually
|
Source has been stopped, e.g. manually
|
||||||
"""
|
"""
|
||||||
|
# Rule: \/ diffusion != null \/ sound != null
|
||||||
start = 0x01
|
start = 0x01
|
||||||
"""
|
""" Diffusion or sound has been request to be played. """
|
||||||
The diffusion or sound has been triggered by the streamer or
|
cancel = 0x02
|
||||||
manually.
|
""" Diffusion has been canceled. """
|
||||||
"""
|
# Rule: \/ sound != null /\ track == null
|
||||||
load = 0x02
|
# \/ sound == null /\ track != null
|
||||||
"""
|
# \/ sound == null /\ track == null /\ comment = sound_path
|
||||||
A playlist has updated, and loading started. A related Diffusion
|
|
||||||
does not means that the playlist is only for it (e.g. after a
|
|
||||||
crash, it can reload previous remaining sound files + thoses of
|
|
||||||
the next diffusion)
|
|
||||||
"""
|
|
||||||
on_air = 0x03
|
on_air = 0x03
|
||||||
"""
|
"""
|
||||||
The sound or diffusion has been detected occurring on air. Can
|
The sound or diffusion has been detected occurring on air. Can
|
||||||
|
@ -186,9 +182,7 @@ class Log(models.Model):
|
||||||
them since they don't have an attached sound archive.
|
them since they don't have an attached sound archive.
|
||||||
"""
|
"""
|
||||||
other = 0x04
|
other = 0x04
|
||||||
"""
|
""" Other log """
|
||||||
Other log
|
|
||||||
"""
|
|
||||||
|
|
||||||
station = models.ForeignKey(
|
station = models.ForeignKey(
|
||||||
Station, models.CASCADE,
|
Station, models.CASCADE,
|
||||||
|
@ -216,12 +210,6 @@ class Log(models.Model):
|
||||||
max_length=512, blank=True, null=True,
|
max_length=512, blank=True, null=True,
|
||||||
verbose_name=_('comment'),
|
verbose_name=_('comment'),
|
||||||
)
|
)
|
||||||
|
|
||||||
diffusion = models.ForeignKey(
|
|
||||||
Diffusion, models.SET_NULL,
|
|
||||||
blank=True, null=True, db_index=True,
|
|
||||||
verbose_name=_('Diffusion'),
|
|
||||||
)
|
|
||||||
sound = models.ForeignKey(
|
sound = models.ForeignKey(
|
||||||
Sound, models.SET_NULL,
|
Sound, models.SET_NULL,
|
||||||
blank=True, null=True, db_index=True,
|
blank=True, null=True, db_index=True,
|
||||||
|
@ -232,6 +220,11 @@ class Log(models.Model):
|
||||||
blank=True, null=True, db_index=True,
|
blank=True, null=True, db_index=True,
|
||||||
verbose_name=_('Track'),
|
verbose_name=_('Track'),
|
||||||
)
|
)
|
||||||
|
diffusion = models.ForeignKey(
|
||||||
|
Diffusion, models.SET_NULL,
|
||||||
|
blank=True, null=True, db_index=True,
|
||||||
|
verbose_name=_('Diffusion'),
|
||||||
|
)
|
||||||
|
|
||||||
objects = LogQuerySet.as_manager()
|
objects = LogQuerySet.as_manager()
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,11 @@ def interactive (id, s) =
|
||||||
description="Seek to a relative position",
|
description="Seek to a relative position",
|
||||||
usage="seek <duration>",
|
usage="seek <duration>",
|
||||||
"seek", fun (x) -> begin seek(s, x) end)
|
"seek", fun (x) -> begin seek(s, x) end)
|
||||||
|
server.register(namespace=id,
|
||||||
|
description="Get source's track remaining time",
|
||||||
|
usage="remaining",
|
||||||
|
"remaining", fun (_) -> begin json_of(source.remaining(s)) end)
|
||||||
|
|
||||||
s = store_metadata(id=id, size=1, s)
|
s = store_metadata(id=id, size=1, s)
|
||||||
add_skip_command(s)
|
add_skip_command(s)
|
||||||
s
|
s
|
||||||
|
|
|
@ -2,16 +2,18 @@ import datetime
|
||||||
import django.utils.timezone as tz
|
import django.utils.timezone as tz
|
||||||
|
|
||||||
|
|
||||||
def date_range(date):
|
def date_range(date, delta=None, **delta_kwargs):
|
||||||
"""
|
"""
|
||||||
|
Return a range of provided date such as `[date-delta, date+delta]`.
|
||||||
|
:param date: the reference date
|
||||||
|
:param delta: timedelta
|
||||||
|
:param \**delta_kwargs: timedelta init arguments
|
||||||
|
|
||||||
Return a datetime range for a given day, as:
|
Return a datetime range for a given day, as:
|
||||||
```(date, 0:0:0:0; date, 23:59:59:999)```.
|
```(date, 0:0:0:0; date, 23:59:59:999)```.
|
||||||
"""
|
"""
|
||||||
date = date_or_default(date, tz.datetime)
|
delta = tz.timedelta(**delta_kwargs) if delta is None else delta
|
||||||
return (
|
return [date - delta, date + delta]
|
||||||
date.replace(hour=0, minute=0, second=0),
|
|
||||||
date.replace(hour=23, minute=59, second=59, microsecond=999)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def cast_date(date, into=datetime.date):
|
def cast_date(date, into=datetime.date):
|
||||||
|
|
6
setup.py
6
setup.py
|
@ -15,12 +15,12 @@ def to_array (path):
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='aircox',
|
name='aircox',
|
||||||
version='0.1',
|
version='0.9',
|
||||||
license='GPLv3',
|
license='GPLv3',
|
||||||
author='bkfox',
|
author='bkfox',
|
||||||
description='Aircox is a radio programs manager that includes tools and cms',
|
description='Aircox is a radio programs manager including tools and cms',
|
||||||
long_description=to_rst('README.md'),
|
long_description=to_rst('README.md'),
|
||||||
url='http://bkfox.net/',
|
url='https://github.com/bkfox/aircox',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=to_array('requirements.txt'),
|
install_requires=to_array('requirements.txt'),
|
||||||
|
|
Loading…
Reference in New Issue
Block a user