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 django.dispatch import receiver
 | 
				
			||||||
from .models import Task
 | 
					from .models import Task
 | 
				
			||||||
from config.signals import stream_inactive
 | 
					from config.signals_shared import stream_inactive
 | 
				
			||||||
from config.models import Stream
 | 
					from config.models import Stream
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@receiver(stream_inactive)
 | 
					@receiver(stream_inactive)
 | 
				
			||||||
def delete_tasks(sender, **kwargs):
 | 
					def delete_tasks_when_stream_inactive(sender, **kwargs):
 | 
				
			||||||
    # when a stream was unpublished, all related tasks need to be deleted.
 | 
					    # 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'])
 | 
					    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 django.contrib import admin
 | 
				
			||||||
from guardian.admin import GuardedModelAdmin
 | 
					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)
 | 
					@admin.register(Stream)
 | 
				
			||||||
| 
						 | 
					@ -11,6 +11,10 @@ class StreamAdmin(GuardedModelAdmin):
 | 
				
			||||||
class RestreamAdmin(GuardedModelAdmin):
 | 
					class RestreamAdmin(GuardedModelAdmin):
 | 
				
			||||||
    fields = ['name', 'active', 'stream', 'format', 'target']
 | 
					    fields = ['name', 'active', 'stream', 'format', 'target']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@admin.register(Pull)
 | 
				
			||||||
 | 
					class PullAdmin(GuardedModelAdmin):
 | 
				
			||||||
 | 
					    fields = ['name', 'active', 'stream', 'source']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@admin.register(SRSNode)
 | 
					@admin.register(SRSNode)
 | 
				
			||||||
class SRSNodeAdmin(GuardedModelAdmin):
 | 
					class SRSNodeAdmin(GuardedModelAdmin):
 | 
				
			||||||
    fields = ['name', 'api_base', 'rtmp_base', 'active']
 | 
					    fields = ['name', 'api_base', 'rtmp_base', 'active']
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,6 +22,20 @@ class RestreamPatch(ModelSchema):
 | 
				
			||||||
    extra = "forbid"
 | 
					    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 Stream(ModelSchema):
 | 
				
			||||||
  class Meta:
 | 
					  class Meta:
 | 
				
			||||||
    model = models.Stream
 | 
					    model = models.Stream
 | 
				
			||||||
| 
						 | 
					@ -87,6 +101,7 @@ def delete_stream(request, id: int):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  stream.delete()
 | 
					  stream.delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					####################################################################################################
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@router.get('/restreams', response=List[Restream])
 | 
					@router.get('/restreams', response=List[Restream])
 | 
				
			||||||
def list_restreams(request):
 | 
					def list_restreams(request):
 | 
				
			||||||
| 
						 | 
					@ -142,3 +157,60 @@ def delete_restream(request, id: int):
 | 
				
			||||||
    raise HttpError(401, "unauthorized")
 | 
					    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'
 | 
					    name = 'config'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def ready(self):
 | 
					    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
 | 
					        # limit the stream selection to user-accessible streams
 | 
				
			||||||
        self.fields['stream'].queryset = get_objects_for_user(user, 'config.view_stream')
 | 
					        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 django.db.models.signals import pre_delete
 | 
				
			||||||
from portier.common import handlers
 | 
					from portier.common import handlers
 | 
				
			||||||
from config import signals_shared
 | 
					from config import signals_shared
 | 
				
			||||||
 | 
					from config.util import validate_stream_url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Stream(models.Model):
 | 
					class Stream(models.Model):
 | 
				
			||||||
