forked from rc/aircox
		
	start app controllers that aims to replace liquidsoap on long term, and be more generic and reusable
This commit is contained in:
		
							
								
								
									
										91
									
								
								controllers/plugins/connector.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								controllers/plugins/connector.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,91 @@
 | 
			
		||||
import os
 | 
			
		||||
import socket
 | 
			
		||||
import re
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Connector:
 | 
			
		||||
    """
 | 
			
		||||
    Simple connector class that retrieve/send data through a unix
 | 
			
		||||
    domain socket file or a TCP/IP connection
 | 
			
		||||
 | 
			
		||||
    It is able to parse list of `key=value`, and JSON data.
 | 
			
		||||
    """
 | 
			
		||||
    __socket = None
 | 
			
		||||
    __available = False
 | 
			
		||||
    address = None
 | 
			
		||||
    """
 | 
			
		||||
    a string to the unix domain socket file, or a tuple (host, port) for
 | 
			
		||||
    TCP/IP connection
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def available(self):
 | 
			
		||||
        return self.__available
 | 
			
		||||
 | 
			
		||||
    def __init__(self, address = None):
 | 
			
		||||
        if address:
 | 
			
		||||
            self.address = address
 | 
			
		||||
 | 
			
		||||
    def open(self):
 | 
			
		||||
        if self.__available:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            family = socket.AF_INET if type(self.address) in (tuple, list) else \
 | 
			
		||||
                     socket.AF_UNIX
 | 
			
		||||
            self.__socket = socket.socket(family, socket.SOCK_STREAM)
 | 
			
		||||
            self.__socket.connect(self.address)
 | 
			
		||||
            self.__available = True
 | 
			
		||||
        except:
 | 
			
		||||
            self.__available = False
 | 
			
		||||
            return -1
 | 
			
		||||
 | 
			
		||||
    def send(self, *data, try_count = 1, parse = False, parse_json = False):
 | 
			
		||||
        if self.open():
 | 
			
		||||
            return ''
 | 
			
		||||
        data = bytes(''.join([str(d) for d in data]) + '\n', encoding='utf-8')
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            reg = re.compile(r'(.*)\s+END\s*$')
 | 
			
		||||
            self.__socket.sendall(data)
 | 
			
		||||
            data = ''
 | 
			
		||||
            while not reg.search(data):
 | 
			
		||||
                data += self.__socket.recv(1024).decode('utf-8')
 | 
			
		||||
 | 
			
		||||
            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):
 | 
			
		||||
        string = string.split('\n')
 | 
			
		||||
        data = {}
 | 
			
		||||
        for line in string:
 | 
			
		||||
            line = re.search(r'(?P<key>[^=]+)="?(?P<value>([^"]|\\")+)"?', line)
 | 
			
		||||
            if not line:
 | 
			
		||||
                continue
 | 
			
		||||
            line = line.groupdict()
 | 
			
		||||
            data[line['key']] = line['value']
 | 
			
		||||
        return data
 | 
			
		||||
 | 
			
		||||
    def parse_json(self, string):
 | 
			
		||||
        try:
 | 
			
		||||
            if string[0] == '"' and string[-1] == '"':
 | 
			
		||||
                string = string[1:-1]
 | 
			
		||||
            return json.loads(string) if string else None
 | 
			
		||||
        except:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										105
									
								
								controllers/plugins/liquidsoap.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								controllers/plugins/liquidsoap.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,105 @@
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
import aircox.controllers.plugins.plugins as plugins
 | 
			
		||||
from aircox.controllers.plugins.connector import Connector
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LiquidSoap(plugins.Plugin):
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def init_station(station):
 | 
			
		||||
        return StationController(station = station)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def init_source(source):
 | 
			
		||||
        return SourceController(source = source)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StationController(plugins.StationController):
 | 
			
		||||
    template_name = 'aircox/controllers/liquidsoap.liq'
 | 
			
		||||
    socket_path = ''
 | 
			
		||||
    connector = None
 | 
			
		||||
 | 
			
		||||
    def __init__(self, station, **kwargs):
 | 
			
		||||
        super().__init__(
 | 
			
		||||
            station = station,
 | 
			
		||||
            path = os.path.join(station.path, 'station.liq'),
 | 
			
		||||
            socket_path = os.path.join(station.path, 'station.sock'),
 | 
			
		||||
            **kwargs
 | 
			
		||||
        )
 | 
			
		||||
        self.connector = Connector(self.socket_path)
 | 
			
		||||
 | 
			
		||||
    def _send(self, *args, **kwargs):
 | 
			
		||||
        self.connector.send(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def fetch(self):
 | 
			
		||||
        super().fetch()
 | 
			
		||||
 | 
			
		||||
        data = self._send('request.on_air')
 | 
			
		||||
        if not data:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        data = self._send('request.metadata', data, parse = True)
 | 
			
		||||
        if not data:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        self.current_sound = data.get('initial_uri')
 | 
			
		||||
        # FIXME: point to the Source object
 | 
			
		||||
        self.current_source = data.get('source')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SourceController(plugins.SourceController):
 | 
			
		||||
    connector = None
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(**kwargs)
 | 
			
		||||
        self.connector = self.source.station.controller.connector
 | 
			
		||||
 | 
			
		||||
    def _send(self, *args, **kwargs):
 | 
			
		||||
        self.connector.send(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def active(self):
 | 
			
		||||
        return self._send('var.get ', self.source.slug, '_active') == 'true'
 | 
			
		||||
 | 
			
		||||
    @active.setter
 | 
			
		||||
    def active(self, value):
 | 
			
		||||
        return self._send('var.set ', self.source.slug, '_active', '=',
 | 
			
		||||
                           'true' if value else 'false')
 | 
			
		||||
 | 
			
		||||
    def skip(self):
 | 
			
		||||
        """
 | 
			
		||||
        Skip a given source. If no source, use master.
 | 
			
		||||
        """
 | 
			
		||||
        self._send(self.source.slug, '.skip')
 | 
			
		||||
 | 
			
		||||
    def fetch(self):
 | 
			
		||||
        data = self._send(self.source.slug, '.get', parse = True)
 | 
			
		||||
        if not data:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # FIXME: still usefull? originally tested only if there ass self.program
 | 
			
		||||
        source = data.get('source') or ''
 | 
			
		||||
        if not source.startswith(self.id):
 | 
			
		||||
            return
 | 
			
		||||
        self.current_sound = data.get('initial_uri')
 | 
			
		||||
 | 
			
		||||
    def stream(self):
 | 
			
		||||
        """
 | 
			
		||||
        Return a dict with stream info for a Stream program, or None if there
 | 
			
		||||
        is not. Used in the template.
 | 
			
		||||
        """
 | 
			
		||||
        stream = self.source.stream
 | 
			
		||||
        if not stream or (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 0
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										198
									
								
								controllers/plugins/plugins.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								controllers/plugins/plugins.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,198 @@
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
from django.template.loader import render_to_string
 | 
			
		||||
 | 
			
		||||
class Plugins(type):
 | 
			
		||||
    registry = {}
 | 
			
		||||
 | 
			
		||||
    def __new__(cls, name, bases, attrs):
 | 
			
		||||
        cl = super().__new__(cls, name, bases, attrs)
 | 
			
		||||
        if name != 'Plugin':
 | 
			
		||||
            if not cl.name:
 | 
			
		||||
                cl.name = name.lower()
 | 
			
		||||
            cls.registry[cl.name] = cl
 | 
			
		||||
        return cl
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def discover(cls):
 | 
			
		||||
        """
 | 
			
		||||
        Discover plugins -- needed because of the import traps
 | 
			
		||||
        """
 | 
			
		||||
        import aircox.controllers.plugins.liquidsoap
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Plugin(metaclass=Plugins):
 | 
			
		||||
    name = ''
 | 
			
		||||
 | 
			
		||||
    def init_station(self, station):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def init_source(self, source):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StationController:
 | 
			
		||||
    """
 | 
			
		||||
    Controller of a Station.
 | 
			
		||||
    """
 | 
			
		||||
    station = None
 | 
			
		||||
    """
 | 
			
		||||
    Related station
 | 
			
		||||
    """
 | 
			
		||||
    template_name = ''
 | 
			
		||||
    """
 | 
			
		||||
    If set, use this template in order to generated the configuration
 | 
			
		||||
    file in self.path file
 | 
			
		||||
    """
 | 
			
		||||
    path = None
 | 
			
		||||
    """
 | 
			
		||||
    Path of the configuration file.
 | 
			
		||||
    """
 | 
			
		||||
    current_sound = ''
 | 
			
		||||
    """
 | 
			
		||||
    Current sound being played (retrieved by fetch)
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def id(self):
 | 
			
		||||
        return '{station.slug}_{station.pk}'.format(station = self.station)
 | 
			
		||||
 | 
			
		||||
    # TODO: add function to launch external program?
 | 
			
		||||
 | 
			
		||||
    def __init__(self, **kwargs):
 | 
			
		||||
        self.__dict__.update(kwargs)
 | 
			
		||||
 | 
			
		||||
    def fetch(self):
 | 
			
		||||
        """
 | 
			
		||||
        Fetch data of the children and so on
 | 
			
		||||
 | 
			
		||||
        The base function just execute the function of all children
 | 
			
		||||
        sources. The plugin must implement the other extra part
 | 
			
		||||
        """
 | 
			
		||||
        sources = self.station.get_sources()
 | 
			
		||||
        for source in sources:
 | 
			
		||||
            if source.controller:
 | 
			
		||||
                source.controller.fetch()
 | 
			
		||||
 | 
			
		||||
    def push(self, config = True):
 | 
			
		||||
        """
 | 
			
		||||
        Update configuration and children's info.
 | 
			
		||||
 | 
			
		||||
        The base function just execute the function of all children
 | 
			
		||||
        sources. The plugin must implement the other extra part
 | 
			
		||||
        """
 | 
			
		||||
        sources = self.station.get_sources()
 | 
			
		||||
        for source in sources:
 | 
			
		||||
            source.prepare()
 | 
			
		||||
            if source.controller:
 | 
			
		||||
                source.controller.push()
 | 
			
		||||
 | 
			
		||||
        if config and self.path and self.template_name:
 | 
			
		||||
            import aircox.controllers.settings as settings
 | 
			
		||||
 | 
			
		||||
            data = render_to_string(self.template_name, {
 | 
			
		||||
                'station': self.station,
 | 
			
		||||
                '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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def skip(self):
 | 
			
		||||
        """
 | 
			
		||||
        Skip the current sound on the station
 | 
			
		||||
        """
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SourceController:
 | 
			
		||||
    """
 | 
			
		||||
    Controller of a Source. Value are usually updated directly on the
 | 
			
		||||
    external side.
 | 
			
		||||
    """
 | 
			
		||||
    source = None
 | 
			
		||||
    """
 | 
			
		||||
    Related source
 | 
			
		||||
    """
 | 
			
		||||
    path = ''
 | 
			
		||||
    """
 | 
			
		||||
    Path to the Source's playlist file. Optional.
 | 
			
		||||
    """
 | 
			
		||||
    active = True
 | 
			
		||||
    """
 | 
			
		||||
    Source is available. May be different from the containing Source,
 | 
			
		||||
    e.g. dealer and liquidsoap.
 | 
			
		||||
    """
 | 
			
		||||
    current_sound = ''
 | 
			
		||||
    """
 | 
			
		||||
    Current sound being played (retrieved by fetch)
 | 
			
		||||
    """
 | 
			
		||||
    current_source = None
 | 
			
		||||
    """
 | 
			
		||||
    Current source being responsible of the current sound
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def id(self):
 | 
			
		||||
        return '{source.station.slug}_{source.slug}'.format(source = self.source)
 | 
			
		||||
 | 
			
		||||
    __playlist = None
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def playlist(self):
 | 
			
		||||
        """
 | 
			
		||||
        Current playlist on the Source, list of paths to play
 | 
			
		||||
        """
 | 
			
		||||
        return self.__playlist
 | 
			
		||||
 | 
			
		||||
    @playlist.setter
 | 
			
		||||
    def playlist(self, value):
 | 
			
		||||
        self.__playlist = value
 | 
			
		||||
        self.push()
 | 
			
		||||
 | 
			
		||||
    def __init__(self, **kwargs):
 | 
			
		||||
        self.__dict__.update(kwargs)
 | 
			
		||||
        self.__playlist = []
 | 
			
		||||
        if not self.path:
 | 
			
		||||
            self.path = os.path.join(self.source.station.path,
 | 
			
		||||
                                     self.source.slug + '.m3u')
 | 
			
		||||
 | 
			
		||||
    def skip(self):
 | 
			
		||||
        """
 | 
			
		||||
        Skip the current sound in the source
 | 
			
		||||
        """
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def fetch(self):
 | 
			
		||||
        """
 | 
			
		||||
        Get the source information
 | 
			
		||||
        """
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def push(self):
 | 
			
		||||
        """
 | 
			
		||||
        Update data relative to the source on the external program.
 | 
			
		||||
        By default write the playlist.
 | 
			
		||||
        """
 | 
			
		||||
        os.makedirs(os.path.dirname(self.path), exist_ok = True)
 | 
			
		||||
        with open(self.path, 'w') as file:
 | 
			
		||||
            file.write('\n'.join(self.playlist or []))
 | 
			
		||||
 | 
			
		||||
    def activate(self, value = True):
 | 
			
		||||
        """
 | 
			
		||||
        Activate/Deactivate current source. May be different from the
 | 
			
		||||
        containing Source.
 | 
			
		||||
        """
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Monitor:
 | 
			
		||||
    station = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user