streamer: integrate, fix, ui change
This commit is contained in:
		@ -24,6 +24,7 @@ Usefull context:
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            "imports": {
 | 
					            "imports": {
 | 
				
			||||||
                "vue": "{{vue_url}}"
 | 
					                "vue": "{{vue_url}}"
 | 
				
			||||||
 | 
					                {% block assets-import-map %}{% endblock %}
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        </script>
 | 
					        </script>
 | 
				
			||||||
 | 
				
			|||||||
@ -70,14 +70,14 @@ class Connector:
 | 
				
			|||||||
        data = bytes("".join([str(d) for d in data]) + "\n", encoding="utf-8")
 | 
					        data = bytes("".join([str(d) for d in data]) + "\n", encoding="utf-8")
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            self.socket.sendall(data)
 | 
					            self.socket.sendall(data)
 | 
				
			||||||
            data = ""
 | 
					            resp = ""
 | 
				
			||||||
            while not response_re.search(data):
 | 
					            while not response_re.search(resp):
 | 
				
			||||||
                data += self.socket.recv(1024).decode("utf-8")
 | 
					                resp += self.socket.recv(1024).decode("utf-8")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if data:
 | 
					            if resp:
 | 
				
			||||||
                data = response_re.sub(r"\1", data).strip()
 | 
					                resp = response_re.sub(r"\1", resp).strip()
 | 
				
			||||||
                data = self.parse(data) if parse else self.parse_json(data) if parse_json else data
 | 
					                resp = self.parse(resp) if parse else self.parse_json(resp) if parse_json else resp
 | 
				
			||||||
            return data
 | 
					            return resp
 | 
				
			||||||
        except Exception:
 | 
					        except Exception:
 | 
				
			||||||
            self.close()
 | 
					            self.close()
 | 
				
			||||||
            if try_count > 0:
 | 
					            if try_count > 0:
 | 
				
			||||||
 | 
				
			|||||||
@ -131,7 +131,7 @@ class QueueSource(Source):
 | 
				
			|||||||
    def push(self, *paths):
 | 
					    def push(self, *paths):
 | 
				
			||||||
        """Add the provided paths to source's play queue."""
 | 
					        """Add the provided paths to source's play queue."""
 | 
				
			||||||
        for path in paths:
 | 
					        for path in paths:
 | 
				
			||||||
            self.controller.send(f"{self.id}_queue.push {path}")
 | 
					            print(self.controller.send(f"{self.id}_queue.push {path}"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def fetch(self):
 | 
					    def fetch(self):
 | 
				
			||||||
        super().fetch()
 | 
					        super().fetch()
 | 
				
			||||||
 | 
				
			|||||||
@ -19,7 +19,7 @@ end
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{# Transition to live sources #}
 | 
					{# Transition to live sources #}
 | 
				
			||||||
def to_live(stream, live)
 | 
					def to_live(stream, live)
 | 
				
			||||||
  stream = fade.final(duration=2., type='log', stream)
 | 
					  stream = fade.out(duration=2., type='log', stream)
 | 
				
			||||||
  live = fade.initial(duration=2., type='log', live)
 | 
					  live = fade.initial(duration=2., type='log', live)
 | 
				
			||||||
  add(normalize=false, [stream,live])
 | 
					  add(normalize=false, [stream,live])
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
@ -31,12 +31,12 @@ def to_stream(live, stream)
 | 
				
			|||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{# Skip command #}
 | 
					{# Skip command #}
 | 
				
			||||||
def add_skip_command(s) =
 | 
					def add_skip_command(id, s) =
 | 
				
			||||||
    def skip(_) =
 | 
					    def skip(_) =
 | 
				
			||||||
        source.skip(s)
 | 
					        source.skip(s)
 | 
				
			||||||
        "Done!"
 | 
					        "Done!"
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
    server.register(namespace="#{source.id(s)}",
 | 
					    server.register(namespace=id,
 | 
				
			||||||
        usage="skip",
 | 
					        usage="skip",
 | 
				
			||||||
        description="Skip the current song.",
 | 
					        description="Skip the current song.",
 | 
				
			||||||
        "skip",skip)
 | 
					        "skip",skip)
 | 
				
			||||||
@ -58,9 +58,8 @@ def interactive (id, s) =
 | 
				
			|||||||
                    usage="remaining",
 | 
					                    usage="remaining",
 | 
				
			||||||
                    "remaining", fun (_) ->  begin json.stringify(source.remaining(s)) end)
 | 
					                    "remaining", fun (_) ->  begin json.stringify(source.remaining(s)) end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    add_skip_command(s)
 | 
					    add_skip_command(id, s)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {# metadata: create an interactive variable as "{id}_meta" #}
 | 
					 | 
				
			||||||
    s_meta = interactive.string("#{id}_meta", "")
 | 
					    s_meta = interactive.string("#{id}_meta", "")
 | 
				
			||||||
    s = source.on_metadata(s, fun(meta) -> s_meta.set(json.stringify(meta)))
 | 
					    s = source.on_metadata(s, fun(meta) -> s_meta.set(json.stringify(meta)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -82,10 +81,7 @@ set("server.socket", true)
 | 
				
			|||||||
set("server.socket.path", "{{ streamer.socket_path }}")
 | 
					set("server.socket.path", "{{ streamer.socket_path }}")
 | 
				
			||||||
set("log.file.path", "{{ log_file }}")
 | 
					set("log.file.path", "{{ log_file }}")
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					{% block config_extras %}{% endblock %}
 | 
				
			||||||
{% block config_extras %}
 | 
					 | 
				
			||||||
{% endblock %}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block sources %}
 | 
					{% block sources %}
 | 
				
			||||||
{% with source=streamer.dealer %}
 | 
					{% with source=streamer.dealer %}
 | 
				
			||||||
@ -94,7 +90,6 @@ live = audio_to_stereo(interactive('{{ source.id }}',
 | 
				
			|||||||
))
 | 
					))
 | 
				
			||||||
{% endwith %}
 | 
					{% endwith %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
streams = rotate(id="streams", [
 | 
					streams = rotate(id="streams", [
 | 
				
			||||||
    {% for source in streamer.sources %}
 | 
					    {% for source in streamer.sources %}
 | 
				
			||||||
    {% if source != streamer.dealer %}
 | 
					    {% if source != streamer.dealer %}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										107
									
								
								aircox_streamer/templates/aircox_streamer/source_item.html
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										107
									
								
								aircox_streamer/templates/aircox_streamer/source_item.html
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							@ -1,8 +1,9 @@
 | 
				
			|||||||
{% comment %}List item for a source.{% endcomment %}
 | 
					{% comment %}List item for a source.{% endcomment %}
 | 
				
			||||||
{% load i18n %}
 | 
					{% load i18n %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<section class="box"><div class="columns is-desktop">
 | 
					<section class="box"><div class="flex-row gap-3">
 | 
				
			||||||
    <div class="column">
 | 
					
 | 
				
			||||||
 | 
					    <div class="flex-grow-1">
 | 
				
			||||||
        <h5 class='title is-5' :class="{'has-text-danger': source.isPlaying, 'has-text-warning': source.isPaused}">
 | 
					        <h5 class='title is-5' :class="{'has-text-danger': source.isPlaying, 'has-text-warning': source.isPaused}">
 | 
				
			||||||
            <span>
 | 
					            <span>
 | 
				
			||||||
                <span v-if="source.isPlaying" class="fas fa-play"></span>
 | 
					                <span v-if="source.isPlaying" class="fas fa-play"></span>
 | 
				
			||||||
@ -21,28 +22,70 @@
 | 
				
			|||||||
            </a>
 | 
					            </a>
 | 
				
			||||||
        </h5>
 | 
					        </h5>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <table class="table bg-transparent">
 | 
				
			||||||
 | 
					            <tbody>
 | 
				
			||||||
 | 
					                <tr><th class="has-text-right ws-nowrap">
 | 
				
			||||||
 | 
					                    {% translate "Status" %}
 | 
				
			||||||
 | 
					                    </th>
 | 
				
			||||||
 | 
					                    <td :class="{'has-text-danger': source.isPlaying, 'has-text-warning': source.isPaused}">
 | 
				
			||||||
 | 
					                        <span v-if="source.isPlaying" class="fas fa-play"></span>
 | 
				
			||||||
 | 
					                        <span v-else-if="source.data.status" class="fas fa-pause"></span>
 | 
				
			||||||
 | 
					                        [[ source.data.status_verbose || "—" ]]
 | 
				
			||||||
 | 
					                    </td>
 | 
				
			||||||
 | 
					                </tr>
 | 
				
			||||||
 | 
					                <tr v-if="source.data.air_time">
 | 
				
			||||||
 | 
					                    <th class="has-text-right ws-nowrap">
 | 
				
			||||||
 | 
					                        {% translate "Air time" %}
 | 
				
			||||||
 | 
					                    </th><td>
 | 
				
			||||||
 | 
					                        <span class="far fa-clock"></span>
 | 
				
			||||||
 | 
					                        <time :datetime="source.date">
 | 
				
			||||||
 | 
					                        [[ source.data.air_time.toLocaleDateString() ]],
 | 
				
			||||||
 | 
					                        [[ source.data.air_time.toLocaleTimeString() ]]
 | 
				
			||||||
 | 
					                        </time>
 | 
				
			||||||
 | 
					                    </td>
 | 
				
			||||||
 | 
					                <tr v-if="source.remaining">
 | 
				
			||||||
 | 
					                    <th class="has-text-right ws-nowrap">
 | 
				
			||||||
 | 
					                        {% translate "Time left" %}
 | 
				
			||||||
 | 
					                    </th><td>
 | 
				
			||||||
 | 
					                        <span class="far fa-hourglass"></span>
 | 
				
			||||||
 | 
					                        [[ source.remainingString ]]
 | 
				
			||||||
 | 
					                    </td>
 | 
				
			||||||
 | 
					                </tr>
 | 
				
			||||||
 | 
					                <tr v-if="source.data.uri">
 | 
				
			||||||
 | 
					                    <th class="has-text-right ws-nowrap">
 | 
				
			||||||
 | 
					                        {% translate "Data source" %}
 | 
				
			||||||
 | 
					                    </th><td>
 | 
				
			||||||
 | 
					                        <span class="far fa-play-circle"></span>
 | 
				
			||||||
 | 
					                        <template v-if="source.data.uri.length > 64">...</template>[[ (source.data.uri && source.data.uri.slice(-64)) || '—' ]]
 | 
				
			||||||
 | 
					                    </td>
 | 
				
			||||||
 | 
					                </tr>
 | 
				
			||||||
 | 
					            </tbody>
 | 
				
			||||||
 | 
					        </table>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
            <button class="button" @click="source.sync()"
 | 
					        <div>
 | 
				
			||||||
                    title="{% translate "Synchronize source with Liquidsoap" %}">
 | 
					            <button class="button smaller mr-2 mb-2" @click="source.restart()"
 | 
				
			||||||
                <span class="icon is-small">
 | 
					 | 
				
			||||||
                    <span class="fas fa-sync"></span>
 | 
					 | 
				
			||||||
                </span>
 | 
					 | 
				
			||||||
                <span>{% translate "Synchronise" %}</span>
 | 
					 | 
				
			||||||
            </button>
 | 
					 | 
				
			||||||
            <button class="button" @click="source.restart()"
 | 
					 | 
				
			||||||
                    title="{% translate "Restart current track" %}">
 | 
					                    title="{% translate "Restart current track" %}">
 | 
				
			||||||
                <span class="icon is-small">
 | 
					                <span class="icon is-small">
 | 
				
			||||||
                    <span class="fas fa-step-backward"></span>
 | 
					                    <span class="fas fa-step-backward"></span>
 | 
				
			||||||
                </span>
 | 
					                </span>
 | 
				
			||||||
                <span>{% translate "Restart" %}</span>
 | 
					                <span>{% translate "Restart" %}</span>
 | 
				
			||||||
            </button>
 | 
					            </button>
 | 
				
			||||||
            <button class="button" @click="source.skip()"
 | 
					            <button class="button smaller mr-2 mb-2" @click="source.skip()"
 | 
				
			||||||
                    title="{% translate "Skip current file" %}">
 | 
					                    title="{% translate "Skip current file" %}">
 | 
				
			||||||
                <span>{% translate "Skip" %}</span>
 | 
					                <span>{% translate "Skip" %}</span>
 | 
				
			||||||
                <span class="icon is-small">
 | 
					                <span class="icon is-small">
 | 
				
			||||||
                    <span class="fas fa-step-forward"></span>
 | 
					                    <span class="fas fa-step-forward"></span>
 | 
				
			||||||
                </span>
 | 
					                </span>
 | 
				
			||||||
            </button>
 | 
					            </button>
 | 
				
			||||||
 | 
					            <button class="button smaller mr-2 mb-2" @click="source.sync()"
 | 
				
			||||||
 | 
					                    title="{% translate "Synchronize source with Liquidsoap" %}">
 | 
				
			||||||
 | 
					                <span class="icon is-small">
 | 
				
			||||||
 | 
					                    <span class="fas fa-sync"></span>
 | 
				
			||||||
 | 
					                </span>
 | 
				
			||||||
 | 
					                <span>{% translate "Synchronise" %}</span>
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div v-if="source.isQueue">
 | 
					        <div v-if="source.isQueue">
 | 
				
			||||||
@ -89,46 +132,4 @@
 | 
				
			|||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="column is-two-fifths">
 | 
					 | 
				
			||||||
        <h6 class="subtitle is-6 is-marginless">Metadata</h6>
 | 
					 | 
				
			||||||
        <table class="table bg-transparent">
 | 
					 | 
				
			||||||
            <tbody>
 | 
					 | 
				
			||||||
                <tr><th class="has-text-right ws-nowrap">
 | 
					 | 
				
			||||||
                    {% translate "Status" %}
 | 
					 | 
				
			||||||
                    </th>
 | 
					 | 
				
			||||||
                    <td :class="{'has-text-danger': source.isPlaying, 'has-text-warning': source.isPaused}">
 | 
					 | 
				
			||||||
                        <span v-if="source.isPlaying" class="fas fa-play"></span>
 | 
					 | 
				
			||||||
                        <span v-else-if="source.data.status" class="fas fa-pause"></span>
 | 
					 | 
				
			||||||
                        [[ source.data.status_verbose || "—" ]]
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                </tr>
 | 
					 | 
				
			||||||
                <tr v-if="source.data.air_time">
 | 
					 | 
				
			||||||
                    <th class="has-text-right ws-nowrap">
 | 
					 | 
				
			||||||
                        {% translate "Air time" %}
 | 
					 | 
				
			||||||
                    </th><td>
 | 
					 | 
				
			||||||
                        <span class="far fa-clock"></span>
 | 
					 | 
				
			||||||
                        <time :datetime="source.date">
 | 
					 | 
				
			||||||
                        [[ source.data.air_time.toLocaleDateString() ]],
 | 
					 | 
				
			||||||
                        [[ source.data.air_time.toLocaleTimeString() ]]
 | 
					 | 
				
			||||||
                        </time>
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                <tr v-if="source.remaining">
 | 
					 | 
				
			||||||
                    <th class="has-text-right ws-nowrap">
 | 
					 | 
				
			||||||
                        {% translate "Time left" %}
 | 
					 | 
				
			||||||
                    </th><td>
 | 
					 | 
				
			||||||
                        <span class="far fa-hourglass"></span>
 | 
					 | 
				
			||||||
                        [[ source.remainingString ]]
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                </tr>
 | 
					 | 
				
			||||||
                <tr v-if="source.data.uri">
 | 
					 | 
				
			||||||
                    <th class="has-text-right ws-nowrap">
 | 
					 | 
				
			||||||
                        {% translate "Data source" %}
 | 
					 | 
				
			||||||
                    </th><td>
 | 
					 | 
				
			||||||
                        <span class="far fa-play-circle"></span>
 | 
					 | 
				
			||||||
                        <template v-if="source.data.uri.length > 64">...</template>[[ (source.data.uri && source.data.uri.slice(-64)) || '—' ]]
 | 
					 | 
				
			||||||
                    </td>
 | 
					 | 
				
			||||||
                </tr>
 | 
					 | 
				
			||||||
            </tbody>
 | 
					 | 
				
			||||||
        </table>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
</div></section>
 | 
					</div></section>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										4
									
								
								aircox_streamer/urls.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										4
									
								
								aircox_streamer/urls.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							@ -9,10 +9,10 @@ from . import views, viewsets
 | 
				
			|||||||
__all__ = ("api", "urls")
 | 
					__all__ = ("api", "urls")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
prefix = "(?P<station_pk>[0-9]+)/"
 | 
					prefix = "<int:station_pk>/"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router = DefaultRouter()
 | 
					router = DefaultRouter(use_regex_path=False)
 | 
				
			||||||
router.register(prefix + "playlist", viewsets.PlaylistSourceViewSet, basename="streamer-playlist")
 | 
					router.register(prefix + "playlist", viewsets.PlaylistSourceViewSet, basename="streamer-playlist")
 | 
				
			||||||
router.register(prefix + "queue", viewsets.QueueSourceViewSet, basename="streamer-queue")
 | 
					router.register(prefix + "queue", viewsets.QueueSourceViewSet, basename="streamer-queue")
 | 
				
			||||||
router.register("streamer", viewsets.StreamerViewSet, basename="streamer")
 | 
					router.register("streamer", viewsets.StreamerViewSet, basename="streamer")
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										10
									
								
								aircox_streamer/viewsets.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										10
									
								
								aircox_streamer/viewsets.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							@ -46,7 +46,7 @@ class ControllerViewSet(viewsets.ViewSet):
 | 
				
			|||||||
        if station_pk is None:
 | 
					        if station_pk is None:
 | 
				
			||||||
            return None
 | 
					            return None
 | 
				
			||||||
        if station_pk not in self.streamers:
 | 
					        if station_pk not in self.streamers:
 | 
				
			||||||
            raise Http404("station not found")
 | 
					            raise Http404(f"station not found: {station_pk}")
 | 
				
			||||||
        return self.streamers[station_pk]
 | 
					        return self.streamers[station_pk]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_serializer(self, **kwargs):
 | 
					    def get_serializer(self, **kwargs):
 | 
				
			||||||
@ -60,6 +60,7 @@ class ControllerViewSet(viewsets.ViewSet):
 | 
				
			|||||||
        return serializer.data
 | 
					        return serializer.data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def dispatch(self, request, *args, station_pk=None, **kwargs):
 | 
					    def dispatch(self, request, *args, station_pk=None, **kwargs):
 | 
				
			||||||
 | 
					        if not self.streamer:
 | 
				
			||||||
            self.streamer = self.get_streamer(station_pk)
 | 
					            self.streamer = self.get_streamer(station_pk)
 | 
				
			||||||
        return super().dispatch(request, *args, **kwargs)
 | 
					        return super().dispatch(request, *args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -80,6 +81,9 @@ class StreamerViewSet(ControllerViewSet):
 | 
				
			|||||||
    def dispatch(self, request, *args, pk=None, **kwargs):
 | 
					    def dispatch(self, request, *args, pk=None, **kwargs):
 | 
				
			||||||
        if pk is not None:
 | 
					        if pk is not None:
 | 
				
			||||||
            kwargs.setdefault("station_pk", pk)
 | 
					            kwargs.setdefault("station_pk", pk)
 | 
				
			||||||
 | 
					        if pk := kwargs.get("station_pk"):
 | 
				
			||||||
 | 
					            kwargs["station_pk"] = int(pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.streamer = self.get_streamer(**kwargs)
 | 
					        self.streamer = self.get_streamer(**kwargs)
 | 
				
			||||||
        self.object = self.streamer
 | 
					        self.object = self.streamer
 | 
				
			||||||
        return super().dispatch(request, *args, **kwargs)
 | 
					        return super().dispatch(request, *args, **kwargs)
 | 
				
			||||||
@ -88,6 +92,8 @@ class StreamerViewSet(ControllerViewSet):
 | 
				
			|||||||
class SourceViewSet(ControllerViewSet):
 | 
					class SourceViewSet(ControllerViewSet):
 | 
				
			||||||
    serializer_class = SourceSerializer
 | 
					    serializer_class = SourceSerializer
 | 
				
			||||||
    model = controllers.Source
 | 
					    model = controllers.Source
 | 
				
			||||||
 | 
					    lookup_field = "pk"
 | 
				
			||||||
 | 
					    lookup_value_converter = "str"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_sources(self):
 | 
					    def get_sources(self):
 | 
				
			||||||
        return (s for s in self.streamer.sources if isinstance(s, self.model))
 | 
					        return (s for s in self.streamer.sources if isinstance(s, self.model))
 | 
				
			||||||
@ -139,7 +145,7 @@ class QueueSourceViewSet(SourceViewSet):
 | 
				
			|||||||
    model = controllers.QueueSource
 | 
					    model = controllers.QueueSource
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_sound_queryset(self, request):
 | 
					    def get_sound_queryset(self, request):
 | 
				
			||||||
        return Sound.objects.station(request.station).broadcast()
 | 
					        return Sound.objects.station(request.station)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @action(detail=True, methods=["POST"])
 | 
					    @action(detail=True, methods=["POST"])
 | 
				
			||||||
    def push(self, request, pk):
 | 
					    def push(self, request, pk):
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user