implement pulls

This commit is contained in:
Jan Koppe 2024-04-01 16:27:31 +02:00
parent 813780a18b
commit d2f980c318
Signed by: thunfisch
GPG Key ID: BE935B0735A2129B
26 changed files with 684 additions and 18 deletions

4
pyrightconfig.json Normal file
View File

@ -0,0 +1,4 @@
{
"venvPath": "source",
"venv": ".venv"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View File

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

View File

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

View File

@ -1,4 +1,6 @@
from django.dispatch import Signal
stream_active = Signal()
stream_inactive = Signal()
stream_inactive = Signal()
pull_active = Signal()
pull_inactive = Signal()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

14
source/config/util.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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