#106: tests: aircox_streamer #110
										
											Binary file not shown.
										
									
								
							@ -1,130 +0,0 @@
 | 
			
		||||
# SOME DESCRIPTIVE TITLE.
 | 
			
		||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
 | 
			
		||||
# This file is distributed under the same license as the PACKAGE package.
 | 
			
		||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
 | 
			
		||||
#
 | 
			
		||||
#, fuzzy
 | 
			
		||||
msgid ""
 | 
			
		||||
msgstr ""
 | 
			
		||||
"Project-Id-Version: PACKAGE VERSION\n"
 | 
			
		||||
"Report-Msgid-Bugs-To: \n"
 | 
			
		||||
"POT-Creation-Date: 2020-01-06 14:14+0100\n"
 | 
			
		||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 | 
			
		||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
			
		||||
"Language-Team: LANGUAGE <LL@li.org>\n"
 | 
			
		||||
"Language: \n"
 | 
			
		||||
"MIME-Version: 1.0\n"
 | 
			
		||||
"Content-Type: text/plain; charset=UTF-8\n"
 | 
			
		||||
"Content-Transfer-Encoding: 8bit\n"
 | 
			
		||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
 | 
			
		||||
 | 
			
		||||
#: models.py:37
 | 
			
		||||
msgid "input"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: models.py:38
 | 
			
		||||
msgid "output"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: models.py:56
 | 
			
		||||
msgid "station"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: models.py:58
 | 
			
		||||
msgid "direction"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: models.py:59
 | 
			
		||||
msgid "type"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: models.py:61
 | 
			
		||||
msgid "active"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: models.py:62
 | 
			
		||||
msgid "this port is active"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: models.py:65
 | 
			
		||||
msgid "port settings"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: models.py:66
 | 
			
		||||
msgid ""
 | 
			
		||||
"list of comma separated params available; this is put in the output config "
 | 
			
		||||
"file as raw code; plugin related"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: templates/aircox_streamer/source_item.html:19
 | 
			
		||||
msgid "Synchronize source with Liquidsoap"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: templates/aircox_streamer/source_item.html:23
 | 
			
		||||
msgid "Synchronise"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: templates/aircox_streamer/source_item.html:26
 | 
			
		||||
msgid "Restart current track"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: templates/aircox_streamer/source_item.html:30
 | 
			
		||||
msgid "Restart"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: templates/aircox_streamer/source_item.html:33
 | 
			
		||||
msgid "Skip current file"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: templates/aircox_streamer/source_item.html:34
 | 
			
		||||
msgid "Skip"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: templates/aircox_streamer/source_item.html:43
 | 
			
		||||
msgid "Add sound"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: templates/aircox_streamer/source_item.html:51
 | 
			
		||||
msgid "Select a sound"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: templates/aircox_streamer/source_item.html:53
 | 
			
		||||
msgid "Add a sound to the queue (queue may start playing)"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: templates/aircox_streamer/source_item.html:62
 | 
			
		||||
msgid "Add"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: templates/aircox_streamer/source_item.html:68
 | 
			
		||||
msgid "Sounds in queue"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: templates/aircox_streamer/source_item.html:86
 | 
			
		||||
msgid "Status"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: templates/aircox_streamer/source_item.html:96
 | 
			
		||||
msgid "Air time"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: templates/aircox_streamer/source_item.html:106
 | 
			
		||||
msgid "Time left"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: templates/aircox_streamer/source_item.html:114
 | 
			
		||||
msgid "Data source"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: templates/aircox_streamer/streamer.html:19
 | 
			
		||||
msgid "Reload"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: templates/aircox_streamer/streamer.html:26
 | 
			
		||||
#: templates/aircox_streamer/streamer.html:27
 | 
			
		||||
msgid "Select a station"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
#: urls.py:9 views.py:9
 | 
			
		||||
msgid "Streamer Monitor"
 | 
			
		||||