| 
						 | 
					@ -41,7 +42,7 @@ class Stream(models.Model):
 | 
				
			||||||
        return _('stream_class_name')
 | 
					        return _('stream_class_name')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return self.name
 | 
					        return str(self.name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pre_delete.connect(handlers.remove_obj_perms_connected_with_user, sender=Stream)
 | 
					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}"
 | 
					        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):
 | 
					class Restream(models.Model):
 | 
				
			||||||
    FORMATS = (
 | 
					    FORMATS = (
 | 
				
			||||||
        ('flv', 'flv (RTMP)'),
 | 
					        ('flv', 'flv (RTMP)'),
 | 
				
			||||||
        ('mpegts', 'mpegts (SRT)'),
 | 
					        ('mpegts', 'mpegts (SRT)'),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    stream = models.ForeignKey(Stream, on_delete=models.CASCADE, help_text=_('restream_stream_help'))
 | 
					    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'))
 | 
					    name = models.CharField(max_length=100, help_text=_('restream_name_help'))
 | 
				
			||||||
    active = models.BooleanField(help_text=_('restream_activate_help'))
 | 
					    active = models.BooleanField(help_text=_('restream_activate_help'))
 | 
				
			||||||
    format = models.CharField(max_length=6, choices=FORMATS, default='flv', help_text=_('restream_format_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)
 | 
					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
 | 
				
			||||||
| 
						 | 
					@ -2,3 +2,5 @@ from django.dispatch import Signal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
stream_active = 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 django.urls import path
 | 
				
			||||||
from config.views import restream, stream
 | 
					from config.views import restream, stream, pull
 | 
				
			||||||
from config.views.srs import callback_srs
 | 
					from config.views.srs import callback_srs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app_name = 'config'
 | 
					app_name = 'config'
 | 
				
			||||||
| 
						 | 
					@ -16,4 +16,9 @@ urlpatterns = [
 | 
				
			||||||
    path('restream/<int:pk>/change', restream.RestreamUpdate.as_view(), name='restream_change'),
 | 
					    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/<int:pk>/delete', restream.RestreamDelete.as_view(), name='restream_delete'),
 | 
				
			||||||
    path('restream/create', restream.RestreamCreate.as_view(), name='restream_create'),
 | 
					    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 UserSchema(ModelSchema):
 | 
				
			||||||
  class Meta:
 | 
					  class Meta:
 | 
				
			||||||
    model = User
 | 
					    model = User
 | 
				
			||||||
    fields = ["id", "username", "email"]
 | 
					    fields = ["id", "username", "email", "is_staff", "is_superuser", "first_name", "last_name"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@core_router.get("/me", response=UserSchema)
 | 
					@core_router.get("/me", response=UserSchema)
 | 
				
			||||||
def me(request):
 | 
					def me(request):
 | 
				
			||||||
| 
						 | 
					@ -22,7 +22,7 @@ api = NinjaAPI(
 | 
				
			||||||
  title="Portier API",
 | 
					  title="Portier API",
 | 
				
			||||||
  version="2.0.0",
 | 
					  version="2.0.0",
 | 
				
			||||||
  description="HTTP API for Portier. Use this to interact with the Portier backend.",
 | 
					  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"])
 | 
					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))
 | 
					DEBUG = int(os.environ.get("DEBUG", default=0))
 | 
				
			||||||
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", default="*").split(" ")
 | 
					ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", default="*").split(" ")
 | 
				
			||||||
CSRF_TRUSTED_ORIGINS = os.environ.get("DJANGO_CSRF_TRUSTED_ORIGINS", default="http://localhost").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'
 | 
					DEFAULT_GROUP = 'default'
 | 
				
			||||||
GLOBAL_STREAM_NAMESPACE = 'live'
 | 
					GLOBAL_STREAM_NAMESPACE = 'live'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,8 @@ django-fontawesome-5
 | 
				
			||||||
django-celery-beat
 | 
					django-celery-beat
 | 
				
			||||||
django-filter
 | 
					django-filter
 | 
				
			||||||
django-ninja
 | 
					django-ninja
 | 
				
			||||||
 | 
					django-cors-headers
 | 
				
			||||||
 | 
					django-types
 | 
				
			||||||
celery>=5.3
 | 
					celery>=5.3
 | 
				
			||||||
gunicorn>=20
 | 
					gunicorn>=20
 | 
				
			||||||
psycopg2-binary
 | 
					psycopg2-binary
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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>
 | 
					              <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>
 | 
				
			||||||
            <li class="nav-item">
 | 
					            <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>
 | 
				
			||||||
            <li class="nav-item">
 | 
					            <li class="nav-item">
 | 
				
			||||||
              <a class="nav-link disabled" href="{% url 'config:stream_list' %}">{% fa5_icon 'broadcast-tower' %} {% trans "navbar_configuration_publish" %}</a>
 | 
					              <a class="nav-link disabled" href="{% url 'config:stream_list' %}">{% fa5_icon 'broadcast-tower' %} {% trans "navbar_configuration_publish" %}</a>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue