forked from rc/aircox
		
	word on liquidsoap interface
This commit is contained in:
		
							
								
								
									
										31
									
								
								aircox_liquidsoap/management/commands/liquidsoap.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								aircox_liquidsoap/management/commands/liquidsoap.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					"""
 | 
				
			||||||
 | 
					Control Liquidsoap
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					from argparse import RawTextHelpFormatter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.core.management.base import BaseCommand, CommandError
 | 
				
			||||||
 | 
					from django.views.generic.base import View
 | 
				
			||||||
 | 
					from django.template.loader import render_to_string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import aircox_liquidsoap.settings as settings
 | 
				
			||||||
 | 
					import aircox_liquidsoap.utils as utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Command (BaseCommand):
 | 
				
			||||||
 | 
					    help= __doc__
 | 
				
			||||||
 | 
					    output_dir = settings.AIRCOX_LIQUIDSOAP_MEDIA
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def add_arguments (self, parser):
 | 
				
			||||||
 | 
					        parser.formatter_class=RawTextHelpFormatter
 | 
				
			||||||
 | 
					        parser.add_argument(
 | 
				
			||||||
 | 
					            '-o', '--on_air', action='store_true',
 | 
				
			||||||
 | 
					            help='Print what is on air'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def handle (self, *args, **options):
 | 
				
			||||||
 | 
					        controller = utils.Controller()
 | 
				
			||||||
 | 
					        controller.get()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -11,6 +11,7 @@ from django.views.generic.base import View
 | 
				
			|||||||
from django.template.loader import render_to_string
 | 
					from django.template.loader import render_to_string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import aircox_liquidsoap.settings as settings
 | 
					import aircox_liquidsoap.settings as settings
 | 
				
			||||||
 | 
					import aircox_liquidsoap.utils as utils
 | 
				
			||||||
import aircox_programs.settings as programs_settings
 | 
					import aircox_programs.settings as programs_settings
 | 
				
			||||||
import aircox_programs.models as models
 | 
					import aircox_programs.models as models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -89,36 +90,9 @@ class Command (BaseCommand):
 | 
				
			|||||||
        with open(path, 'w+') as file:
 | 
					        with open(path, 'w+') as file:
 | 
				
			||||||
            file.write(data)
 | 
					            file.write(data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					 | 
				
			||||||
    def liquid_stream (stream):
 | 
					 | 
				
			||||||
        def to_seconds (time):
 | 
					 | 
				
			||||||
            return 3600 * time.hour + 60 * time.minute + time.second
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return {
 | 
					 | 
				
			||||||
            'name': stream.program.get_slug_name(),
 | 
					 | 
				
			||||||
            'begin': stream.begin.strftime('%Hh%M') if stream.begin else None,
 | 
					 | 
				
			||||||
            'end': stream.end.strftime('%Hh%M') if stream.end else None,
 | 
					 | 
				
			||||||
            'delay': to_seconds(stream.delay) if stream.delay else None,
 | 
					 | 
				
			||||||
            'file': '{}/stream_{}.m3u'.format(
 | 
					 | 
				
			||||||
                settings.AIRCOX_LIQUIDSOAP_MEDIA,
 | 
					 | 
				
			||||||
                stream.pk,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def liquid_station (self, station):
 | 
					 | 
				
			||||||
        station.streams =  [
 | 
					 | 
				
			||||||
            self.liquid_stream(stream)
 | 
					 | 
				
			||||||
            for stream in models.Stream.objects.filter(
 | 
					 | 
				
			||||||
                program__active = True, program__station = station
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
        return station
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_config (self, output = None):
 | 
					    def get_config (self, output = None):
 | 
				
			||||||
        context = {
 | 
					        context = {
 | 
				
			||||||
            'stations': [ self.liquid_station(station)
 | 
					            'monitor': utils.Monitor(),
 | 
				
			||||||
                for station in models.Station.objects.filter(active = True)
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
            'settings': settings,
 | 
					            'settings': settings,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -129,16 +103,18 @@ class Command (BaseCommand):
 | 
				
			|||||||
        self.print(data, output, 'aircox.liq')
 | 
					        self.print(data, output, 'aircox.liq')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_playlist (self, stream = None, output = None):
 | 
					    def get_playlist (self, stream = None, output = None):
 | 
				
			||||||
        path =  os.path.join(
 | 
					        program = stream.program
 | 
				
			||||||
            programs_settings.AIRCOX_SOUND_ARCHIVES_SUBDIR,
 | 
					        source = utils.Source(program = program)
 | 
				
			||||||
            stream.program.path
 | 
					
 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        sounds = models.Sound.objects.filter(
 | 
					        sounds = models.Sound.objects.filter(
 | 
				
			||||||
            # good_quality = True,
 | 
					            # good_quality = True,
 | 
				
			||||||
            type = models.Sound.Type['archive'],
 | 
					            type = models.Sound.Type['archive'],
 | 
				
			||||||
                path__startswith = path
 | 
					            path__startswith = os.path.join(
 | 
				
			||||||
 | 
					                programs_settings.AIRCOX_SOUND_ARCHIVES_SUBDIR,
 | 
				
			||||||
 | 
					                program.path
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        data = '\n'.join(sound.path for sound in sounds)
 | 
					        data = '\n'.join(sound.path for sound in sounds)
 | 
				
			||||||
        self.print(data, output, 'stream_{}.m3u'.format(stream.pk))
 | 
					        self.print(data, output, utils.Source(program = program).path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					{% with metadata=source.metadata %}
 | 
				
			||||||
 | 
					<div class="source {% if metadata.initial_uri == controller.master.metadata.initial_uri %}on_air{% endif %}">
 | 
				
			||||||
 | 
					    <h2>{{ source.name }}</h2>
 | 
				
			||||||
 | 
					    <time>{{ metadata.on_air }}</time>
 | 
				
			||||||
 | 
					    <span class="path">{{ metadata.initial_uri }}</span>
 | 
				
			||||||
 | 
					    <span class="status" status="{{ metadata.status }}">{{ metadata.status }}</span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <button onclick="liquid_action('{{controller.id}}','{{source.id}}','skip');">
 | 
				
			||||||
 | 
					        skip
 | 
				
			||||||
 | 
					    </button>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					{% endwith %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										31
									
								
								aircox_liquidsoap/templates/aircox_liquidsoap/config.liq
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								aircox_liquidsoap/templates/aircox_liquidsoap/config.liq
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					{# Utilities #}
 | 
				
			||||||
 | 
					def interactive_source (id, s, ) = \
 | 
				
			||||||
 | 
					    def apply_metadata(m) = \
 | 
				
			||||||
 | 
					        m = json_of(compact=true, m) \
 | 
				
			||||||
 | 
					        ignore(interactive.string('#{id}_meta', m)) \
 | 
				
			||||||
 | 
					    end \
 | 
				
			||||||
 | 
					    \
 | 
				
			||||||
 | 
					    s = on_metadata(id = id, apply_metadata, s) \
 | 
				
			||||||
 | 
					    add_skip_command(s) \
 | 
				
			||||||
 | 
					    s \
 | 
				
			||||||
 | 
					end \
 | 
				
			||||||
 | 
					\
 | 
				
			||||||
 | 
					def stream (id, file) = \
 | 
				
			||||||
 | 
					    s = playlist(id = '#{id}_playlist', mode = "random", file) \
 | 
				
			||||||
 | 
					    interactive_source(id, s) \
 | 
				
			||||||
 | 
					end \
 | 
				
			||||||
 | 
					\
 | 
				
			||||||
 | 
					{# Config #}
 | 
				
			||||||
 | 
					set("server.socket", true) \
 | 
				
			||||||
 | 
					set("server.socket.path", "{{ settings.AIRCOX_LIQUIDSOAP_SOCKET }}") \
 | 
				
			||||||
 | 
					{% for key, value in settings.AIRCOX_LIQUIDSOAP_SET.items %}
 | 
				
			||||||
 | 
					set("{{ key|safe }}", {{ value|safe }}) \
 | 
				
			||||||
 | 
					{% endfor %}
 | 
				
			||||||
 | 
					\
 | 
				
			||||||
 | 
					\
 | 
				
			||||||
 | 
					{% for controller in monitor.controllers.values %}
 | 
				
			||||||
 | 
					{% include 'aircox_liquidsoap/station.liq' %} \
 | 
				
			||||||
 | 
					{{ controller.id }} = make_station_{{ controller.id }}() \
 | 
				
			||||||
 | 
					output.alsa({{ controller.id }}) \
 | 
				
			||||||
 | 
					{% endfor %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1,11 +1,129 @@
 | 
				
			|||||||
 | 
					{% if not embed %}
 | 
				
			||||||
 | 
					<html>
 | 
				
			||||||
 | 
					    <head>
 | 
				
			||||||
 | 
					        <style>
 | 
				
			||||||
 | 
					        .station {
 | 
				
			||||||
 | 
					            margin: 2em;
 | 
				
			||||||
 | 
					            border: 1px grey solid;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% for station in stations %}
 | 
					            .sources {
 | 
				
			||||||
{% for name, source in station.sources.items %}
 | 
					                padding: 0.5em;
 | 
				
			||||||
<div class="source">
 | 
					                box-shadow: inset 0.1em 0.1em 0.5em rgba(0, 0, 0, 0.5);
 | 
				
			||||||
    {{ name }}:
 | 
					            }
 | 
				
			||||||
        {{ source.initial_uri }}
 | 
					
 | 
				
			||||||
        <time style="font-size: 0.9em; color: grey">{{ source.on_air }}</time>
 | 
					        .station h1 {
 | 
				
			||||||
</div>
 | 
					            font-size: 1.2em;
 | 
				
			||||||
{% endfor %}
 | 
					            margin: 0.2em;
 | 
				
			||||||
{% endfor %}
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .source {
 | 
				
			||||||
 | 
					            border-left: 0.5em solid grey;
 | 
				
			||||||
 | 
					            font-size: 0.9em;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .on_air {
 | 
				
			||||||
 | 
					                display: block;
 | 
				
			||||||
 | 
					                border-left: 0.5em solid #f00;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .source h2 {
 | 
				
			||||||
 | 
					                display: inline-block;
 | 
				
			||||||
 | 
					                min-width: 10em;
 | 
				
			||||||
 | 
					                font-size: 1em;
 | 
				
			||||||
 | 
					                margin: 0.2em;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .source time {
 | 
				
			||||||
 | 
					                display: inline-block;
 | 
				
			||||||
 | 
					                margin-right: 2em;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            .source span {
 | 
				
			||||||
 | 
					                font-size: 1em;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .error {
 | 
				
			||||||
 | 
					            padding: 0.2em;
 | 
				
			||||||
 | 
					            color: red;
 | 
				
			||||||
 | 
					            font-weight: bold;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        </style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            function get_token() {
 | 
				
			||||||
 | 
					                return document.cookie.replace(/.*csrftoken=([^;]+)(;.*|$)/, '$1');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            function liquid_action (controller, source, action) {
 | 
				
			||||||
 | 
					                params = 'controller=' + controller + '&&source=' + source +
 | 
				
			||||||
 | 
					                         '&&action=' + action;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                req = new XMLHttpRequest()
 | 
				
			||||||
 | 
					                req.open('POST', '{% url 'liquid-controller' %}', false);
 | 
				
			||||||
 | 
					                req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
 | 
				
			||||||
 | 
					                req.setRequestHeader("Content-length", params.length);
 | 
				
			||||||
 | 
					                req.setRequestHeader("Connection", "close");
 | 
				
			||||||
 | 
					                req.setRequestHeader("X-CSRFToken", get_token());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                req.send(params);
 | 
				
			||||||
 | 
					                liquid_update()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            function liquid_update (update) {
 | 
				
			||||||
 | 
					                req = new XMLHttpRequest()
 | 
				
			||||||
 | 
					                req.open('GET', '{% url 'liquid-controller' %}?embed', true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                req.onreadystatechange = function() {
 | 
				
			||||||
 | 
					                    if(req.readyState != 4 || (req.status != 200 && req.status != 0))
 | 
				
			||||||
 | 
					                        return;
 | 
				
			||||||
 | 
					                    document.getElementById('liquid-stations').innerHTML =
 | 
				
			||||||
 | 
					                        req.responseText;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if(update)
 | 
				
			||||||
 | 
					                        window.setTimeout(function() { liquid_update(update);}, 5000);
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                req.send();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            liquid_update(true);
 | 
				
			||||||
 | 
					        </script>
 | 
				
			||||||
 | 
					    </head>
 | 
				
			||||||
 | 
					    <body>
 | 
				
			||||||
 | 
					        <main id="liquid-stations">
 | 
				
			||||||
 | 
					{% endif %}
 | 
				
			||||||
 | 
					            {% for c_id, controller in monitor.controllers.items %}
 | 
				
			||||||
 | 
					            {% with on_air=controller.on_air %}
 | 
				
			||||||
 | 
					            <div id="{{ c_id }}" class="station">
 | 
				
			||||||
 | 
					                <header>
 | 
				
			||||||
 | 
					                    {% if not controller.connector.available %}
 | 
				
			||||||
 | 
					                    <span class="error" style="float:right;">disconnected</span>
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                    <h1>
 | 
				
			||||||
 | 
					                        {{ controller.station.name }}
 | 
				
			||||||
 | 
					                    </h1>
 | 
				
			||||||
 | 
					                </header>
 | 
				
			||||||
 | 
					                <div class="sources">
 | 
				
			||||||
 | 
					                    {% with source=controller.master %}
 | 
				
			||||||
 | 
					                    {% include 'aircox_liquidsoap/base_source.html' %}
 | 
				
			||||||
 | 
					                    {% endwith %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    {% with source=controller.dealer %}
 | 
				
			||||||
 | 
					                    {% include 'aircox_liquidsoap/base_source.html' %}
 | 
				
			||||||
 | 
					                    {% endwith %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    {% for source in controller.streams.values %}
 | 
				
			||||||
 | 
					                    {% include 'aircox_liquidsoap/base_source.html' %}
 | 
				
			||||||
 | 
					                    {% endfor %}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            {% endwith %}
 | 
				
			||||||
 | 
					            {% endfor %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% if not embed %}
 | 
				
			||||||
 | 
					        </main>
 | 
				
			||||||
 | 
					    </body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
 | 
					{% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -5,54 +5,50 @@ A station has multiple sources:
 | 
				
			|||||||
- streams: a rotate source with all playlists
 | 
					- streams: a rotate source with all playlists
 | 
				
			||||||
- single: security song
 | 
					- single: security song
 | 
				
			||||||
{% endcomment %}
 | 
					{% endcomment %}
 | 
				
			||||||
{% with name=station.name streams=station.streams %}
 | 
					def make_station_{{ controller.id }} () = \
 | 
				
			||||||
def make_station_{{ name }} () = \
 | 
					 | 
				
			||||||
    {# dealer #}
 | 
					    {# dealer #}
 | 
				
			||||||
    dealer = interactive_source('{{ name }}_dealer', playlist.once( \
 | 
					    {% with source=controller.dealer %}
 | 
				
			||||||
 | 
					    {% if source %}
 | 
				
			||||||
 | 
					    dealer = interactive_source('{{ source.id }}', playlist.once( \
 | 
				
			||||||
        reload_mode='watch', \
 | 
					        reload_mode='watch', \
 | 
				
			||||||
        '/tmp/dealer.m3u', \
 | 
					        "{{ source.path }}", \
 | 
				
			||||||
    )) \
 | 
					    )) \
 | 
				
			||||||
    \
 | 
					    \
 | 
				
			||||||
    dealer_on = interactive.bool("{{ name }}_dealer_on", false) \
 | 
					    dealer_on = interactive.bool("{{ source.id }}_on", false) \
 | 
				
			||||||
 | 
					    {% endif %}
 | 
				
			||||||
 | 
					    {% endwith %}
 | 
				
			||||||
    \
 | 
					    \
 | 
				
			||||||
    {# streams #}
 | 
					    {# streams #}
 | 
				
			||||||
    streams = interactive_source("streams", rotate([ \
 | 
					    streams = interactive_source("streams", rotate([ \
 | 
				
			||||||
    {% for stream in streams %}
 | 
					    {% for source in controller.streams.values %}
 | 
				
			||||||
    {% if stream.delay %}
 | 
					    {% with info=source.stream_info %}
 | 
				
			||||||
        delay({{ stream.delay }}., stream("{{ name }}_{{ stream.name }}", "{{ stream.file }}")), \
 | 
					    {% if info.delay %}
 | 
				
			||||||
 | 
					        delay({{ info.delay }}., stream("{{ source.id }}", "{{ source.path }}")), \
 | 
				
			||||||
 | 
					    {% elif info.begin and info.end %}
 | 
				
			||||||
 | 
					        at({ {{info.begin}}-{{info.end}} }, stream("{{ source.id }}", "{{ source.path }}")), \
 | 
				
			||||||
    {% endif %}
 | 
					    {% endif %}
 | 
				
			||||||
 | 
					    {% endwith %}
 | 
				
			||||||
    {% endfor %}
 | 
					    {% endfor %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        switch([ \
 | 
					    {% for source in controller.streams.values %}
 | 
				
			||||||
    {% for stream in streams %}
 | 
					    {% if not source.stream_info %}
 | 
				
			||||||
    {% if stream.begin and stream.end %}
 | 
					        stream("{{ source.id }}", "{{ source.path }}"), \
 | 
				
			||||||
            ({ stream.begin, stream.end }, stream("{{ name }}_{{ stream.name }}", "{{ stream.file }}")), \
 | 
					 | 
				
			||||||
    {% endif %}
 | 
					 | 
				
			||||||
    {% endfor %}
 | 
					 | 
				
			||||||
        ]), \
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    {% for stream in streams %}
 | 
					 | 
				
			||||||
    {% if not stream.delay %}
 | 
					 | 
				
			||||||
    {% if not stream.begin or not stream.end %}
 | 
					 | 
				
			||||||
        stream("{{ name }}_{{ stream.name }}", "{{ stream.file }}"), \
 | 
					 | 
				
			||||||
    {% endif %}
 | 
					 | 
				
			||||||
    {% endif %}
 | 
					    {% endif %}
 | 
				
			||||||
    {% endfor %}
 | 
					    {% endfor %}
 | 
				
			||||||
    ])) \
 | 
					    ])) \
 | 
				
			||||||
    \
 | 
					    \
 | 
				
			||||||
    {# station #}
 | 
					    {# station #}
 | 
				
			||||||
    interactive_source ( \
 | 
					    interactive_source ( \
 | 
				
			||||||
        "{{ name }}", \
 | 
					        "{{ controller.id }}", \
 | 
				
			||||||
        fallback(track_sensitive = false, [ \
 | 
					        fallback(track_sensitive = false, [ \
 | 
				
			||||||
            at(dealer_on, dealer), \
 | 
					            at(dealer_on, dealer), \
 | 
				
			||||||
        {% if station.fallback %}
 | 
					 | 
				
			||||||
            streams, \
 | 
					            streams, \
 | 
				
			||||||
            single("{{ station.fallback }}") \
 | 
					        {% if controller.station.fallback %}
 | 
				
			||||||
 | 
					            single("{{ controller.station.fallback }}"), \
 | 
				
			||||||
        {% else %}
 | 
					        {% else %}
 | 
				
			||||||
            mksafe(streams) \
 | 
					            blank(), \
 | 
				
			||||||
        {% endif %}
 | 
					        {% endif %}
 | 
				
			||||||
        ]) \
 | 
					        ]) \
 | 
				
			||||||
    ) \
 | 
					    ) \
 | 
				
			||||||
end \
 | 
					end \
 | 
				
			||||||
{% endwith %}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,34 +1,72 @@
 | 
				
			|||||||
 | 
					import os
 | 
				
			||||||
import socket
 | 
					import socket
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
 | 
					import json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.utils.translation import ugettext as _, ugettext_lazy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import aircox_programs.models as models
 | 
				
			||||||
import aircox_liquidsoap.settings as settings
 | 
					import aircox_liquidsoap.settings as settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Controller:
 | 
					 | 
				
			||||||
    socket = None
 | 
					 | 
				
			||||||
    available = False
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__ (self):
 | 
					class Connector:
 | 
				
			||||||
        self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
 | 
					    """
 | 
				
			||||||
 | 
					    Telnet connector utility.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    address: a string to the unix domain socket file, or a tuple
 | 
				
			||||||
 | 
					        (host, port) for TCP/IP connection
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    __socket = None
 | 
				
			||||||
 | 
					    __available = False
 | 
				
			||||||
 | 
					    address = settings.AIRCOX_LIQUIDSOAP_SOCKET
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def available (self):
 | 
				
			||||||
 | 
					        return self.__available
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__ (self, address = None):
 | 
				
			||||||
 | 
					        self.address = address
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def open (self):
 | 
					    def open (self):
 | 
				
			||||||
        if self.available:
 | 
					        if self.__available:
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        address = settings.AIRCOX_LIQUIDSOAP_SOCKET
 | 
					 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            self.socket.connect(address)
 | 
					            family = socket.AF_INET if type(self.address) in (tuple, list) else \
 | 
				
			||||||
            self.available = True
 | 
					                     socket.AF_UNIX
 | 
				
			||||||
 | 
					            self.__socket = socket.socket(family, socket.SOCK_STREAM)
 | 
				
			||||||
 | 
					            self.__socket.connect(self.address)
 | 
				
			||||||
 | 
					            self.__available = True
 | 
				
			||||||
        except:
 | 
					        except:
 | 
				
			||||||
            print('can not connect to liquidsoap socket {}'.format(address))
 | 
					            print('can not connect to liquidsoap socket {}'.format(address))
 | 
				
			||||||
            self.available = False
 | 
					            self.__available = False
 | 
				
			||||||
            return -1
 | 
					            return -1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def send (self, data):
 | 
					    def send (self, *data, try_count = 1, parse = False, parse_json = False):
 | 
				
			||||||
        if self.open():
 | 
					        if self.open():
 | 
				
			||||||
            return ''
 | 
					            return ''
 | 
				
			||||||
        data = bytes(data + '\n', encoding='utf-8')
 | 
					        data = bytes(''.join([str(d) for d in data]) + '\n', encoding='utf-8')
 | 
				
			||||||
        self.socket.sendall(data)
 | 
					
 | 
				
			||||||
        return self.socket.recv(10240).decode('utf-8')
 | 
					        try:
 | 
				
			||||||
 | 
					            reg = re.compile('(.*)[\n\r]+END[\n\r]*$')
 | 
				
			||||||
 | 
					            self.__socket.sendall(data)
 | 
				
			||||||
 | 
					            data = ''
 | 
				
			||||||
 | 
					            while not reg.match(data):
 | 
				
			||||||
 | 
					                data += self.__socket.recv(1024).decode('unicode_escape')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if data:
 | 
				
			||||||
 | 
					                data = reg.sub(r'\1', data)
 | 
				
			||||||
 | 
					                data = data.strip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if parse:
 | 
				
			||||||
 | 
					                    data = self.parse(data)
 | 
				
			||||||
 | 
					                elif parse_json:
 | 
				
			||||||
 | 
					                    data = self.parse_json(data)
 | 
				
			||||||
 | 
					            return data
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            self.__available = False
 | 
				
			||||||
 | 
					            if try_count > 0:
 | 
				
			||||||
 | 
					                return self.send(data, try_count - 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def parse (self, string):
 | 
					    def parse (self, string):
 | 
				
			||||||
        string = string.split('\n')
 | 
					        string = string.split('\n')
 | 
				
			||||||
@ -41,10 +79,241 @@ class Controller:
 | 
				
			|||||||
            data[line['key']] = line['value']
 | 
					            data[line['key']] = line['value']
 | 
				
			||||||
        return data
 | 
					        return data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get (self, station, source = None):
 | 
					    def parse_json (self, string):
 | 
				
			||||||
        if source:
 | 
					        try:
 | 
				
			||||||
            r = self.send('{}_{}.get'.format(station.get_slug_name(), source))
 | 
					            if string[0] == '"' and string[-1] == '"':
 | 
				
			||||||
        else:
 | 
					                string = string[1:-1]
 | 
				
			||||||
            r = self.send('{}.get'.format(station.get_slug_name()))
 | 
					            return json.loads(string) if string else None
 | 
				
			||||||
        return self.parse(r) if r else None
 | 
					        except:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Source:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    A structure that holds informations about a LiquidSoap source.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    controller = None
 | 
				
			||||||
 | 
					    program = None
 | 
				
			||||||
 | 
					    metadata = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__ (self, controller = None, program = None):
 | 
				
			||||||
 | 
					        self.controller = controller
 | 
				
			||||||
 | 
					        self.program = program
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def station (self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Proxy to self.(program|controller).station
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self.program.station if self.program else \
 | 
				
			||||||
 | 
					                self.controller.station
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def connector (self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Proxy to self.controller.connector
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return self.controller.connector
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def id (self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Identifier for the source, scoped in the station's one
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        postfix = ('_stream_' + str(self.program.id)) if self.program else ''
 | 
				
			||||||
 | 
					        return self.station.slug + postfix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def name (self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Name of the related object (program or station)
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if self.program:
 | 
				
			||||||
 | 
					            return self.program.name
 | 
				
			||||||
 | 
					        return self.station.name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def path (self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Path to the playlist
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        return os.path.join(
 | 
				
			||||||
 | 
					            settings.AIRCOX_LIQUIDSOAP_MEDIA,
 | 
				
			||||||
 | 
					            self.id + '.m3u'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def playlist (self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        The playlist as an array
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            with open(self.path, 'r') as file:
 | 
				
			||||||
 | 
					                return file.readlines()
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            return []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @playlist.setter
 | 
				
			||||||
 | 
					    def playlist (self, sounds):
 | 
				
			||||||
 | 
					        with open(self.path, 'w') as file:
 | 
				
			||||||
 | 
					            file.write('\n'.join(sounds))
 | 
				
			||||||
 | 
					            self.connector.send(self.name, '_playlist.reload')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def stream_info (self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Return a dict with info related to the program's stream
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if not self.program:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        stream = models.Stream.objects.get(program = self.program)
 | 
				
			||||||
 | 
					        if not stream.begin and not stream.delay:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def to_seconds (time):
 | 
				
			||||||
 | 
					            return 3600 * time.hour + 60 * time.minute + time.second
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            'begin': stream.begin.strftime('%Hh%M') if stream.begin else None,
 | 
				
			||||||
 | 
					            'end': stream.end.strftime('%Hh%M') if stream.end else None,
 | 
				
			||||||
 | 
					            'delay': to_seconds(stream.delay) if stream.delay else None
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def skip (self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Skip a given source. If no source, use master.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.connector.send(self.id, '.skip')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def update (self, metadata = None):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Update metadata with the given metadata dict or request them to
 | 
				
			||||||
 | 
					        liquidsoap if nothing is given.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Return -1 in case no update happened
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if metadata:
 | 
				
			||||||
 | 
					            source = metadata.get('source') or ''
 | 
				
			||||||
 | 
					            if self.program and not source.startswith(self.id):
 | 
				
			||||||
 | 
					                return -1
 | 
				
			||||||
 | 
					            self.metadata = metadata
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        r = self.connector.send('var.get ', self.id + '_meta', parse_json=True)
 | 
				
			||||||
 | 
					        return self.update(metadata = r) if r else -1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Dealer (Source):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    The Dealer source is a source that is used for scheduled diffusions and
 | 
				
			||||||
 | 
					    manual sound diffusion.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    name = _('Dealer')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def id (self):
 | 
				
			||||||
 | 
					        return self.station.name + '_dealer'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def stream_info (self):
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_next_diffusion (self):
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def on_air (self, value = True):
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def playlist (self):
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            with open(self.path, 'r') as file:
 | 
				
			||||||
 | 
					                return file.readlines()
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            return []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @playlist.setter
 | 
				
			||||||
 | 
					    def playlist (self, sounds):
 | 
				
			||||||
 | 
					        with open(self.path, 'w') as file:
 | 
				
			||||||
 | 
					            file.write('\n'.join(sounds))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Controller:
 | 
				
			||||||
 | 
					    connector = None
 | 
				
			||||||
 | 
					    station = None      # the related station
 | 
				
			||||||
 | 
					    master = None       # master source (station's source)
 | 
				
			||||||
 | 
					    dealer = None       # dealer source
 | 
				
			||||||
 | 
					    streams = None      # streams sources
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def on_air (self):
 | 
				
			||||||
 | 
					        return self.master
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def id (self):
 | 
				
			||||||
 | 
					        return self.master and self.master.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def name (self):
 | 
				
			||||||
 | 
					        return self.master and self.master.name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__ (self, station, connector = None):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        use_connector: avoids the creation of a Connector, in case it is not needed
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.connector = connector
 | 
				
			||||||
 | 
					        self.station = station
 | 
				
			||||||
 | 
					        self.station.controller = self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.master = Source(self)
 | 
				
			||||||
 | 
					        self.dealer = Dealer(self)
 | 
				
			||||||
 | 
					        self.streams = {
 | 
				
			||||||
 | 
					            source.id : source
 | 
				
			||||||
 | 
					            for source in [
 | 
				
			||||||
 | 
					                Source(self, program)
 | 
				
			||||||
 | 
					                for program in models.Program.objects.filter(station = station,
 | 
				
			||||||
 | 
					                                                             active = True)
 | 
				
			||||||
 | 
					                if program.stream_set.count()
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get (self, source_id):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Get a source by its id
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if source_id == self.master.id:
 | 
				
			||||||
 | 
					            return self.master
 | 
				
			||||||
 | 
					        if source_id == self.dealer.id:
 | 
				
			||||||
 | 
					            return self.dealer
 | 
				
			||||||
 | 
					        return self.streams.get(source_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def update_all (self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Fetch and update all sources metadata.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.master.update()
 | 
				
			||||||
 | 
					        self.dealer.update()
 | 
				
			||||||
 | 
					        for source in self.streams.values():
 | 
				
			||||||
 | 
					            source.update()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Monitor:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Monitor multiple controllers.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    controllers = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__ (self, connector = None):
 | 
				
			||||||
 | 
					        self.controllers = {
 | 
				
			||||||
 | 
					            controller.id : controller
 | 
				
			||||||
 | 
					            for controller in [
 | 
				
			||||||
 | 
					                Controller(station, connector)
 | 
				
			||||||
 | 
					                for station in models.Station.objects.filter(active = True)
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def update (self):
 | 
				
			||||||
 | 
					        for controller in self.controllers.values():
 | 
				
			||||||
 | 
					            controller.update_all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -9,36 +9,50 @@ import aircox_liquidsoap.settings as settings
 | 
				
			|||||||
import aircox_liquidsoap.utils as utils
 | 
					import aircox_liquidsoap.utils as utils
 | 
				
			||||||
import aircox_programs.models as models
 | 
					import aircox_programs.models as models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					view_monitor = utils.Monitor(
 | 
				
			||||||
 | 
					    utils.Connector(address = settings.AIRCOX_LIQUIDSOAP_SOCKET)
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Actions:
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def exec (cl, monitor, controller, source, action):
 | 
				
			||||||
 | 
					        controller = monitor.controllers.get(controller)
 | 
				
			||||||
 | 
					        source = controller and controller.get(source)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not controller or not source or \
 | 
				
			||||||
 | 
					                action.startswith('__') or \
 | 
				
			||||||
 | 
					                action not in cl.__dict__:
 | 
				
			||||||
 | 
					            return -1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        action = getattr(Actions, action)
 | 
				
			||||||
 | 
					        return action(monitor, controller, source)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def skip (cl, monitor, controller, source):
 | 
				
			||||||
 | 
					        source.skip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LiquidControl (View):
 | 
					class LiquidControl (View):
 | 
				
			||||||
    template_name = 'aircox_liquidsoap/controller.html'
 | 
					    template_name = 'aircox_liquidsoap/controller.html'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data (self, **kwargs):
 | 
					    def get_context_data (self, **kwargs):
 | 
				
			||||||
        stations = models.Station.objects.all()
 | 
					        view_monitor.update()
 | 
				
			||||||
        controller = utils.Controller()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for station in stations:
 | 
					 | 
				
			||||||
            name = station.get_slug_name()
 | 
					 | 
				
			||||||
            streams = models.Stream.objects.filter(
 | 
					 | 
				
			||||||
                program__active = True,
 | 
					 | 
				
			||||||
                program__station = station
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # list sources
 | 
					 | 
				
			||||||
            sources = [ 'dealer' ] + \
 | 
					 | 
				
			||||||
                      [ stream.program.get_slug_name() for stream in streams]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # sources status
 | 
					 | 
				
			||||||
            station.sources = { name: controller.get(station) }
 | 
					 | 
				
			||||||
            station.sources.update({
 | 
					 | 
				
			||||||
                source: controller.get(station, source)
 | 
					 | 
				
			||||||
                for source in sources
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
            'request': self.request,
 | 
					            'request': self.request,
 | 
				
			||||||
            'stations': stations,
 | 
					            'monitor': view_monitor,
 | 
				
			||||||
 | 
					            'embed': 'embed' in self.request.GET,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def post (self, request = None, **kwargs):
 | 
				
			||||||
 | 
					        if 'action' in request.POST:
 | 
				
			||||||
 | 
					            POST = request.POST
 | 
				
			||||||
 | 
					            controller = POST.get('controller')
 | 
				
			||||||
 | 
					            source = POST.get('source')
 | 
				
			||||||
 | 
					            action = POST.get('action')
 | 
				
			||||||
 | 
					            Actions.exec(view_monitor, controller, source, action)
 | 
				
			||||||
 | 
					        return HttpResponse('')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get (self, request = None, **kwargs):
 | 
					    def get (self, request = None, **kwargs):
 | 
				
			||||||
        self.request = request
 | 
					        self.request = request
 | 
				
			||||||
        context = self.get_context_data(**kwargs)
 | 
					        context = self.get_context_data(**kwargs)
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user