# 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
[docs]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',
)
[docs] 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)
[docs] 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)
[docs]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
[docs] def clientConnectionFailed(self, connector, reason):
self.error(f'connection failed: {reason.getErrorMessage()}')
[docs] def clientConnectionLost(self, connector, reason):
self.error(f'connection lost: {reason.getErrorMessage()}')
[docs] def startFactory(self):
self.messageQueue = []
self.clientInstance = None
[docs] def clientReady(self, instance):
self.info('clientReady')
self.backend.init_completed = True
self.clientInstance = instance
for msg in self.messageQueue:
self.sendMessage(msg)
[docs] def sendMessage(self, msg):
if self.clientInstance is not None:
self.clientInstance.sendLine(msg)
else:
self.messageQueue.append(msg)
[docs] def rebrowse(self):
self.backend.clear()
self.browse()
[docs] def browse(self):
self.sendMessage('browse')
[docs]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
[docs]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
[docs] 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
[docs] 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
[docs] 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
[docs] def get_children(self, start=0, request_count=0):
if request_count == 0:
return self.children[start:]
else:
return self.children[start:request_count]
[docs] def get_child_count(self):
return self.child_count
[docs] def get_id(self):
return self.id
[docs] def get_update_id(self):
if hasattr(self, 'update_id'):
return self.update_id
else:
return None
[docs] def get_path(self):
return self.url
[docs] def get_name(self):
return self.name
[docs] def get_parent(self):
return self.parent
[docs] def get_item(self):
return self.item
[docs] 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
[docs]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]
[docs] 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
[docs] 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
[docs] 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)
[docs] def clear(self):
for item in self.get_by_id(1000).get_children():
self.remove(item.get_id())
self.buzztard.connection.browse()
[docs] def len(self):
return len(self.store)
[docs] def get_by_id(self, id):
id = int(id)
if id == 0:
id = 1000
try:
return self.store[id]
except KeyError:
return None
[docs] def getnextID(self):
ret = self.next_id
self.next_id += 1
return ret
[docs] 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()
[docs]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)
[docs] 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]
[docs] def poll_player(self):
self.buzztard.connection.sendMessage('status')
[docs] 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)
[docs] def start(self, uri):
self.load(uri)
self.play()
[docs] def stop(self):
self.buzztard.connection.sendMessage('stop')
[docs] 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}')
[docs] def pause(self):
self.buzztard.connection.sendMessage('pause')
[docs] 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
'''
[docs] def mute(self):
self.buzztard.connection.sendMessage('set|mute|on')
[docs] def unmute(self):
self.buzztard.connection.sendMessage('set|mute|off')
[docs] 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)
[docs] 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')
[docs] 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')
[docs] def get_volume(self, line):
infos = line.split('|')[1:]
self.server.rendering_control_server.set_variable(
0, 'Volume', int(infos[0]))
[docs] 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}')
[docs] 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)
[docs] def upnp_Play(self, *args, **kwargs):
InstanceID = int(kwargs['InstanceID'])
Speed = int(kwargs['Speed'])
self.play()
return {}
[docs] def upnp_Pause(self, *args, **kwargs):
InstanceID = int(kwargs['InstanceID'])
self.pause()
return {}
[docs] def upnp_Stop(self, *args, **kwargs):
InstanceID = int(kwargs['InstanceID'])
self.stop()
return {}
[docs] 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))
[docs] 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 {}
[docs] def upnp_SetVolume(self, *args, **kwargs):
InstanceID = int(kwargs['InstanceID'])
Channel = kwargs['Channel']
DesiredVolume = int(kwargs['DesiredVolume'])
self.set_volume(DesiredVolume)
return {}
[docs]def on_init_complete(backend):
print('Houston, we have a touchdown!')
backend.buzztard.sendMessage('browse')
[docs]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()