# Licensed under the MIT license
# http://opensource.org/licenses/mit-license.php
# Copyright 2007, Frank Scholz <coherence@beebits.net>
from twisted.internet import protocol
from twisted.internet.task import LoopingCall
from twisted.protocols.basic import LineReceiver
from twisted.python import failure
import coherence.extern.louie as louie
from coherence import log
from coherence.extern.simple_plugin import Plugin
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, log.LogAble):
logCategory = 'buzztard_client'
factory = None
def __init__(self, *args, **kwargs):
log.LogAble.__init__(self)
[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("received: %s", line)
if line == 'flush':
louie.send('Buzztard.Response.flush', None)
elif line.find('event') == 0:
louie.send('Buzztard.Response.event', None, line)
elif line.find('volume') == 0:
louie.send('Buzztard.Response.volume', None, line)
elif line.find('mute') == 0:
louie.send('Buzztard.Response.mute', None, line)
elif line.find('repeat') == 0:
louie.send('Buzztard.Response.repeat', None, line)
elif line.find('playlist') == 0:
louie.send('Buzztard.Response.browse', None, line)
[docs]class BzFactory(protocol.ClientFactory, log.LogAble):
logCategory = 'buzztard_factory'
protocol = BzClient
def __init__(self, backend):
log.LogAble.__init__(self)
self.backend = backend
[docs] def clientConnectionFailed(self, connector, reason):
self.error('connection failed: %s', reason.getErrorMessage())
[docs] def clientConnectionLost(self, connector, reason):
self.error('connection lost: %s', reason.getErrorMessage())
[docs] def startFactory(self):
self.messageQueue = []
self.clientInstance = None
[docs] def clientReady(self, instance):
self.info("clientReady")
louie.send('Coherence.UPnP.Backend.init_completed', None,
backend=self.backend)
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
"""
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):
cls.debug(cls, "BzConnection __new__")
obj = getattr(cls, '_instance_', None)
if obj is not None:
louie.send('Coherence.UPnP.Backend.init_completed', None,
backend=kwargs['backend'])
return obj
else:
obj = super(BzConnection, cls).__new__(cls, *args, **kwargs)
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, 'internal:%s:%s:*' % (
host, self.mimetype))
res.size = None
self.item.res.append(res)
self.item.artist = self.parent.name
def __del__(self):
self.debug("BuzztardItem __del__ %s %s", self.id, self.name)
pass
[docs] def remove(self, store):
self.debug("BuzztardItem remove %s %s %s", 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):
self.item.childCount += 1
if update:
self.update_id += 1
[docs] def remove_child(self, child):
self.debug("remove_from %d (%s) child %d (%s)", self.id,
self.get_name(), child.id, 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(log.LogAble, Plugin):
logCategory = 'buzztard_store'
implements = ['MediaServer']
def __init__(self, server, **kwargs):
log.LogAble.__init__(self)
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
louie.connect(self.add_content, 'Buzztard.Response.browse', louie.Any)
louie.connect(self.clear, 'Buzztard.Response.flush', louie.Any)
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)
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):
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)
louie.connect(self.event, 'Buzztard.Response.event', louie.Any)
louie.connect(self.get_volume, 'Buzztard.Response.volume', louie.Any)
louie.connect(self.get_mute, 'Buzztard.Response.mute', louie.Any)
louie.connect(self.get_repeat, 'Buzztard.Response.repeat', louie.Any)
self.buzztard = BzConnection(backend=self, host=self.host,
port=self.port)
[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("load %s %s", 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('play|%s' % id)
[docs] def pause(self):
self.buzztard.connection.sendMessage('pause')
[docs] def seek(self, location):
"""
@param location: simple number = 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('set|volume|%d' % volume)
[docs] def upnp_init(self):
self.current_connection_id = None
self.server.connection_manager_server.set_variable(
0, 'SinkProtocolInfo', ['internal:%s:audio/mpeg:*' % self.host],
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 test_init_complete(backend):
print("Houston, we have a touchdown!")
backend.buzztard.sendMessage('browse')
[docs]def main():
louie.connect(test_init_complete, 'Coherence.UPnP.Backend.init_completed',
louie.Any)
f = BuzztardStore(None)
f.parent = f.append('Buzztard', 'directory', None)
print(f.parent)
print(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()