implement pulls
This commit is contained in:
parent
813780a18b
commit
d2f980c318
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"venvPath": "source",
|
||||
"venv": ".venv"
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
from django.dispatch import receiver
|
||||
from .models import Task
|
||||
from config.signals import stream_inactive
|
||||
from config.signals_shared import stream_inactive
|
||||
from config.models import Stream
|
||||
|
||||
|
||||
@receiver(stream_inactive)
|
||||
def delete_tasks(sender, **kwargs):
|
||||
# when a stream was unpublished, all related tasks need to be deleted.
|
||||
def delete_tasks_when_stream_inactive(sender, **kwargs):
|
||||
# when a stream has become inactive, all related tasks which are created for the stream need to be deleted.
|
||||
# we need to exclude the pull tasks, as they are not created for the stream.
|
||||
stream = Stream.objects.get(stream=kwargs['stream'])
|
||||
Task.objects.filter(stream=stream).delete()
|
||||
Task.objects.filter(stream=stream).exclude(type="pull").delete()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.contrib import admin
|
||||
from guardian.admin import GuardedModelAdmin
|
||||
from config.models import Stream, Restream, SRSNode, SRSStreamInstance
|
||||
from config.models import Stream, Restream, Pull, SRSNode, SRSStreamInstance
|
||||
|
||||
|
||||
@admin.register(Stream)
|
||||
|
@ -11,6 +11,10 @@ class StreamAdmin(GuardedModelAdmin):
|
|||
class RestreamAdmin(GuardedModelAdmin):
|
||||
fields = ['name', 'active', 'stream', 'format', 'target']
|
||||
|
||||
@admin.register(Pull)
|
||||
class PullAdmin(GuardedModelAdmin):
|
||||
fields = ['name', 'active', 'stream', 'source']
|
||||
|
||||
@admin.register(SRSNode)
|
||||
class SRSNodeAdmin(GuardedModelAdmin):
|
||||
fields = ['name', 'api_base', 'rtmp_base', 'active']
|
||||
|
@ -24,4 +28,4 @@ class SRSStreamInstanceAdmin(GuardedModelAdmin):
|
|||
def has_change_permission(self, request, obj=None):
|
||||
return False
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
return False
|
||||
|
|
|
@ -22,6 +22,20 @@ class RestreamPatch(ModelSchema):
|
|||
extra = "forbid"
|
||||
|
||||
|
||||
class Pull(ModelSchema):
|
||||
class Meta:
|
||||
model = models.Pull
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class PullPatch(ModelSchema):
|
||||
class Meta:
|
||||
model = models.Pull
|
||||
exclude = ["id"]
|
||||
fields_optional = "__all__"
|
||||
extra = "forbid"
|
||||
|
||||
|
||||
class Stream(ModelSchema):
|
||||
class Meta:
|
||||
model = models.Stream
|
||||
|
@ -87,6 +101,7 @@ def delete_stream(request, id: int):
|
|||
|
||||
stream.delete()
|
||||
|
||||
####################################################################################################
|
||||
|
||||
@router.get('/restreams', response=List[Restream])
|
||||
def list_restreams(request):
|
||||
|
@ -141,4 +156,61 @@ def delete_restream(request, id: int):
|
|||
if not request.user.has_perm('delete_restream', restream):
|
||||
raise HttpError(401, "unauthorized")
|
||||
|
||||
restream.delete()
|
||||
restream.delete()
|
||||
|
||||
####################################################################################################
|
||||
|
||||
@router.get('/pulls', response=List[Pull])
|
||||
def list_pulls(request):
|
||||
return get_objects_for_user(request.user, 'view_pull', models.Pull.objects.all())
|
||||
|
||||
|
||||
@router.post('/pulls', response=Pull)
|
||||
def create_pull(request, payload: Pull):
|
||||
if not request.user.has_perm('view_stream', payload.stream):
|
||||
raise HttpError(401, "unauthorized")
|
||||
|
||||
pull = models.Pull.objects.create(**payload.dict())
|
||||
assign_perm( 'view_pull', request.user, Pull)
|
||||
assign_perm('change_pull', request.user, Pull)
|
||||
assign_perm('delete_pull', request.user, Pull)
|
||||
return pull
|
||||
|
||||
|
||||
@router.get('/pulls/{id}', response=Pull)
|
||||
def get_pull(request, id: int):
|
||||
pull = get_object_or_404(models.Pull, id=id)
|
||||
|
||||
if not request.user.has_perm('view_pull', pull):
|
||||
raise HttpError(401, "unauthorized")
|
||||
|
||||
return pull
|
||||
|
||||
|
||||
@router.patch('/pulls/{id}', response=Pull)
|
||||
def patch_pull(request, id: int, payload: PullPatch):
|
||||
pull = get_object_or_404(models.Pull, id=id)
|
||||
|
||||
if not request.user.has_perm('change_pull', pull):
|
||||
raise HttpError(401, "unauthorized")
|
||||
|
||||
if payload.stream:
|
||||
payload.stream = get_object_or_404(models.Stream, id=payload.stream)
|
||||
|
||||
if not request.user.has_perm('view_stream', payload.stream):
|
||||
raise HttpError(401, "unauthorized")
|
||||
|
||||
for key, value in payload.dict(exclude_unset=True).items():
|
||||
setattr(pull, key, value)
|
||||
pull.save()
|
||||
return pull
|
||||
|
||||
|
||||
@router.delete('/pulls/{id}', response=None)
|
||||
def delete_pull(request, id: int):
|
||||
pull = get_object_or_404(models.Pull, id=id)
|
||||
|
||||
if not request.user.has_perm('delete_pull', pull):
|
||||
raise HttpError(401, "unauthorized")
|
||||
|
||||
pull.delete()
|
|
@ -6,4 +6,4 @@ class ConfigConfig(AppConfig):
|
|||
name = 'config'
|
||||
|
||||
def ready(self):
|
||||
import config.signals, config.signals_shared # noqa
|
||||
import config.signals.pull, config.signals.restream, config.signals_shared # noqa
|
||||
|
|
|
@ -14,3 +14,15 @@ class RestreamFilteredStreamForm(ModelForm):
|
|||
|
||||
# limit the stream selection to user-accessible streams
|
||||
self.fields['stream'].queryset = get_objects_for_user(user, 'config.view_stream')
|
||||
|
||||
class PullFilteredStreamForm(ModelForm):
|
||||
class Meta:
|
||||
model = models.Pull
|
||||
fields = ['name', 'stream', 'source', 'active']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user = kwargs.pop('user', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# limit the stream selection to user-accessible streams
|
||||
self.fields['stream'].queryset = get_objects_for_user(user, 'config.view_stream')
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 5.0.2 on 2024-04-01 08:08
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0002_srsnode_srsstreaminstance'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Pull',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('source', models.CharField(help_text='pull_source_help', max_length=500)),
|
||||
('active', models.BooleanField(help_text='pull_activate_help')),
|
||||
('name', models.CharField(help_text='pull_name_help', max_length=100)),
|
||||
('stream', models.ForeignKey(help_text='pull_stream_help', on_delete=django.db.models.deletion.CASCADE, to='config.stream')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'pull_verbose_name',
|
||||
'verbose_name_plural': 'pull_verbose_name_plural',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 5.0.2 on 2024-04-01 10:42
|
||||
|
||||
import config.util
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('config', '0003_pull'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='pull',
|
||||
name='source',
|
||||
field=models.CharField(help_text='pull_source_help', max_length=500, validators=[config.util.validate_stream_url]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='restream',
|
||||
name='target',
|
||||
field=models.CharField(help_text='restream_target_help', max_length=500, validators=[config.util.validate_stream_url]),
|
||||
),
|
||||
]
|
|
@ -8,6 +8,7 @@ from django.utils.translation import gettext as _
|
|||
from django.db.models.signals import pre_delete
|
||||
from portier.common import handlers
|
||||
from config import signals_shared
|
||||
from config.util import validate_stream_url
|
||||
|
||||
|
||||
class Stream(models.Model):
|
||||
|
@ -41,7 +42,7 @@ class Stream(models.Model):
|
|||
return _('stream_class_name')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
return str(self.name)
|
||||
|
||||
|
||||
pre_delete.connect(handlers.remove_obj_perms_connected_with_user, sender=Stream)
|
||||
|
@ -75,13 +76,64 @@ class SRSStreamInstance(models.Model):
|
|||
return f"{self.stream} on {self.node}"
|
||||
|
||||
|
||||
class Pull(models.Model):
|
||||
stream = models.ForeignKey(Stream, on_delete=models.CASCADE, help_text=_('pull_stream_help'))
|
||||
source = models.CharField(max_length=500, validators=[validate_stream_url], help_text=_('pull_source_help'))
|
||||
active = models.BooleanField(help_text=_('pull_activate_help'))
|
||||
name = models.CharField(max_length=100, help_text=_('pull_name_help'))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('pull_verbose_name')
|
||||
verbose_name_plural = _('pull_verbose_name_plural')
|
||||
|
||||
def class_name(self):
|
||||
return _('pull_class_name')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('config:pull_detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if self.active:
|
||||
signals_shared.pull_active.send(
|
||||
sender=self.__class__,
|
||||
pull_id=self.id,
|
||||
)
|
||||
else:
|
||||
signals_shared.pull_inactive.send(
|
||||
sender=self.__class__,
|
||||
pull_id=self.id,
|
||||
)
|
||||
|
||||
def get_concierge_configuration(self):
|
||||
if not self.active:
|
||||
return None
|
||||
|
||||
# Select a random node that is active to ingest the pulled stream.
|
||||
node = SRSNode.objects.filter(active=True).order_by('?').first()
|
||||
|
||||
return {
|
||||
"type": "pull",
|
||||
"active": self.active,
|
||||
"name": self.name,
|
||||
"source": self.source,
|
||||
"target": f"{node.rtmp_base}/{settings.GLOBAL_STREAM_NAMESPACE}/{self.stream.stream}",
|
||||
}
|
||||
|
||||
|
||||
pre_delete.connect(handlers.remove_obj_perms_connected_with_user, sender=Pull)
|
||||
|
||||
|
||||
class Restream(models.Model):
|
||||
FORMATS = (
|
||||
('flv', 'flv (RTMP)'),
|
||||
('mpegts', 'mpegts (SRT)'),
|
||||
)
|
||||
stream = models.ForeignKey(Stream, on_delete=models.CASCADE, help_text=_('restream_stream_help'))
|
||||
target = models.CharField(max_length=500, help_text=_('restream_target_help'))
|
||||
target = models.CharField(max_length=500, validators=[validate_stream_url], help_text=_('restream_target_help'))
|
||||
name = models.CharField(max_length=100, help_text=_('restream_name_help'))
|
||||
active = models.BooleanField(help_text=_('restream_activate_help'))
|
||||
format = models.CharField(max_length=6, choices=FORMATS, default='flv', help_text=_('restream_format_help'))
|
||||
|
@ -128,4 +180,3 @@ class Restream(models.Model):
|
|||
|
||||
|
||||
pre_delete.connect(handlers.remove_obj_perms_connected_with_user, sender=Restream)
|
||||
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import json
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
|
||||
from config.models import Stream, Pull
|
||||
from config.signals_shared import stream_active, stream_inactive, pull_active, pull_inactive
|
||||
from concierge.models import Task
|
||||
|
||||
@receiver(pull_active)
|
||||
def create_pull_tasks_on_activate(sender, **kwargs):
|
||||
pull = Pull.objects.get(id=kwargs['pull_id'])
|
||||
Task.objects.get_or_create(stream=pull.stream, type='pull', config_id=pull.id,
|
||||
configuration=json.dumps(pull.get_concierge_configuration()))
|
||||
|
||||
|
||||
@receiver(pull_inactive)
|
||||
def delete_pull_tasks_on_inactivate(sender, **kwargs):
|
||||
pull = Pull.objects.get(id=kwargs['pull_id'])
|
||||
Task.objects.filter(type='pull', config_id=pull.id).delete()
|
||||
|
||||
|
||||
@receiver(post_save, sender=Pull)
|
||||
def update_pull_tasks(sender, **kwargs):
|
||||
instance = kwargs['instance']
|
||||
|
||||
try:
|
||||
task = Task.objects.filter(type='pull', config_id=instance.id).get()
|
||||
task.delete()
|
||||
except Task.DoesNotExist:
|
||||
pass
|
||||
|
||||
if instance.active:
|
||||
task = Task(stream=instance.stream, type='pull', config_id=instance.id,
|
||||
configuration=json.dumps(instance.get_concierge_configuration()))
|
||||
task.save()
|
||||
|
||||
@receiver(post_delete, sender=Pull)
|
||||
def delete_pull_tasks(sender, **kwargs):
|
||||
instance = kwargs['instance']
|
||||
# Get the current task instance if it exists, and remove it
|
||||
try:
|
||||
task = Task.objects.filter(type='pull', config_id=instance.id).get()
|
||||
task.delete()
|
||||
except Task.DoesNotExist:
|
||||
pass
|
|
@ -0,0 +1,46 @@
|
|||
import json
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
|
||||
from config.models import Restream, Stream
|
||||
from config.signals_shared import stream_active, stream_inactive
|
||||
from concierge.models import Task
|
||||
|
||||
@receiver(stream_active)
|
||||
def create_restream_tasks(sender, **kwargs):
|
||||
stream = Stream.objects.get(stream=kwargs['stream'])
|
||||
restreams = Restream.objects.filter(active=True, stream=stream)
|
||||
for restream in restreams:
|
||||
Task.objects.get_or_create(stream=restream.stream, type='restream', config_id=restream.id,
|
||||
configuration=json.dumps(restream.get_concierge_configuration()))
|
||||
|
||||
|
||||
@receiver(post_save, sender=Restream)
|
||||
def update_restream_tasks(sender, **kwargs):
|
||||
instance = kwargs['instance']
|
||||
# TODO: check for breaking changes using update_fields. This needs custom save_model functions though.
|
||||
|
||||
# Get the current task instance if it exists, and remove it
|
||||
try:
|
||||
task = Task.objects.filter(type='restream', config_id=instance.id).get()
|
||||
task.delete()
|
||||
except Task.DoesNotExist:
|
||||
pass
|
||||
|
||||
# If the configuration is set to be active, and the stream is published, (re)create new task
|
||||
if instance.active and instance.stream.publish_counter > 0:
|
||||
task = Task(stream=instance.stream, type='restream', config_id=instance.id,
|
||||
configuration=json.dumps(instance.get_concierge_configuration()))
|
||||
task.save()
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Restream)
|
||||
def delete_restream_tasks(sender, **kwargs):
|
||||
instance = kwargs['instance']
|
||||
# Get the current task instance if it exists, and remove it
|
||||
try:
|
||||
task = Task.objects.filter(type='restream', config_id=instance.id).get()
|
||||
task.delete()
|
||||
except Task.DoesNotExist:
|
||||
pass
|
|
@ -1,4 +1,6 @@
|
|||
from django.dispatch import Signal
|
||||
|
||||
stream_active = Signal()
|
||||
stream_inactive = Signal()
|
||||
stream_inactive = Signal()
|
||||
pull_active = Signal()
|
||||
pull_inactive = Signal()
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap4 %}
|
||||
{% load fontawesome_5 %}
|
||||
|
||||
{% block 'sidenav' %}
|
||||
{% with 'pull' as section %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block 'content' %}
|
||||
<h6>{% trans "confirm_delete_header" %}</h6>
|
||||
<hr class="my-4">
|
||||
<div class="row">
|
||||
<div class="col-sm border-right">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<p>{% blocktrans with restream_config_name=object.name %}are_you_sure_you_want_to_delete_"{{ pull_config_name }}"?{% endblocktrans %}</p>
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn btn-danger" value="login">
|
||||
{% fa5_icon 'trash' %} {% trans "delete" %}
|
||||
</button>
|
||||
{% endbuttons %}
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,78 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap4 %}
|
||||
{% load fontawesome_5 %}
|
||||
{% load guardian_tags %}
|
||||
|
||||
{% get_obj_perms user for object as "obj_perms" %}
|
||||
|
||||
|
||||
{% block 'sidenav' %}
|
||||
{% with 'pull' as section %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block 'content' %}
|
||||
<div class="row justify-content-between">
|
||||
<div class="col">
|
||||
<h6>{% trans "pull_configuration_details_header" %}</h6>
|
||||
</div>
|
||||
{% get_obj_perms user for object as "obj_perms" %}
|
||||
<div class="col-auto">
|
||||
{% if "change_pull" in obj_perms %}
|
||||
<a href="{% url 'config:pull_change' pk=object.pk %}" type="button" class="btn btn-sm btn-outline-primary">{% fa5_icon 'edit' %} {% trans 'change' %}</a>
|
||||
{% endif %}
|
||||
{% if "delete_pull" in obj_perms %}
|
||||
<a href="{% url 'config:pull_delete' pk=object.pk %}" type="button" class="btn btn-sm btn-outline-danger">{% fa5_icon 'trash' %} {% trans 'delete' %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-4">
|
||||
<div class="row">
|
||||
<div class="col-sm border-right">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-3">{% trans "name" %}</dt>
|
||||
<dd class="col-sm-9">{{ object.name }}</dd>
|
||||
<dt class="col-sm-3">{% trans "stream" %}</dt>
|
||||
<dd class="col-sm-9"><a href="{% url 'config:stream_detail' pk=object.stream.pk %}">{{ object.stream.name }}</a></dd>
|
||||
<dt class="col-sm-3">{% trans "active" %}</dt>
|
||||
<dd class="col-sm-9">
|
||||
{% if object.active %}
|
||||
<span class="text-success">{% fa5_icon 'check-circle' %}</span>
|
||||
{% else %}
|
||||
<span class="text-danger">{% fa5_icon 'times-circle' %}</span>
|
||||
{% endif %}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<p>{% trans "configured_source_url" %}</p>
|
||||
<div class="input-group mb-4" id="show_hide_source_url">
|
||||
<input readonly class="form-control" type="password" value="{{ object.source }}">
|
||||
<div class="input-group-append">
|
||||
<a href="" class="btn btn-outline-secondary"><i class="fa fa-eye-slash" aria-hidden="true"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$(function () {
|
||||
$('[data-toggle="popover"]').popover()
|
||||
})
|
||||
$(function() {
|
||||
$("#show_hide_source_url a").on('click', function(event) {
|
||||
event.preventDefault();
|
||||
if ($('#show_hide_source_url input').attr('type') == 'text') {
|
||||
$('#show_hide_source_url input').attr('type', 'password');
|
||||
$('#show_hide_source_url i').addClass('fa-eye-slash');
|
||||
$('#show_hide_source_url i').removeClass('fa-eye');
|
||||
} else if ($('#show_hide_source_url input').attr('type') == 'password') {
|
||||
$('#show_hide_source_url input').attr('type', 'text');
|
||||
$('#show_hide_source_url i').removeClass('fa-eye-slash');
|
||||
$('#show_hide_source_url i').addClass('fa-eye');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,31 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap4 %}
|
||||
|
||||
{% block 'sidenav' %}
|
||||
{% with 'pull' as section %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block 'content' %}
|
||||
<h6>{% trans "create_new_pull_configuration_header" %}</h6>
|
||||
<hr class="my-4">
|
||||
<div class="row">
|
||||
<div class="col-sm border-right">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn btn-primary" value="login">
|
||||
{% trans "submit" %}
|
||||
</button>
|
||||
{% endbuttons %}
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
{% trans "pull_configuration_text_html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,70 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap4 %}
|
||||
{% load fontawesome_5 %}
|
||||
|
||||
|
||||
{% block 'sidenav' %}
|
||||
{% with 'pull' as section %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block 'content' %}
|
||||
|
||||
<div class="row justify-content-between">
|
||||
<div class="col">
|
||||
<h6>{% trans "pull_configuration_header" %}</h6>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{% url 'config:pull_create' %}" type="button" class="btn btn-sm btn-outline-primary">{% fa5_icon 'plus' %} {% trans "create" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-4">
|
||||
<div id="app">
|
||||
<div v-if="isLoading">
|
||||
<div class="my-4 text-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">{% trans "loading..." %}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<table v-else class="table">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th scope="col">{% trans "name" %}</th>
|
||||
<th scope="col">{% trans "active" %}</th>
|
||||
<th scope="col" class="text-right">{% trans "actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="cfg in cfgs">
|
||||
<th scope="row">{% verbatim %}{{cfg.name}}{% endverbatim %}</th>
|
||||
<td v-if="cfg.active">
|
||||
<button v-on:click="toggleActive(cfg)" type="button" class="btn btn-outline-success">
|
||||
{% fa5_icon 'check-circle' %}
|
||||
</button>
|
||||
</td>
|
||||
<td v-else>
|
||||
<button v-on:click="toggleActive(cfg)" type="button" class="btn btn-outline-danger">
|
||||
{% fa5_icon 'times-circle' %}
|
||||
</button>
|
||||
</td>
|
||||
<td align="right">
|
||||
<a v-bind:href="detailLink(cfg.id)" type="button" class="btn btn-sm btn-outline-primary">{% trans "details" %}</a>
|
||||
<a v-bind:href="deleteLink(cfg.id)" type="button" class="btn btn-sm btn-outline-danger">{% fa5_icon 'trash' %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/vue.min.js' %}"></script>
|
||||
<script src="{% static 'js/axios.min.js' %}"></script>
|
||||
<script src="{% static 'js/pull-list.js' %}"></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,31 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap4 %}
|
||||
|
||||
{% block 'sidenav' %}
|
||||
{% with 'pull' as section %}
|
||||
{{ block.super }}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
||||
{% block 'content' %}
|
||||
<h6>{% trans "update_pull_configuration_header" %}</h6>
|
||||
<hr class="my-4">
|
||||
<div class="row">
|
||||
<div class="col-sm border-right">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn btn-primary" value="login">
|
||||
{% trans "submit" %}
|
||||
</button>
|
||||
{% endbuttons %}
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
{% trans "pull_configuration_text_html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,5 +1,5 @@
|
|||
from django.urls import path
|
||||
from config.views import restream, stream
|
||||
from config.views import restream, stream, pull
|
||||
from config.views.srs import callback_srs
|
||||
|
||||
app_name = 'config'
|
||||
|
@ -16,4 +16,9 @@ urlpatterns = [
|
|||
path('restream/<int:pk>/change', restream.RestreamUpdate.as_view(), name='restream_change'),
|
||||
path('restream/<int:pk>/delete', restream.RestreamDelete.as_view(), name='restream_delete'),
|
||||
path('restream/create', restream.RestreamCreate.as_view(), name='restream_create'),
|
||||
path('pull/', pull.PullList.as_view(), name='pull_list'),
|
||||
path('pull/<int:pk>/', pull.PullDetail.as_view(), name='pull_detail'),
|
||||
path('pull/<int:pk>/change', pull.PullUpdate.as_view(), name='pull_change'),
|
||||
path('pull/<int:pk>/delete', pull.PullDelete.as_view(), name='pull_delete'),
|
||||
path('pull/create', pull.PullCreate.as_view(), name='pull_create'),
|
||||
]
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
def validate_stream_url(value, protocols=['rtmp://', 'srt://']):
|
||||
# Make sure that the URL uses one of the allowed protocols
|
||||
if not any([value.startswith(protocol) for protocol in protocols]):
|
||||
raise ValidationError(
|
||||
_('Invalid URL: %(value)s. Must start with one of the following: %(protocols)s'),
|
||||
params={'value': value, 'protocols': ', '.join(protocols)},
|
||||
)
|
||||
|
||||
# TODO: Add more validation here
|
||||
|
||||
return value
|
|
@ -0,0 +1,76 @@
|
|||
import logging
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.views.generic import ListView, DetailView, CreateView, DeleteView, UpdateView
|
||||
from guardian.decorators import permission_required_or_403
|
||||
from guardian.shortcuts import assign_perm
|
||||
|
||||
from config import models, forms
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
@method_decorator(login_required, name='dispatch')
|
||||
@method_decorator(permission_required_or_403('config.add_pull'),
|
||||
name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
class PullList(ListView):
|
||||
model = models.Pull
|
||||
|
||||
|
||||
@method_decorator(login_required, name='dispatch')
|
||||
@method_decorator(permission_required_or_403('config.view_pull',
|
||||
(models.Pull, 'pk', 'pk')),
|
||||
name='dispatch')
|
||||
class PullDetail(DetailView):
|
||||
model = models.Pull
|
||||
|
||||
|
||||
@method_decorator(login_required, name='dispatch')
|
||||
@method_decorator(permission_required_or_403('config.change_pull',
|
||||
(models.Pull, 'pk', 'pk')),
|
||||
name='dispatch')
|
||||
class PullUpdate(UpdateView):
|
||||
model = models.Pull
|
||||
form_class = forms.PullFilteredStreamForm
|
||||
template_name_suffix = '_update_form'
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['user'] = self.request.user
|
||||
return kwargs
|
||||
|
||||
|
||||
@method_decorator(login_required, name='dispatch')
|
||||
@method_decorator(permission_required_or_403('config.add_pull'),
|
||||
name='dispatch')
|
||||
class PullCreate(CreateView):
|
||||
model = models.Pull
|
||||
form_class = forms.PullFilteredStreamForm
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['user'] = self.request.user
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form):
|
||||
valid = super().form_valid(form)
|
||||
if valid:
|
||||
user = self.request.user
|
||||
assign_perm('view_pull', user, self.object)
|
||||
assign_perm('change_pull', user, self.object)
|
||||
assign_perm('delete_pull', user, self.object)
|
||||
return valid
|
||||
|
||||
|
||||
@method_decorator(login_required, name='dispatch')
|
||||
@method_decorator(permission_required_or_403('config.delete_pull',
|
||||
(models.Pull, 'pk', 'pk')),
|
||||
name='dispatch')
|
||||
class PullDelete(DeleteView):
|
||||
model = models.Pull
|
||||
success_url = reverse_lazy('config:pull_list')
|
|
@ -10,7 +10,7 @@ core_router = Router()
|
|||
class UserSchema(ModelSchema):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["id", "username", "email"]
|
||||
fields = ["id", "username", "email", "is_staff", "is_superuser", "first_name", "last_name"]
|
||||
|
||||
@core_router.get("/me", response=UserSchema)
|
||||
def me(request):
|
||||
|
@ -22,7 +22,7 @@ api = NinjaAPI(
|
|||
title="Portier API",
|
||||
version="2.0.0",
|
||||
description="HTTP API for Portier. Use this to interact with the Portier backend.",
|
||||
csrf=True,
|
||||
csrf=False, # Disable CSRF for now
|
||||
)
|
||||
|
||||
api.add_router("/", core_router, auth=django_auth, tags=["Core"])
|
||||
|
|
|
@ -27,7 +27,8 @@ SECRET_KEY = os.environ.get("SECRET_KEY", default="CHANGE_ME!")
|
|||
DEBUG = int(os.environ.get("DEBUG", default=0))
|
||||
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", default="*").split(" ")
|
||||
CSRF_TRUSTED_ORIGINS = os.environ.get("DJANGO_CSRF_TRUSTED_ORIGINS", default="http://localhost").split(" ")
|
||||
|
||||
CORS_ALLOW_ALL_ORIGINS = bool(os.environ.get("DEBUG", default=False))
|
||||
SESSION_COOKIE_SAMESITE = 'None'
|
||||
|
||||
DEFAULT_GROUP = 'default'
|
||||
GLOBAL_STREAM_NAMESPACE = 'live'
|
||||
|
|
|
@ -6,8 +6,10 @@ django-fontawesome-5
|
|||
django-celery-beat
|
||||
django-filter
|
||||
django-ninja
|
||||
django-cors-headers
|
||||
django-types
|
||||
celery>=5.3
|
||||
gunicorn>=20
|
||||
psycopg2-binary
|
||||
requests
|
||||
redis
|
||||
redis
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
var app = new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
cfgs: [],
|
||||
isLoading: true
|
||||
},
|
||||
methods: {
|
||||
detailLink(id) {
|
||||
return `${id}/`
|
||||
},
|
||||
deleteLink(id) {
|
||||
return `${id}/delete`
|
||||
},
|
||||
toggleActive(cfg) {
|
||||
axios
|
||||
.patch('/api/v2/config/pulls/' + cfg.id, { active: !cfg.active })
|
||||
.then(response => {
|
||||
i = this.cfgs.findIndex((obj => obj.id == cfg.id))
|
||||
Vue.set(this.cfgs, i, response.data)
|
||||
}
|
||||
)
|
||||
},
|
||||
fetchData() {
|
||||
axios
|
||||
.get('/api/v2/config/pulls')
|
||||
.then(response => {
|
||||
this.cfgs = response.data
|
||||
this.isLoading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
axios.defaults.xsrfCookieName = 'csrftoken'
|
||||
axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN'
|
||||
this.fetchData()
|
||||
}
|
||||
})
|
|
@ -84,7 +84,7 @@
|
|||
<a class="nav-link{% if not perms.config.add_restream %} disabled{% endif %}{% if section == "restream" %} active {% endif %}" href="{% url 'config:restream_list' %}">{% fa5_icon 'expand-arrows-alt' %} {% trans "navbar_configuration_restream" %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="{% url 'config:stream_list' %}">{% fa5_icon 'compress-arrows-alt' %} {% trans "navbar_configuration_pull" %}</a>
|
||||
<a class="nav-link{% if not perms.config.add_pull %} disable{% endif %}{% if section == "pull" %} active {% endif %}" href="{% url 'config:pull_list' %}">{% fa5_icon 'compress-arrows-alt' %} {% trans "navbar_configuration_pull" %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="{% url 'config:stream_list' %}">{% fa5_icon 'broadcast-tower' %} {% trans "navbar_configuration_publish" %}</a>
|
||||
|
|
Loading…
Reference in New Issue