msgstr ""
 | 
			
		||||
@ -12,6 +12,8 @@ class Connector:
 | 
			
		||||
    Received data can be parsed from list of `key=value` or JSON.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    socket_class = socket.socket
 | 
			
		||||
    """Socket class to instanciate on open."""
 | 
			
		||||
    socket = None
 | 
			
		||||
    """The socket."""
 | 
			
		||||
    address = None
 | 
			
		||||
@ -26,27 +28,42 @@ class Connector:
 | 
			
		||||
        if address:
 | 
			
		||||
            self.address = address
 | 
			
		||||
 | 
			
		||||
    def __enter__(self):
 | 
			
		||||
        r = self.open()
 | 
			
		||||
        if r == -1:
 | 
			
		||||
            raise RuntimeError("can not open socket.")
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
    def __exit__(self):
 | 
			
		||||
        self.close()
 | 
			
		||||
 | 
			
		||||
    def open(self):
 | 
			
		||||
        """Open connection.
 | 
			
		||||
 | 
			
		||||
        :return: 0 (success), 1 (already opened), -1 (failure)
 | 
			
		||||
        """
 | 
			
		||||
        if self.is_open:
 | 
			
		||||
            return
 | 
			
		||||
            return 1
 | 
			
		||||
 | 
			
		||||
        family = (
 | 
			
		||||
            socket.AF_UNIX if isinstance(self.address, str) else socket.AF_INET
 | 
			
		||||
        )
 | 
			
		||||
        try:
 | 
			
		||||
            self.socket = socket.socket(family, socket.SOCK_STREAM)
 | 
			
		||||
            self.socket = self.socket_class(family, socket.SOCK_STREAM)
 | 
			
		||||
            self.socket.connect(self.address)
 | 
			
		||||
            return 0
 | 
			
		||||
        except Exception:
 | 
			
		||||
            self.close()
 | 
			
		||||
            return -1
 | 
			
		||||
 | 
			
		||||
    def close(self):
 | 
			
		||||
        self.socket.close()
 | 
			
		||||
        self.socket = None
 | 
			
		||||
        if self.is_open:
 | 
			
		||||
            self.socket.close()
 | 
			
		||||
            self.socket = None
 | 
			
		||||
 | 
			
		||||
    # FIXME: return None on failed
 | 
			
		||||
    def send(self, *data, try_count=1, parse=False, parse_json=False):
 | 
			
		||||
        if self.open():
 | 
			
		||||
        if self.open() == -1:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        data = bytes("".join([str(d) for d in data]) + "\n", encoding="utf-8")
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										9
									
								
								aircox_streamer/controllers/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								aircox_streamer/controllers/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
# TODO: for the moment, update in station and program names do not update the
 | 
			
		||||
#       related fields.
 | 
			
		||||
 | 
			
		||||
from .base import Request
 | 
			
		||||
from .streamer import Streamer
 | 
			
		||||
from .sources import Source, PlaylistSource, QueueSource
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ("Request", "Streamer", "Source", "PlaylistSource", "QueueSource")
 | 
			
		||||
							
								
								
									
										90
									
								
								aircox_streamer/controllers/base.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										90
									
								
								aircox_streamer/controllers/base.py
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,90 @@
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
import tzlocal
 | 
			
		||||
