coherence.backends.buzztard_control — Cohen3 0.8.0 documentation

# Source code for coherence.backends.buzztard_control

    # Licensed under the MIT license
    # http://opensource.org/licenses/mit-license.php

    # Copyright 2007, Frank Scholz <coherence@beebits.net>
    # Copyright 2018, Pol Canelles <canellestudi@gmail.com>

    from twisted.internet import protocol
    from twisted.internet.task import LoopingCall
    from twisted.protocols.basic import LineReceiver
    from twisted.python import failure

    from eventdispatcher import EventDispatcher

    from coherence import log
    from coherence.backend import Backend
    from coherence.upnp.core import DIDLLite
    from coherence.upnp.core.DIDLLite import classChooser, Container, Resource
    from coherence.upnp.core.soap_service import errorCode

    class BzClient(LineReceiver, EventDispatcher, log.LogAble):
        '''

        .. versionchanged:: 0.9.0

            * Migrated from louie/dispatcher to EventDispatcher
            * The emitted events changed:

                - Buzztard.Response.flush => response_flush
                - Buzztard.Response.event => response_event
                - Buzztard.Response.volume => response_volume
                - Buzztard.Response.mute=> response_mute
                - Buzztard.Response.repeat => response_repeat
                - Buzztard.Response.browse => response_browse
        '''
        logCategory = 'buzztard_client'
        factory = None

        def __init__(self, *args, **kwargs):
            EventDispatcher.__init__(self)
            log.LogAble.__init__(self)
            self.register_event(
                'response_flush',
                'response_event',
                'response_volume',
                'response_mute',
                'response_repeat',
                'response_browse',
            )

        def connectionMade(self):
            self.info('connected to Buzztard')
            if self.factory is None:
                self.error('Cannot access to BzClient.factory '
                           'property...cancelled')
                return False
            self.factory.clientReady(self)

        def lineReceived(self, line):
            self.debug(f'received: {line}')

            if line == 'flush':
                self.dispatch_event('response_flush')
            elif line.find('event') == 0:
                self.dispatch_event('response_event', line)
            elif line.find('volume') == 0:
                self.dispatch_event('response_volume', line)
            elif line.find('mute') == 0:
                self.dispatch_event('response_mute', line)
            elif line.find('repeat') == 0:
                self.dispatch_event('response_repeat', line)
            elif line.find('playlist') == 0:
                self.dispatch_event('response_browse', line)

    class BzFactory(protocol.ClientFactory, log.LogAble):
        '''
        .. versionchanged:: 0.9.0
            Migrated from louie/dispatcher to EventDispatcher
        '''
        logCategory = 'buzztard_factory'
        protocol = BzClient

        def __init__(self, backend):
            log.LogAble.__init__(self)
            self.backend = backend

        def clientConnectionFailed(self, connector, reason):
            self.error(f'connection failed: {reason.getErrorMessage()}')

        def clientConnectionLost(self, connector, reason):
            self.error(f'connection lost: {reason.getErrorMessage()}')

        def startFactory(self):
            self.messageQueue = []
            self.clientInstance = None

        def clientReady(self, instance):
            self.info('clientReady')
            self.backend.init_completed = True
            self.clientInstance = instance
            for msg in self.messageQueue:
                self.sendMessage(msg)

        def sendMessage(self, msg):
            if self.clientInstance is not None:
                self.clientInstance.sendLine(msg)
            else:
                self.messageQueue.append(msg)

        def rebrowse(self):
            self.backend.clear()
            self.browse()

        def browse(self):
            self.sendMessage('browse')

    class BzConnection(log.LogAble):
        '''
        A singleton class

        .. versionchanged:: 0.9.0
            Migrated from louie/dispatcher to EventDispatcher
        '''
        logCategory = 'buzztard_connection'
        connection = None

        def __init__(self, backend=None, host='localhost', port=7654):
            log.LogAble.__init__(self)
            self.debug('BzConnection __init__')

        def __new__(cls, *args, **kwargs):
            print(cls, 'BzConnection __new__')
            obj = getattr(cls, '_instance_', None)
            if obj is not None:
                if kwargs['backend']:
                    kwargs['backend'].init_completed = True
                return obj
            else:
                obj = super(BzConnection, cls).__new__(cls)
                cls._instance_ = obj
                obj.connection = BzFactory(kwargs['backend'])
                reactor.connectTCP(kwargs['host'], kwargs['port'], obj.connection)
                return obj

    class BuzztardItem(log.LogAble):
        logCategory = 'buzztard_item'

        def __init__(self, id, name, parent, mimetype,
                     urlbase, host, update=False):
            log.LogAble.__init__(self)
            self.id = id
            self.name = name
            self.mimetype = mimetype

            self.parent = parent
            if parent:
                parent.add_child(self, update=update)

            if parent is None:
                parent_id = -1
            else:
                parent_id = parent.get_id()

            UPnPClass = classChooser(
                mimetype, sub='music')  # FIXME: this is stupid
            self.item = UPnPClass(id, parent_id, self.name)
            self.child_count = 0
            self.children = []

            if len(urlbase) and urlbase[-1] != '/':
                urlbase += '/'

            # self.url = urlbase + str(self.id)
            self.url = self.name

            if self.mimetype == 'directory':
                self.update_id = 0
            else:
                res = Resource(self.url, f'internal:{host}:{self.mimetype}:*')
                res.size = None
                self.item.res.append(res)
                self.item.artist = self.parent.name

        def __del__(self):
            self.debug(f'BuzztardItem __del__ {self.id} {self.name}')
            pass

        def remove(self, store):
            self.debug(f'BuzztardItem remove {self.id} {self.name} {self.parent}')
            while len(self.children) > 0:
                child = self.children.pop()
                self.remove_child(child)
                del store[int(child.id)]

            if self.parent:
                self.parent.remove_child(self)

            del store[int(self.id)]
            del self.item
            del self

        def add_child(self, child, update=False):
            self.children.append(child)
            self.child_count += 1
            if isinstance(self.item, Container):
                if self.item.childCount is None:
                    self.item.childCount = 0
                self.item.childCount += 1
            if update:
                self.update_id += 1

        def remove_child(self, child):
            self.debug(
                f'remove_from {self.id:d} ({self.get_name()}) '
                f'child {child.id:d} ({child.get_name()})')
            if child in self.children:
                self.child_count -= 1
                if isinstance(self.item, Container):
                    self.item.childCount -= 1
                self.children.remove(child)
                self.update_id += 1

        def get_children(self, start=0, request_count=0):
            if request_count == 0:
                return self.children[start:]
            else:
                return self.children[start:request_count]

        def get_child_count(self):
            return self.child_count

        def get_id(self):
            return self.id

        def get_update_id(self):
            if hasattr(self, 'update_id'):
                return self.update_id
            else:
                return None

        def get_path(self):
            return self.url

        def get_name(self):
            return self.name

        def get_parent(self):
            return self.parent

        def get_item(self):
            return self.item

        def get_xml(self):
            return self.item.toString()

        def __repr__(self):
            if self.parent is None:
                parent = 'root'
            else:
                parent = str(self.parent.get_id())
            return 'id: ' + str(
                self.id) + '/' + self.name + '/' + parent + ' ' + str(
                self.child_count) + ' @ ' + self.url

    class BuzztardStore(Backend):
        '''
        .. versionchanged:: 0.9.0

            * Migrated from louie/dispatcher to EventDispatcher
            * Introduced :class:`~coherence.backend.Backend`'s inheritance
        '''
        logCategory = 'buzztard_store'
        implements = ['MediaServer']

        def __init__(self, server, **kwargs):
            Backend.__init__(self, server, **kwargs)
            self.next_id = 1000
            self.config = kwargs
            self.name = kwargs.get('name', 'Buzztard')

            self.urlbase = kwargs.get('urlbase', '')
            if (len(self.urlbase) > 0 and
                    self.urlbase[len(self.urlbase) - 1] != '/'):
                self.urlbase += '/'

            self.host = kwargs.get('host', '127.0.0.1')
            self.port = int(kwargs.get('port', 7654))

            self.server = server
            self.update_id = 0
            self.store = {}
            self.parent = None

            self.buzztard = BzConnection(backend=self, host=self.host,
                                         port=self.port)

        def __repr__(self):
            return str(self.__class__).split('.')[-1]

        def add_content(self, line):
            data = line.split('|')[1:]
            parent = self.append(data[0], 'directory', self.parent)
            i = 0
            for label in data[1:]:
                self.append(':'.join((label, str(i))), 'audio/mpeg', parent)
                i += 1

        def append(self, name, mimetype, parent):

            id = self.getnextID()
            update = False
            if hasattr(self, 'update_id'):
                update = True

            self.store[id] = BuzztardItem(id, name, parent, mimetype,
                                          self.urlbase, self.host, update=update)
            if hasattr(self, 'update_id'):
                self.update_id += 1
                if self.server:
                    self.server.content_directory_server.set_variable(
                        0, 'SystemUpdateID', self.update_id)
                if parent:
                    # value = '%d,%d' % (parent.get_id(),parent_get_update_id())
                    value = (parent.get_id(), parent.get_update_id())
                    if self.server:
                        self.server.content_directory_server.set_variable(
                            0, 'ContainerUpdateIDs', value)

            if mimetype == 'directory':
                return self.store[id]

            return None

        def remove(self, id):
            item = self.store[int(id)]
            parent = item.get_parent()
            item.remove(self.store)
            try:
                del self.store[int(id)]
            except (ValueError, KeyError):
                pass
            if hasattr(self, 'update_id'):
                self.update_id += 1
                if self.server:
                    self.server.content_directory_server.set_variable(
                        0, 'SystemUpdateID', self.update_id)
                value = (parent.get_id(), parent.get_update_id())
                if self.server:
                    self.server.content_directory_server.set_variable(
                        0, 'ContainerUpdateIDs', value)

        def clear(self):
            for item in self.get_by_id(1000).get_children():
                self.remove(item.get_id())
            self.buzztard.connection.browse()

        def len(self):
            return len(self.store)

        def get_by_id(self, id):
            id = int(id)
            if id == 0:
                id = 1000
            try:
                return self.store[id]
            except KeyError:
                return None

        def getnextID(self):
            ret = self.next_id
            self.next_id += 1
            return ret

        def upnp_init(self):
            self.current_connection_id = None
            self.parent = self.append('Buzztard', 'directory', None)
            self.buzztard.connection.protocol.bind(
                response_browse=self.add_content)
            self.buzztard.connection.protocol.bind(
                response_browse=self.clear)

            source_protocols = ''
            if self.server:
                self.server.connection_manager_server.set_variable(
                    0, 'SourceProtocolInfo', source_protocols, default=True)

            self.buzztard.connection.browse()

    class BuzztardPlayer(log.LogAble):
        '''
        .. versionchanged:: 0.9.0
            Migrated from louie/dispatcher to EventDispatcher
        '''
        logCategory = 'buzztard_player'
        implements = ['MediaRenderer']
        vendor_value_defaults = {
            'RenderingControl': {'A_ARG_TYPE_Channel': 'Master'}}
        vendor_range_defaults = {'RenderingControl': {'Volume': {'maximum': 100}}}

        def __init__(self, device, **kwargs):
            log.LogAble.__init__(self)
            self.name = kwargs.get('name', 'Buzztard MediaRenderer')
            self.host = kwargs.get('host', '127.0.0.1')
            self.port = int(kwargs.get('port', 7654))
            self.player = None

            self.playing = False
            self.state = None
            self.duration = None
            self.view = []
            self.tags = {}
            self.server = device

            self.poll_LC = LoopingCall(self.poll_player)

            self.buzztard = BzConnection(
                backend=self, host=self.host, port=self.port)
            self.buzztard.connection.protocol.bind(
                response_event=self.event)
            self.buzztard.connection.protocol.bind(
                response_volume=self.get_volume)
            self.buzztard.connection.protocol.bind(
                response_mute=self.get_mute)
            self.buzztard.connection.protocol.bind(
                response_repeat=self.get_repeat)

        def event(self, line):
            infos = line.split('|')[1:]
            self.debug(infos)
            if infos[0] == 'playing':
                transport_state = 'PLAYING'
            if infos[0] == 'stopped':
                transport_state = 'STOPPED'
            if infos[0] == 'paused':
                transport_state = 'PAUSED_PLAYBACK'
            if self.server is not None:
                connection_id = \
                    self.server.connection_manager_server.lookup_avt_id(
                        self.current_connection_id)
            if self.state != transport_state:
                self.state = transport_state
                if self.server is not None:
                    self.server.av_transport_server.set_variable(
                        connection_id, 'TransportState', transport_state)

            label = infos[1]
            position = infos[2].split('.')[0]
            duration = infos[3].split('.')[0]
            if self.server is not None:
                self.server.av_transport_server.set_variable(
                    connection_id, 'CurrentTrack', 0)
                self.server.av_transport_server.set_variable(
                    connection_id, 'CurrentTrackDuration', duration)
                self.server.av_transport_server.set_variable(
                    connection_id, 'CurrentMediaDuration', duration)
                self.server.av_transport_server.set_variable(
                    connection_id, 'RelativeTimePosition', position)
                self.server.av_transport_server.set_variable(
                    connection_id, 'AbsoluteTimePosition', position)

            try:
                self.server.rendering_control_server.set_variable(
                    connection_id, 'Volume', int(infos[4]))
            except Exception:
                pass

            try:
                if infos[5] in ['on', '1', 'true', 'True', 'yes', 'Yes']:
                    mute = True
                else:
                    mute = False
                self.server.rendering_control_server.set_variable(
                    connection_id, 'Mute', mute)
            except Exception:
                pass

            try:
                if infos[6] in ['on', '1', 'true', 'True', 'yes', 'Yes']:
                    self.server.av_transport_server.set_variable(
                        connection_id, 'CurrentPlayMode', 'REPEAT_ALL')
                else:
                    self.server.av_transport_server.set_variable(
                        connection_id, 'CurrentPlayMode', 'NORMAL')
            except Exception:
                pass

        def __repr__(self):
            return str(self.__class__).split('.')[-1]

        def poll_player(self):
            self.buzztard.connection.sendMessage('status')

        def load(self, uri, metadata=None):
            self.debug(f'load {uri} {metadata}')
            self.duration = None
            self.metadata = metadata
            connection_id = self.server.connection_manager_server.lookup_avt_id(
                self.current_connection_id)
            self.server.av_transport_server.set_variable(
                connection_id, 'CurrentTransportActions', 'Play,Stop')
            self.server.av_transport_server.set_variable(
                connection_id, 'NumberOfTracks', 1)
            self.server.av_transport_server.set_variable(
                connection_id, 'CurrentTrackURI', uri)
            self.server.av_transport_server.set_variable(
                connection_id, 'AVTransportURI', uri)
            self.server.av_transport_server.set_variable(
                connection_id, 'AVTransportURIMetaData', metadata)
            self.server.av_transport_server.set_variable(
                connection_id, 'CurrentTrackURI', uri)
            self.server.av_transport_server.set_variable(
                connection_id, 'CurrentTrackMetaData', metadata)

        def start(self, uri):
            self.load(uri)
            self.play()

        def stop(self):
            self.buzztard.connection.sendMessage('stop')

        def play(self):
            connection_id = self.server.connection_manager_server.lookup_avt_id(
                self.current_connection_id)
            label_id = self.server.av_transport_server.get_variable(
                'CurrentTrackURI', connection_id).value
            id = '0'
            if ':' in label_id:
                label, id = label_id.split(':')
            self.buzztard.connection.sendMessage(f'play|{id}')

        def pause(self):
            self.buzztard.connection.sendMessage('pause')

        def seek(self, location):
            '''

            Args:
                location (int): time to seek to, in seconds:

                                   * +nL = relative seek forward n seconds
                                   * -nL = relative seek backwards n seconds
            '''

        def mute(self):
            self.buzztard.connection.sendMessage('set|mute|on')

        def unmute(self):
            self.buzztard.connection.sendMessage('set|mute|off')

        def get_mute(self, line):
            infos = line.split('|')[1:]
            if infos[0] in ['on', '1', 'true', 'True', 'yes', 'Yes']:
                mute = True
            else:
                mute = False
            self.server.rendering_control_server.set_variable(
                0, 'Mute', mute)

        def get_repeat(self, line):
            infos = line.split('|')[1:]
            if infos[0] in ['on', '1', 'true', 'True', 'yes', 'Yes']:
                self.server.av_transport_server.set_variable(
                    0, 'CurrentPlayMode', 'REPEAT_ALL')
            else:
                self.server.av_transport_server.set_variable(
                    0, 'CurrentPlayMode', 'NORMAL')

        def set_repeat(self, playmode):
            if playmode in ['REPEAT_ONE', 'REPEAT_ALL']:
                self.buzztard.connection.sendMessage('set|repeat|on')
            else:
                self.buzztard.connection.sendMessage('set|repeat|off')

        def get_volume(self, line):
            infos = line.split('|')[1:]
            self.server.rendering_control_server.set_variable(
                0, 'Volume', int(infos[0]))

        def set_volume(self, volume):
            volume = int(volume)
            if volume < 0:
                volume = 0
            if volume > 100:
                volume = 100
            self.buzztard.connection.sendMessage(f'set|volume|{volume:d}')

        def upnp_init(self):
            self.current_connection_id = None
            self.server.connection_manager_server.set_variable(
                0, 'SinkProtocolInfo', [f'internal:{self.host}:audio/mpeg:*'],
                default=True)
            self.server.av_transport_server.set_variable(
                0, 'TransportState', 'NO_MEDIA_PRESENT', default=True)
            self.server.av_transport_server.set_variable(
                0, 'TransportStatus', 'OK', default=True)
            self.server.av_transport_server.set_variable(
                0, 'CurrentPlayMode', 'NORMAL', default=True)
            self.server.av_transport_server.set_variable(
                0, 'CurrentTransportActions', '', default=True)
            self.buzztard.connection.sendMessage('get|volume')
            self.buzztard.connection.sendMessage('get|mute')
            self.buzztard.connection.sendMessage('get|repeat')

            self.poll_LC.start(1.0, True)

        def upnp_Play(self, *args, **kwargs):
            InstanceID = int(kwargs['InstanceID'])
            Speed = int(kwargs['Speed'])
            self.play()
            return {}

        def upnp_Pause(self, *args, **kwargs):
            InstanceID = int(kwargs['InstanceID'])
            self.pause()
            return {}

        def upnp_Stop(self, *args, **kwargs):
            InstanceID = int(kwargs['InstanceID'])
            self.stop()
            return {}

        def upnp_SetAVTransportURI(self, *args, **kwargs):
            InstanceID = int(kwargs['InstanceID'])
            CurrentURI = kwargs['CurrentURI']
            CurrentURIMetaData = kwargs['CurrentURIMetaData']
            local_protocol_info = \
                self.server.connection_manager_server.get_variable(
                    'SinkProtocolInfo').value.split(',')
            if len(CurrentURIMetaData) == 0:
                self.load(CurrentURI, CurrentURIMetaData)
                return {}
            else:
                elt = DIDLLite.DIDLElement.fromString(CurrentURIMetaData)
                print(elt.numItems())
                if elt.numItems() == 1:
                    item = elt.getItems()[0]
                    for res in item.res:
                        print(res.protocolInfo, local_protocol_info)
                        if res.protocolInfo in local_protocol_info:
                            self.load(CurrentURI, CurrentURIMetaData)
                            return {}
            return failure.Failure(errorCode(714))

        def upnp_SetMute(self, *args, **kwargs):
            InstanceID = int(kwargs['InstanceID'])
            Channel = kwargs['Channel']
            DesiredMute = kwargs['DesiredMute']
            if DesiredMute in ['TRUE', 'True', 'true', '1', 'Yes', 'yes']:
                self.mute()
            else:
                self.unmute()
            return {}

        def upnp_SetVolume(self, *args, **kwargs):
            InstanceID = int(kwargs['InstanceID'])
            Channel = kwargs['Channel']
            DesiredVolume = int(kwargs['DesiredVolume'])
            self.set_volume(DesiredVolume)
            return {}

    def on_init_complete(backend):
        print('Houston, we have a touchdown!')
        backend.buzztard.sendMessage('browse')

    def main():
        '''
        .. versionchanged:: 0.9.0
            Migrated from louie/dispatcher to EventDispatcher
        '''

        f = BuzztardStore(None)
        f.bind(init_completed=on_init_complete)

        f.parent = f.append('Buzztard', 'directory', None)
        print(f.parent)
        print(f'f.store is: {f.store}')
        f.add_content('playlist|test label|start|stop')
        print(f.store)
        f.clear()
        print(f.store)
        f.add_content('playlist|after flush label|flush-start|flush-stop')
        print(f.store)

        # def got_upnp_result(result):
        #    print('upnp', result)

        # f.upnp_init()
        # print f.store
        # r = f.upnp_Browse(BrowseFlag='BrowseDirectChildren',
        #                    RequestedCount=0,
        #                    StartingIndex=0,
        #                    ObjectID=0,
        #                    SortCriteria='*',
        #                    Filter='')
        # got_upnp_result(r)

    if __name__ == '__main__':
        from twisted.internet import reactor

        reactor.callWhenRunning(main)
        reactor.run()