from django.utils import timezone as tz
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = (
 | 
			
		||||
    "BaseMetadata",
 | 
			
		||||
    "Request",
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
local_tz = tzlocal.get_localzone()
 | 
			
		||||
logger = logging.getLogger("aircox")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# FIXME liquidsoap does not manage timezones -- we have to convert
 | 
			
		||||
#       'on_air' metadata we get from it into utc one in order to work
 | 
			
		||||
#       correctly.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseMetadata:
 | 
			
		||||
    """Base class for handling request metadata."""
 | 
			
		||||
 | 
			
		||||
    controller = None
 | 
			
		||||
    """Controller."""
 | 
			
		||||
    rid = None
 | 
			
		||||
    """Request id."""
 | 
			
		||||
    uri = None
 | 
			
		||||
    """Request uri."""
 | 
			
		||||
    status = None
 | 
			
		||||
    """Current playing status."""
 | 
			
		||||
    request_status = None
 | 
			
		||||
    """Requests' status."""
 | 
			
		||||
    air_time = None
 | 
			
		||||
    """Launch datetime."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, controller=None, rid=None, data=None):
 | 
			
		||||
        self.controller = controller
 | 
			
		||||
        self.rid = rid
 | 
			
		||||
        if data is not None:
 | 
			
		||||
            self.validate(data)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_playing(self):
 | 
			
		||||
        return self.status == "playing"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def status_verbose(self):
 | 
			
		||||
        return self.validate_status(self.status, True)
 | 
			
		||||
 | 
			
		||||
    def fetch(self):
 | 
			
		||||
        data = self.controller.send("request.metadata ", self.rid, parse=True)
 | 
			
		||||
        if data:
 | 
			
		||||
            self.validate(data)
 | 
			
		||||
 | 
			
		||||
    def validate_status(self, status, i18n=False):
 | 
			
		||||
        on_air = self.controller.source
 | 
			
		||||
        if (
 | 
			
		||||
            on_air
 | 
			
		||||
            and status == "playing"
 | 
			
		||||
            and (on_air == self or on_air.rid == self.rid)
 | 
			
		||||
        ):
 | 
			
		||||
            return _("playing") if i18n else "playing"
 | 
			
		||||
        elif status == "playing":
 | 
			
		||||
            return _("paused") if i18n else "paused"
 | 
			
		||||
        else:
 | 
			
		||||
            return _("stopped") if i18n else "stopped"
 | 
			
		||||
 | 
			
		||||
    def validate_air_time(self, air_time):
 | 
			
		||||
        if air_time:
 | 
			
		||||
            air_time = tz.datetime.strptime(air_time, "%Y/%m/%d %H:%M:%S")
 | 
			
		||||
            return local_tz.localize(air_time)
 | 
			
		||||
 | 
			
		||||
    def validate(self, data):
 | 
			
		||||
        """Validate provided data and set as attribute (must already be
 | 
			
		||||
        declared)"""
 | 
			
		||||
        for key, value in data.items():
 | 
			
		||||
            if hasattr(self, key) and not callable(getattr(self, key)):
 | 
			
		||||
                setattr(self, key, value)
 | 
			
		||||
        self.uri = data.get("initial_uri")
 | 
			
		||||
 | 
			
		||||
        self.air_time = self.validate_air_time(data.get("on_air"))
 | 
			
		||||
        self.status = self.validate_status(data.get("status"))
 | 
			
		||||
        self.request_status = data.get("status")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Request(BaseMetadata):
 | 
			
		||||
    title = None
 | 
			
		||||
    artist = None
 | 
			
		||||
@ -16,21 +16,13 @@ from aircox.utils import to_seconds
 | 
			
		||||
 | 
			
		||||
from .connector import Connector
 | 
			
		||||
 | 
			
		||||
__all__ = [
 | 
			
		||||
    "BaseMetadata",
 | 
			
		||||
    "Request",
 | 
			
		||||
    "Streamer",
 | 
			
		||||
 | 
			
		||||
__all__ = (
 | 
			
		||||
    "Source",
 | 
			
		||||
    "PlaylistSource",
 | 
			
		||||
    "QueueSource",
 | 
			
		||||
]
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
# TODO: for the moment, update in station and program names do not update the
 | 
			
		||||
#       related fields.
 | 
			
		||||
 | 
			
		||||
# FIXME liquidsoap does not manage timezones -- we have to convert
 | 
			
		||||
#       'on_air' metadata we get from it into utc one in order to work
 | 
			
		||||
#       correctly.
 | 
			
		||||
 | 
			
		||||
local_tz = tzlocal.get_localzone()
 | 
			
		||||
logger = logging.getLogger("aircox")
 | 
			
		||||
							
								
								
									
										192
									
								
								aircox_streamer/controllers/streamer.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										192
									
								
								aircox_streamer/controllers/streamer.py
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,192 @@
 | 
			
		||||
import atexit
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
import signal
 | 
			
		||||
import subprocess
 | 
			
		||||
 | 
			
		||||
import psutil
 | 
			
		||||
import tzlocal
 | 
			
		||||
from django.template.loader import render_to_string
 | 
			
		||||
 | 
			
		||||
from aircox.conf import settings
 | 
			
		||||
 | 
			
		||||
from ..connector import Connector
 | 
			
		||||
from .sources import PlaylistSource, QueueSource
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ("Streamer",)
 | 
			
		||||
 | 
			
		||||
local_tz = tzlocal.get_localzone()
 | 
			
		||||
logger = logging.getLogger("aircox")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Streamer:
 | 
			
		||||
    connector = None
 | 
			
		||||
    process = None
 | 
			
		||||
 | 
			
		||||
    station = None
 | 
			
		||||
    template_name = "aircox_streamer/scripts/station.liq"
 | 
			
		||||
    path = None
 | 
			
		||||
    """Config path."""
 | 
			
		||||
    sources = None
 | 
			
		||||
    """List of all monitored sources."""
 | 
			
		||||
    source = None
 | 
			
		||||
    """Current source being played on air."""
 | 
			
		||||
    # note: we disable on_air rids since we don't have use of it for the
 | 
			
		||||
    # moment
 | 
			
		||||
    # on_air = None
 | 
			
		||||
    # """ On-air request ids (rid) """
 | 
			
		||||
    inputs = None
 | 
			
		||||
    """Queryset to input ports."""
 | 
			
		||||
    outputs = None
 | 
			
		||||
    """Queryset to output ports."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, station, connector=None):
 | 
			
		||||
        self.station = station
 | 
			
		||||
        self.inputs = self.station.port_set.active().input()
 | 
			
		||||
        self.outputs = self.station.port_set.active().output()
 | 
			
		||||
 | 
			
		||||
        self.id = self.station.slug.replace("-", "_")
 | 
			
		||||
        self.path = os.path.join(station.path, "station.liq")
 | 
			
		||||
        self.connector = Connector(os.path.join(station.path, "station.sock"))
 | 
			
		||||
        self.init_sources()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def socket_path(self):
 | 
			
		||||
        """Path to Unix socket file."""
 | 
			
		||||
        return self.connector.address
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_ready(self):
 | 
			
		||||
        """If external program is ready to use, returns True."""
 | 
			
		||||
        return self.send("list") != ""
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_running(self):
 | 
			
		||||
        """True if holds a running process."""
 | 
			
		||||
        if self.process is None:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        returncode = self.process.poll()
 | 
			
		||||
        if returncode is None:
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        self.process = None
 | 
			
		||||
        logger.debug("process died with return code %s" % returncode)
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def playlists(self):
 | 
			
		||||
        return (s for s in self.sources if isinstance(s, PlaylistSource))
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def queues(self):
 | 
			
		||||
        return (s for s in self.sources if isinstance(s, QueueSource))
 | 
			
		||||
 | 
			
		||||
    # Sources and config ###############################################
 | 
			
		||||
    def send(self, *args, **kwargs):
 | 
			
		||||
        return self.connector.send(*args, **kwargs) or ""
 | 
			
		||||
 | 
			
		||||
    def init_sources(self):
 | 
			
		||||
        streams = self.station.program_set.filter(stream__isnull=False)
 | 
			
		||||
        self.dealer = QueueSource(self, "dealer")
 | 
			
		||||
        self.sources = [self.dealer] + [
 | 
			
		||||
            PlaylistSource(self, program=program) for program in streams
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    def make_config(self):
 | 
			
		||||
        """Make configuration files and directory (and sync sources)"""
 | 
			
		||||
        data = render_to_string(
 | 
			
		||||
            self.template_name,
 | 
			
		||||
            {
 | 
			
		||||
                "station": self.station,
 | 
			
		||||
                "streamer": self,
 | 
			
		||||
                "settings": settings,
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        data = re.sub("[\t ]+\n", "\n", data)
 | 
			
		||||
        data = re.sub("\n{3,}", "\n\n", data)
 | 
			
		||||
 | 
			
		||||
        os.makedirs(os.path.dirname(self.path), exist_ok=True)
 | 
			
		||||
        with open(self.path, "w+") as file:
 | 
			
		||||
            file.write(data)
 | 
			
		||||
 | 
			
		||||
        self.sync()
 | 
			
		||||
 | 
			
		||||
    def sync(self):
 | 
			
		||||
        """Sync all sources."""
 | 
			
		||||
        for source in self.sources:
 | 
			
		||||
            source.sync()
 | 
			
		||||
 | 
			
		||||
    def fetch(self):
 | 
			
		||||
        """Fetch data from liquidsoap."""
 | 
			
		||||
        for source in self.sources:
 | 
			
		||||
            source.fetch()
 | 
			
		||||
 | 
			
		||||
        # request.on_air is not ordered: we need to do it manually
 | 
			
		||||
        self.source = next(
 | 
			
		||||
            iter(
 | 
			
		||||
                sorted(
 | 
			
		||||
                    (
 | 
			
		||||
                        source
 | 
			
		||||
                        for source in self.sources
 | 
			
		||||
                        if source.request_status == "playing"
 | 
			
		||||
                        and source.air_time
 | 
			
		||||
                    ),
 | 
			
		||||
                    key=lambda o: o.air_time,
 | 
			
		||||
                    reverse=True,
 | 
			
		||||
                )
 | 
			
		||||
            ),
 | 
			
		||||
            None,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Process ##########################################################
 | 
			
		||||
    def get_process_args(self):
 | 
			
		||||
        return ["liquidsoap", "-v", self.path]
 | 
			
		||||
 | 
			
		||||
    def check_zombie_process(self):
 | 
			
		||||
        if not os.path.exists(self.socket_path):
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        conns = [
 | 
			
		||||
            conn
 | 
			
		||||
            for conn in psutil.net_connections(kind="unix")
 | 
			
		||||
            if conn.laddr == self.socket_path
 | 
			
		||||
        ]
 | 
			
		||||
        for conn in conns:
 | 
			
		||||
            if conn.pid is not None:
 | 
			
		||||
                os.kill(conn.pid, signal.SIGKILL)
 | 
			
		||||
 | 
			
		||||
    def run_process(self):
 | 
			
		||||
        """Execute the external application with corresponding informations.
 | 
			
		||||
 | 
			
		||||
        This function must make sure that all needed files have been
 | 
			
		||||
        generated.
 | 
			
		||||
        """
 | 
			
		||||
        if self.process:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        args = self.get_process_args()
 | 
			
		||||
        if not args:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        self.check_zombie_process()
 | 
			
		||||
        self.process = subprocess.Popen(args, stderr=subprocess.STDOUT)
 | 
			
		||||
        atexit.register(lambda: self.kill_process())
 | 
			
		||||
 | 
			
		||||
    def kill_process(self):
 | 
			
		||||
        if self.process:
 | 
			
		||||
            logger.debug(
 | 
			
		||||
                "kill process %s: %s",
 | 
			
		||||
                self.process.pid,
 | 
			
		||||
                " ".join(self.get_process_args()),
 | 
			
		||||
            )
 | 
			
		||||
            self.process.kill()
 | 
			
		||||
            self.process = None
 | 
			
		||||
 | 
			
		||||
    def wait_process(self):
 | 
			
		||||
        """Wait for the process to terminate if there is a process."""
 | 
			
		||||
        if self.process:
 | 
			
		||||
            self.process.wait()
 | 
			
		||||
            self.process = None
 | 
			
		||||
							
								
								
									
										51
									
								
								aircox_streamer/tests/conftest.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								aircox_streamer/tests/conftest.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
from aircox_streamer import connector
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FakeSocket:
 | 
			
		||||
    FAILING_ADDRESS = -1
 | 
			
		||||
    """Connect with this address fails."""
 | 
			
		||||
 | 
			
		||||
    family, type, address = None, None, None
 | 
			
		||||
    sent_data = None
 | 
			
		||||
    """List of data that have been `send[all]`"""
 | 
			
		||||
    recv_data = None
 | 
			
		||||
    """Response data to return on recv."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, family, type):
 | 
			
		||||
        self.family = family
 | 
			
		||||
        self.type = type
 | 
			
		||||
        self.sent_data = []
 | 
			
		||||
 | 
			
		||||
    def connect(self, address):
 | 
			
		||||
        if address == self.FAILING_ADDRESS:
 | 
			
		||||
            raise RuntimeError("invalid connection")
 | 
			
		||||
        self.address = address
 | 
			
		||||
 | 
			
		||||
    def close(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def sendall(self, data):
 | 
			
		||||
        self.sent_data.append(data)
 | 
			
		||||
 | 
			
		||||
    def recv(self, count):
 | 
			
		||||
        data = self.recv_data[:count]
 | 
			
		||||
        self.recv_data = data[count:]
 | 
			
		||||
        return data.encode("utf-8") if isinstance(data, str) else data
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Connector(connector.Connector):
 | 
			
		||||
    socket_class = FakeSocket
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def connector(request):
 | 
			
		||||
    obj = Connector("test")
 | 
			
		||||
    yield obj
 | 
			
		||||
    obj.close()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture
 | 
			
		||||
def fail_connector():
 | 
			
		||||
    return Connector(FakeSocket.FAILING_ADDRESS)
 | 
			
		||||
							
								
								
									
										67
									
								
								aircox_streamer/tests/test_connector.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								aircox_streamer/tests/test_connector.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,67 @@
 | 
			
		||||
import json
 | 
			
		||||
import socket
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
from aircox_streamer.connector import Connector
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestConnector:
 | 
			
		||||
    payload = "non_value_info\n" 'a="value_1"\n' 'b="value_b"\n' "END"
 | 
			
		||||
    """Test payload."""
 | 
			
		||||
    payload_data = {"a": "value_1", "b": "value_b"}
 | 
			
		||||
    """Resulting data of payload."""
 | 
			
		||||
 | 
			
		||||
    def test_open(self, connector):
 | 
			
		||||
        assert connector.open() == 0
 | 
			
		||||
        assert connector.is_open
 | 
			
		||||
        assert connector.socket.family == socket.AF_UNIX
 | 
			
		||||
        assert connector.socket.type == socket.SOCK_STREAM
 | 
			
		||||
        assert connector.socket.address == "test"
 | 
			
		||||
        connector.close()
 | 
			
		||||
 | 
			
		||||
    def test_open_af_inet(self):
 | 
			
		||||
        address = ("test", 30)
 | 
			
		||||
        connector = Connector(address)
 | 
			
		||||
        assert connector.open() == 0
 | 
			
		||||
        assert connector.is_open
 | 
			
		||||
        assert connector.socket.family == socket.AF_INET
 | 
			
		||||
        assert connector.socket.type == socket.SOCK_STREAM
 | 
			
		||||
        assert connector.socket.address == address
 | 
			
		||||
 | 
			
		||||
    def test_open_is_already_open(self, connector):
 | 
			
		||||
        connector.open()
 | 
			
		||||
        assert connector.open() == 1
 | 
			
		||||
 | 
			
		||||
    def test_open_failure(self, fail_connector):
 | 
			
		||||
        assert fail_connector.open() == -1
 | 
			
		||||
        assert fail_connector.socket is None  # close() called
 | 
			
		||||
 | 
			
		||||
    def test_close(self, connector):
 | 
			
		||||
        connector.open()
 | 
			
		||||
        assert connector.socket is not None
 | 
			
		||||
        connector.close()
 | 
			
		||||
        assert connector.socket is None
 | 
			
		||||
 | 
			
		||||
    def test_send(self, connector):
 | 
			
		||||
        connector.socket.recv_data = self.payload
 | 
			
		||||
        result = connector.send("fake_action", parse=True)
 | 
			
		||||
        assert result == self.payload_data
 | 
			
		||||
 | 
			
		||||
    def test_send_open_failure(self, fail_connector):
 | 
			
		||||
        assert fail_connector.send("fake_action", parse=True) is None
 | 
			
		||||
 | 
			
		||||
    def test_parse(self, connector):
 | 
			
		||||
        result = connector.parse(self.payload)
 | 
			
		||||
        assert result == self.payload_data
 | 
			
		||||
 | 
			
		||||
    def test_parse_json(self, connector):
 | 
			
		||||
        # include case where json string is surrounded by '"'
 | 
			
		||||
        dumps = '"' + json.dumps(self.payload_data) + '"'
 | 
			
		||||
        result = connector.parse_json(dumps)
 | 
			
		||||
        assert result == self.payload_data
 | 
			
		||||
 | 
			
		||||
    def test_parse_json_empty_value(self, connector):
 | 
			
		||||
        assert connector.parse_json('""') is None
 | 
			
		||||
 | 
			
		||||
    def test_parse_json_failure(self, connector):
 | 
			
		||||
        assert connector.parse_json("-- invalid json string --") is None
 | 
			
		||||
							
								
								
									
										35
									
								
								aircox_streamer/tests/test_controllers_base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								aircox_streamer/tests/test_controllers_base.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
			
		||||
# import pytest
 | 
			
		||||
 | 
			
		||||
# from aircox_streamer import controllers
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestBaseMetaData:
 | 
			
		||||
    def test_is_playing(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_status_verbose(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_fetch(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_fetch_no_data(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_validate_status_playing(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_validate_status_paused(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_validate_status_stopped(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_validate_air_time(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_validate_air_time_none(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_validate(self):
 | 
			
		||||
        pass
 | 
			
		||||
							
								
								
									
										51
									
								
								aircox_streamer/tests/test_controllers_sources.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								aircox_streamer/tests/test_controllers_sources.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
			
		||||
# import pytest
 | 
			
		||||
 | 
			
		||||
# from aircox_streamer import controllers
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestSource:
 | 
			
		||||
    def test_station(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_sync(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_fetch(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_skip(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_restart(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_seek(self, n):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestPlaylistSource:
 | 
			
		||||
    def test_get_sound_queryset(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_get_playlist(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_write_playlist(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_stream(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_sync(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestQueueSource:
 | 
			
		||||
    def test_push(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_fetch(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_requests(self):
 | 
			
		||||
        pass
 | 
			
		||||
							
								
								
									
										50
									
								
								aircox_streamer/tests/test_controllers_streamer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								aircox_streamer/tests/test_controllers_streamer.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
			
		||||
# import pytest
 | 
			
		||||
 | 
			
		||||
# from aircox_streamer import controllers
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestStreamer:
 | 
			
		||||
    def test_socket_path(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_is_ready(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_is_running(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_playlists(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_queues(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_send(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_init_sources(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_make_config(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_sync(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_fetch(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_get_process_args(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_check_zombie_process(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_run_process(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_kill_process(self):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def test_wait_process(self):
 | 
			
		||||
        pass
 | 
			
		||||
		Reference in New Issue
	
	Block a user