[git-p4: depot-paths = "//depot/": change = 763]replace/4e84fdb41ea781c7a8f872baa423e8b3be4045a7
| @@ -10,10 +10,10 @@ from twisted.web import resource, static, soap | |||||
| from upnp import UPnPPublisher | from upnp import UPnPPublisher | ||||
| class ConnectionManagerControl(UPnPPublisher): | class ConnectionManagerControl(UPnPPublisher): | ||||
| pass | |||||
| pass | |||||
| class ConnectionManagerServer(resource.Resource): | class ConnectionManagerServer(resource.Resource): | ||||
| def __init__(self): | |||||
| resource.Resource.__init__(self) | |||||
| self.putChild('scpd.xml', static.File('connection-manager-scpd.xml')) | |||||
| self.putChild('control', ConnectionManagerControl()) | |||||
| def __init__(self): | |||||
| resource.Resource.__init__(self) | |||||
| self.putChild('scpd.xml', static.File('connection-manager-scpd.xml')) | |||||
| self.putChild('control', ConnectionManagerControl()) | |||||
| @@ -14,9 +14,9 @@ | |||||
| # twisted swallows the tracebacks. At the moment I'm going: | # twisted swallows the tracebacks. At the moment I'm going: | ||||
| # | # | ||||
| # try: | # try: | ||||
| # .... | |||||
| # .... | |||||
| # except: | # except: | ||||
| # traceback.print_exc(file = log.logfile) | |||||
| # traceback.print_exc(file = log.logfile) | |||||
| # | # | ||||
| from twisted.python import log | from twisted.python import log | ||||
| @@ -30,208 +30,208 @@ import traceback | |||||
| from urllib import quote | from urllib import quote | ||||
| class ContentDirectoryControl(UPnPPublisher, dict): | class ContentDirectoryControl(UPnPPublisher, dict): | ||||
| """This class implements the CDS actions over SOAP.""" | |||||
| def getnextID(self): | |||||
| ret = str(self.nextID) | |||||
| self.nextID += 1 | |||||
| return ret | |||||
| def addContainer(self, parent, title, klass = Container, **kwargs): | |||||
| ret = self.addItem(parent, klass, title, **kwargs) | |||||
| self.children[ret] = self[ret] | |||||
| return ret | |||||
| def addItem(self, parent, klass, *args, **kwargs): | |||||
| assert isinstance(self[parent], Container) | |||||
| nid = self.getnextID() | |||||
| i = klass(self, nid, parent, *args, **kwargs) | |||||
| self.children[parent].append(i) | |||||
| self[i.id] = i | |||||
| return i.id | |||||
| def delItem(self, id): | |||||
| """This class implements the CDS actions over SOAP.""" | |||||
| def getnextID(self): | |||||
| ret = str(self.nextID) | |||||
| self.nextID += 1 | |||||
| return ret | |||||
| def addContainer(self, parent, title, klass = Container, **kwargs): | |||||
| ret = self.addItem(parent, klass, title, **kwargs) | |||||
| self.children[ret] = self[ret] | |||||
| return ret | |||||
| def addItem(self, parent, klass, *args, **kwargs): | |||||
| assert isinstance(self[parent], Container) | |||||
| nid = self.getnextID() | |||||
| i = klass(self, nid, parent, *args, **kwargs) | |||||
| self.children[parent].append(i) | |||||
| self[i.id] = i | |||||
| return i.id | |||||
| def delItem(self, id): | |||||
| if isinstance(self[id], Container): | if isinstance(self[id], Container): | ||||
| for i in self.children[id]: | for i in self.children[id]: | ||||
| self.delItem(i) | self.delItem(i) | ||||
| assert len(self.children[id]) == 0 | assert len(self.children[id]) == 0 | ||||
| del self.children[id] | del self.children[id] | ||||
| del self[id] | |||||
| del self[id] | |||||
| def getchildren(self, item): | |||||
| assert isinstance(self[item], Container) | |||||
| return self.children[item][:] | |||||
| def getchildren(self, item): | |||||
| assert isinstance(self[item], Container) | |||||
| return self.children[item][:] | |||||
| def __init__(self, title, *args): | |||||
| super(ContentDirectoryControl, self).__init__(*args) | |||||
| fakeparent = '-1' | |||||
| self.nextID = 0 | |||||
| self.children = { fakeparent: []} | |||||
| self.needupdate = False | |||||
| self.updateId = 0 | |||||
| self[fakeparent] = Container(None, '-1', 'fake') | |||||
| root = self.addContainer(fakeparent, title) | |||||
| assert root == '0' | |||||
| del self[fakeparent] | |||||
| del self.children[fakeparent] | |||||
| def __init__(self, title, *args): | |||||
| super(ContentDirectoryControl, self).__init__(*args) | |||||
| fakeparent = '-1' | |||||
| self.nextID = 0 | |||||
| self.children = { fakeparent: []} | |||||
| self.needupdate = False | |||||
| self.updateId = 0 | |||||
| self[fakeparent] = Container(None, '-1', 'fake') | |||||
| root = self.addContainer(fakeparent, title) | |||||
| assert root == '0' | |||||
| del self[fakeparent] | |||||
| del self.children[fakeparent] | |||||
| def doupdate(self): | |||||
| if self.needupdate: | |||||
| self.needupdate += 1 | |||||
| self.needupdate = False | |||||
| def doupdate(self): | |||||
| if self.needupdate: | |||||
| self.needupdate += 1 | |||||
| self.needupdate = False | |||||
| # Required actions | |||||
| # Required actions | |||||
| def soap_GetSearchCapabilities(self, *args, **kwargs): | |||||
| """Required: Return the searching capabilities supported by the device.""" | |||||
| def soap_GetSearchCapabilities(self, *args, **kwargs): | |||||
| """Required: Return the searching capabilities supported by the device.""" | |||||
| log.msg('GetSearchCapabilities()') | |||||
| return { 'SearchCapabilitiesResponse': { 'SearchCaps': '' }} | |||||
| log.msg('GetSearchCapabilities()') | |||||
| return { 'SearchCapabilitiesResponse': { 'SearchCaps': '' }} | |||||
| def soap_GetSortCapabilities(self, *args, **kwargs): | |||||
| """Required: Return the CSV list of meta-data tags that can be used in | |||||
| sortCriteria.""" | |||||
| def soap_GetSortCapabilities(self, *args, **kwargs): | |||||
| """Required: Return the CSV list of meta-data tags that can be used in | |||||
| sortCriteria.""" | |||||
| log.msg('GetSortCapabilities()') | |||||
| return { 'SortCapabilitiesResponse': { 'SortCaps': '' }} | |||||
| def soap_GetSystemUpdateID(self, *args, **kwargs): | |||||
| """Required: Return the current value of state variable SystemUpdateID.""" | |||||
| log.msg('GetSortCapabilities()') | |||||
| return { 'SortCapabilitiesResponse': { 'SortCaps': '' }} | |||||
| log.msg('GetSystemUpdateID()') | |||||
| self.needupdate = True | |||||
| return { 'SystemUpdateIdResponse': { 'Id': self.updateId }} | |||||
| def soap_GetSystemUpdateID(self, *args, **kwargs): | |||||
| """Required: Return the current value of state variable SystemUpdateID.""" | |||||
| BrowseFlags = ('BrowseMetaData', 'BrowseDirectChildren') | |||||
| log.msg('GetSystemUpdateID()') | |||||
| self.needupdate = True | |||||
| return { 'SystemUpdateIdResponse': { 'Id': self.updateId }} | |||||
| def soap_Browse(self, *args): | |||||
| """Required: Incrementally browse the native heirachy of the Content | |||||
| Directory objects exposed by the Content Directory Service.""" | |||||
| BrowseFlags = ('BrowseMetaData', 'BrowseDirectChildren') | |||||
| (ObjectID, BrowseFlag, Filter, StartingIndex, RequestedCount, | |||||
| SortCriteria) = args | |||||
| StartingIndex = int(StartingIndex) | |||||
| RequestedCount = int(RequestedCount) | |||||
| def soap_Browse(self, *args): | |||||
| """Required: Incrementally browse the native heirachy of the Content | |||||
| Directory objects exposed by the Content Directory Service.""" | |||||
| log.msg('Browse(ObjectID=%s, BrowseFlags=%s, Filter=%s, ' | |||||
| 'StartingIndex=%s RequestedCount=%s SortCriteria=%s)' % | |||||
| (`ObjectID`, `BrowseFlag`, `Filter`, `StartingIndex`, | |||||
| `RequestedCount`, `SortCriteria`)) | |||||
| (ObjectID, BrowseFlag, Filter, StartingIndex, RequestedCount, | |||||
| SortCriteria) = args | |||||
| StartingIndex = int(StartingIndex) | |||||
| RequestedCount = int(RequestedCount) | |||||
| didl = DIDLElement() | |||||
| result = {} | |||||
| log.msg('Browse(ObjectID=%s, BrowseFlags=%s, Filter=%s, ' | |||||
| 'StartingIndex=%s RequestedCount=%s SortCriteria=%s)' % | |||||
| (`ObjectID`, `BrowseFlag`, `Filter`, `StartingIndex`, | |||||
| `RequestedCount`, `SortCriteria`)) | |||||
| try: | |||||
| if BrowseFlag == 'BrowseDirectChildren': | |||||
| ch = self.getchildren(ObjectID)[StartingIndex: StartingIndex + RequestedCount] | |||||
| filter(lambda x, s = self, d = didl: d.addItem(s[x.id]) and None, ch) | |||||
| else: | |||||
| didl.addItem(self[ObjectID]) | |||||
| didl = DIDLElement() | |||||
| result = {} | |||||
| result = {'BrowseResponse': {'Result': didl.toString() , | |||||
| 'NumberReturned': didl.numItems(), | |||||
| 'TotalMatches': didl.numItems(), | |||||
| 'UpdateID': 0}} | |||||
| try: | |||||
| if BrowseFlag == 'BrowseDirectChildren': | |||||
| ch = self.getchildren(ObjectID)[StartingIndex: StartingIndex + RequestedCount] | |||||
| filter(lambda x, s = self, d = didl: d.addItem(s[x.id]) and None, ch) | |||||
| else: | |||||
| didl.addItem(self[ObjectID]) | |||||
| except: | |||||
| traceback.print_exc(file=log.logfile) | |||||
| result = {'BrowseResponse': {'Result': didl.toString() , | |||||
| 'NumberReturned': didl.numItems(), | |||||
| 'TotalMatches': didl.numItems(), | |||||
| 'UpdateID': 0}} | |||||
| log.msg('Returning: %s' % result) | |||||
| except: | |||||
| traceback.print_exc(file=log.logfile) | |||||
| return result | |||||
| log.msg('Returning: %s' % result) | |||||
| # Optional actions | |||||
| return result | |||||
| def soap_Search(self, *args, **kwargs): | |||||
| """Search for objects that match some search criteria.""" | |||||
| # Optional actions | |||||
| (ContainerID, SearchCriteria, Filter, StartingIndex, | |||||
| RequestedCount, SortCriteria) = args | |||||
| def soap_Search(self, *args, **kwargs): | |||||
| """Search for objects that match some search criteria.""" | |||||
| log.msg('Search(ContainerID=%s, SearchCriteria=%s, Filter=%s, ' \ | |||||
| 'StartingIndex=%s, RequestedCount=%s, SortCriteria=%s)' % | |||||
| (`ContainerID`, `SearchCriteria`, `Filter`, | |||||
| `StartingIndex`, `RequestedCount`, `SortCriteria`)) | |||||
| def soap_CreateObject(self, *args, **kwargs): | |||||
| """Create a new object.""" | |||||
| (ContainerID, SearchCriteria, Filter, StartingIndex, | |||||
| RequestedCount, SortCriteria) = args | |||||
| (ContainerID, Elements) = args | |||||
| log.msg('Search(ContainerID=%s, SearchCriteria=%s, Filter=%s, ' \ | |||||
| 'StartingIndex=%s, RequestedCount=%s, SortCriteria=%s)' % | |||||
| (`ContainerID`, `SearchCriteria`, `Filter`, | |||||
| `StartingIndex`, `RequestedCount`, `SortCriteria`)) | |||||
| log.msg('CreateObject(ContainerID=%s, Elements=%s)' % | |||||
| (`ContainerID`, `Elements`)) | |||||
| def soap_CreateObject(self, *args, **kwargs): | |||||
| """Create a new object.""" | |||||
| def soap_DestroyObject(self, *args, **kwargs): | |||||
| """Destroy the specified object.""" | |||||
| (ContainerID, Elements) = args | |||||
| (ObjectID) = args | |||||
| log.msg('CreateObject(ContainerID=%s, Elements=%s)' % | |||||
| (`ContainerID`, `Elements`)) | |||||
| log.msg('DestroyObject(ObjectID=%s)' % `ObjectID`) | |||||
| def soap_DestroyObject(self, *args, **kwargs): | |||||
| """Destroy the specified object.""" | |||||
| def soap_UpdateObject(self, *args, **kwargs): | |||||
| """Modify, delete or insert object metadata.""" | |||||
| (ObjectID) = args | |||||
| (ObjectID, CurrentTagValue, NewTagValue) = args | |||||
| log.msg('DestroyObject(ObjectID=%s)' % `ObjectID`) | |||||
| log.msg('UpdateObject(ObjectID=%s, CurrentTagValue=%s, ' \ | |||||
| 'NewTagValue=%s)' % (`ObjectID`, `CurrentTagValue`, | |||||
| `NewTagValue`)) | |||||
| def soap_UpdateObject(self, *args, **kwargs): | |||||
| """Modify, delete or insert object metadata.""" | |||||
| def soap_ImportResource(self, *args, **kwargs): | |||||
| """Transfer a file from a remote source to a local | |||||
| destination in the Content Directory Service.""" | |||||
| (ObjectID, CurrentTagValue, NewTagValue) = args | |||||
| (SourceURI, DestinationURI) = args | |||||
| log.msg('UpdateObject(ObjectID=%s, CurrentTagValue=%s, ' \ | |||||
| 'NewTagValue=%s)' % (`ObjectID`, `CurrentTagValue`, | |||||
| `NewTagValue`)) | |||||
| log.msg('ImportResource(SourceURI=%s, DestinationURI=%s)' % | |||||
| (`SourceURI`, `DestinationURI`)) | |||||
| def soap_ImportResource(self, *args, **kwargs): | |||||
| """Transfer a file from a remote source to a local | |||||
| destination in the Content Directory Service.""" | |||||
| def soap_ExportResource(self, *args, **kwargs): | |||||
| """Transfer a file from a local source to a remote | |||||
| destination.""" | |||||
| (SourceURI, DestinationURI) = args | |||||
| (SourceURI, DestinationURI) = args | |||||
| log.msg('ImportResource(SourceURI=%s, DestinationURI=%s)' % | |||||
| (`SourceURI`, `DestinationURI`)) | |||||
| log.msg('ExportResource(SourceURI=%s, DestinationURI=%s)' % | |||||
| (`SourceURI`, `DestinationURI`)) | |||||
| def soap_ExportResource(self, *args, **kwargs): | |||||
| """Transfer a file from a local source to a remote | |||||
| destination.""" | |||||
| def soap_StopTransferResource(self, *args, **kwargs): | |||||
| """Stop a file transfer initiated by ImportResource or | |||||
| ExportResource.""" | |||||
| (SourceURI, DestinationURI) = args | |||||
| (TransferID) = args | |||||
| log.msg('ExportResource(SourceURI=%s, DestinationURI=%s)' % | |||||
| (`SourceURI`, `DestinationURI`)) | |||||
| log.msg('StopTransferResource(TransferID=%s)' % TransferID) | |||||
| def soap_StopTransferResource(self, *args, **kwargs): | |||||
| """Stop a file transfer initiated by ImportResource or | |||||
| ExportResource.""" | |||||
| def soap_GetTransferProgress(self, *args, **kwargs): | |||||
| """Query the progress of a file transfer initiated by | |||||
| an ImportResource or ExportResource action.""" | |||||
| (TransferID) = args | |||||
| (TransferID, TransferStatus, TransferLength, TransferTotal) = args | |||||
| log.msg('StopTransferResource(TransferID=%s)' % TransferID) | |||||
| log.msg('GetTransferProgress(TransferID=%s, TransferStatus=%s, ' \ | |||||
| 'TransferLength=%s, TransferTotal=%s)' % | |||||
| (`TransferId`, `TransferStatus`, `TransferLength`, | |||||
| `TransferTotal`)) | |||||
| def soap_DeleteResource(self, *args, **kwargs): | |||||
| """Delete a specified resource.""" | |||||
| def soap_GetTransferProgress(self, *args, **kwargs): | |||||
| """Query the progress of a file transfer initiated by | |||||
| an ImportResource or ExportResource action.""" | |||||
| (ResourceURI) = args | |||||
| (TransferID, TransferStatus, TransferLength, TransferTotal) = args | |||||
| log.msg('DeleteResource(ResourceURI=%s)' % `ResourceURI`) | |||||
| log.msg('GetTransferProgress(TransferID=%s, TransferStatus=%s, ' \ | |||||
| 'TransferLength=%s, TransferTotal=%s)' % | |||||
| (`TransferId`, `TransferStatus`, `TransferLength`, | |||||
| `TransferTotal`)) | |||||
| def soap_CreateReference(self, *args, **kwargs): | |||||
| """Create a reference to an existing object.""" | |||||
| def soap_DeleteResource(self, *args, **kwargs): | |||||
| """Delete a specified resource.""" | |||||
| (ContainerID, ObjectID) = args | |||||
| (ResourceURI) = args | |||||
| log.msg('CreateReference(ContainerID=%s, ObjectID=%s)' % | |||||
| (`ContainerID`, `ObjectID`)) | |||||
| log.msg('DeleteResource(ResourceURI=%s)' % `ResourceURI`) | |||||
| def soap_CreateReference(self, *args, **kwargs): | |||||
| """Create a reference to an existing object.""" | |||||
| (ContainerID, ObjectID) = args | |||||
| log.msg('CreateReference(ContainerID=%s, ObjectID=%s)' % | |||||
| (`ContainerID`, `ObjectID`)) | |||||
| class ContentDirectoryServer(resource.Resource): | class ContentDirectoryServer(resource.Resource): | ||||
| def __init__(self, title): | |||||
| resource.Resource.__init__(self) | |||||
| self.putChild('scpd.xml', static.File('content-directory-scpd.xml')) | |||||
| self.control = ContentDirectoryControl(title) | |||||
| self.putChild('control', self.control) | |||||
| def __init__(self, title): | |||||
| resource.Resource.__init__(self) | |||||
| self.putChild('scpd.xml', static.File('content-directory-scpd.xml')) | |||||
| self.control = ContentDirectoryControl(title) | |||||
| self.putChild('control', self.control) | |||||
| @@ -6,346 +6,346 @@ | |||||
| from elementtree.ElementTree import Element, SubElement, tostring, _ElementInterface | from elementtree.ElementTree import Element, SubElement, tostring, _ElementInterface | ||||
| class Resource: | class Resource: | ||||
| """An object representing a resource.""" | |||||
| """An object representing a resource.""" | |||||
| def __init__(self, data, protocolInfo): | |||||
| self.data = data | |||||
| self.protocolInfo = protocolInfo | |||||
| self.bitrate = None | |||||
| self.size = None | |||||
| def toElement(self): | |||||
| def __init__(self, data, protocolInfo): | |||||
| self.data = data | |||||
| self.protocolInfo = protocolInfo | |||||
| self.bitrate = None | |||||
| self.size = None | |||||
| root = Element('res') | |||||
| root.attrib['protocolInfo'] = self.protocolInfo | |||||
| root.text = self.data | |||||
| def toElement(self): | |||||
| if self.bitrate is not None: | |||||
| root.attrib['bitrate'] = str(self.bitrate) | |||||
| root = Element('res') | |||||
| root.attrib['protocolInfo'] = self.protocolInfo | |||||
| root.text = self.data | |||||
| if self.size is not None: | |||||
| root.attrib['size'] = str(self.size) | |||||
| if self.bitrate is not None: | |||||
| root.attrib['bitrate'] = str(self.bitrate) | |||||
| return root | |||||
| if self.size is not None: | |||||
| root.attrib['size'] = str(self.size) | |||||
| return root | |||||
| class Object: | class Object: | ||||
| """The root class of the entire content directory class heirachy.""" | |||||
| """The root class of the entire content directory class heirachy.""" | |||||
| klass = 'object' | |||||
| creator = None | |||||
| res = None | |||||
| writeStatus = None | |||||
| klass = 'object' | |||||
| creator = None | |||||
| res = None | |||||
| writeStatus = None | |||||
| def __init__(self, cd, id, parentID, title, restricted = False, | |||||
| creator = None): | |||||
| def __init__(self, cd, id, parentID, title, restricted = False, | |||||
| creator = None): | |||||
| self.cd = cd | |||||
| self.id = id | |||||
| self.parentID = parentID | |||||
| self.title = title | |||||
| self.creator = creator | |||||
| self.cd = cd | |||||
| self.id = id | |||||
| self.parentID = parentID | |||||
| self.title = title | |||||
| self.creator = creator | |||||
| if restricted: | |||||
| self.restricted = '1' | |||||
| else: | |||||
| self.restricted = '0' | |||||
| if restricted: | |||||
| self.restricted = '1' | |||||
| else: | |||||
| self.restricted = '0' | |||||
| def toElement(self): | |||||
| def toElement(self): | |||||
| root = Element(self.elementName) | |||||
| root = Element(self.elementName) | |||||
| root.attrib['id'] = self.id | |||||
| root.attrib['parentID'] = self.parentID | |||||
| SubElement(root, 'dc:title').text = self.title | |||||
| SubElement(root, 'upnp:class').text = self.klass | |||||
| root.attrib['id'] = self.id | |||||
| root.attrib['parentID'] = self.parentID | |||||
| SubElement(root, 'dc:title').text = self.title | |||||
| SubElement(root, 'upnp:class').text = self.klass | |||||
| root.attrib['restricted'] = self.restricted | |||||
| root.attrib['restricted'] = self.restricted | |||||
| if self.creator is not None: | |||||
| SubElement(root, 'dc:creator').text = self.creator | |||||
| if self.creator is not None: | |||||
| SubElement(root, 'dc:creator').text = self.creator | |||||
| if self.res is not None: | |||||
| root.append(self.res.toElement()) | |||||
| if self.res is not None: | |||||
| root.append(self.res.toElement()) | |||||
| if self.writeStatus is not None: | |||||
| SubElement(root, 'upnp:writeStatus').text = self.writeStatus | |||||
| if self.writeStatus is not None: | |||||
| SubElement(root, 'upnp:writeStatus').text = self.writeStatus | |||||
| return root | |||||
| def toString(self): | |||||
| return tostring(self.toElement()) | |||||
| return root | |||||
| def toString(self): | |||||
| return tostring(self.toElement()) | |||||
| class Item(Object): | class Item(Object): | ||||
| """A class used to represent atomic (non-container) content | |||||
| objects.""" | |||||
| """A class used to represent atomic (non-container) content | |||||
| objects.""" | |||||
| klass = Object.klass + '.item' | |||||
| elementName = 'item' | |||||
| refID = None | |||||
| klass = Object.klass + '.item' | |||||
| elementName = 'item' | |||||
| refID = None | |||||
| def toElement(self): | |||||
| def toElement(self): | |||||
| root = Object.toElement(self) | |||||
| root = Object.toElement(self) | |||||
| if self.refID is not None: | |||||
| SubElement(root, 'refID').text = self.refID | |||||
| if self.refID is not None: | |||||
| SubElement(root, 'refID').text = self.refID | |||||
| return root | |||||
| return root | |||||
| class ImageItem(Item): | class ImageItem(Item): | ||||
| klass = Item.klass + '.imageItem' | |||||
| klass = Item.klass + '.imageItem' | |||||
| class Photo(ImageItem): | class Photo(ImageItem): | ||||
| klass = ImageItem.klass + '.photo' | |||||
| klass = ImageItem.klass + '.photo' | |||||
| class AudioItem(Item): | class AudioItem(Item): | ||||
| """A piece of content that when rendered generates some audio.""" | |||||
| """A piece of content that when rendered generates some audio.""" | |||||
| klass = Item.klass + '.audioItem' | |||||
| klass = Item.klass + '.audioItem' | |||||
| genre = None | |||||
| description = None | |||||
| longDescription = None | |||||
| publisher = None | |||||
| language = None | |||||
| relation = None | |||||
| rights = None | |||||
| genre = None | |||||
| description = None | |||||
| longDescription = None | |||||
| publisher = None | |||||
| language = None | |||||
| relation = None | |||||
| rights = None | |||||
| def toElement(self): | |||||
| def toElement(self): | |||||
| root = Item.toElement(self) | |||||
| root = Item.toElement(self) | |||||
| if self.genre is not None: | |||||
| SubElement(root, 'upnp:genre').text = self.genre | |||||
| if self.genre is not None: | |||||
| SubElement(root, 'upnp:genre').text = self.genre | |||||
| if self.description is not None: | |||||
| SubElement(root, 'dc:description').text = self.description | |||||
| if self.description is not None: | |||||
| SubElement(root, 'dc:description').text = self.description | |||||
| if self.longDescription is not None: | |||||
| SubElement(root, 'upnp:longDescription').text = \ | |||||
| self.longDescription | |||||
| if self.longDescription is not None: | |||||
| SubElement(root, 'upnp:longDescription').text = \ | |||||
| self.longDescription | |||||
| if self.publisher is not None: | |||||
| SubElement(root, 'dc:publisher').text = self.publisher | |||||
| if self.publisher is not None: | |||||
| SubElement(root, 'dc:publisher').text = self.publisher | |||||
| if self.language is not None: | |||||
| SubElement(root, 'dc:language').text = self.language | |||||
| if self.language is not None: | |||||
| SubElement(root, 'dc:language').text = self.language | |||||
| if self.relation is not None: | |||||
| SubElement(root, 'dc:relation').text = self.relation | |||||
| if self.relation is not None: | |||||
| SubElement(root, 'dc:relation').text = self.relation | |||||
| if self.rights is not None: | |||||
| SubElement(root, 'dc:rights').text = self.rights | |||||
| if self.rights is not None: | |||||
| SubElement(root, 'dc:rights').text = self.rights | |||||
| return root | |||||
| return root | |||||
| class MusicTrack(AudioItem): | class MusicTrack(AudioItem): | ||||
| """A discrete piece of audio that should be interpreted as music.""" | |||||
| """A discrete piece of audio that should be interpreted as music.""" | |||||
| klass = AudioItem.klass + '.musicTrack' | |||||
| klass = AudioItem.klass + '.musicTrack' | |||||
| artist = None | |||||
| album = None | |||||
| originalTrackNumber = None | |||||
| playlist = None | |||||
| storageMedium = None | |||||
| contributor = None | |||||
| date = None | |||||
| artist = None | |||||
| album = None | |||||
| originalTrackNumber = None | |||||
| playlist = None | |||||
| storageMedium = None | |||||
| contributor = None | |||||
| date = None | |||||
| def toElement(self): | |||||
| def toElement(self): | |||||
| root = AudioItem.toElement(self) | |||||
| root = AudioItem.toElement(self) | |||||
| if self.artist is not None: | |||||
| SubElement(root, 'upnp:artist').text = self.artist | |||||
| if self.artist is not None: | |||||
| SubElement(root, 'upnp:artist').text = self.artist | |||||
| if self.album is not None: | |||||
| SubElement(root, 'upnp:album').text = self.album | |||||
| if self.album is not None: | |||||
| SubElement(root, 'upnp:album').text = self.album | |||||
| if self.originalTrackNumber is not None: | |||||
| SubElement(root, 'upnp:originalTrackNumber').text = \ | |||||
| self.originalTrackNumber | |||||
| if self.originalTrackNumber is not None: | |||||
| SubElement(root, 'upnp:originalTrackNumber').text = \ | |||||
| self.originalTrackNumber | |||||
| if self.playlist is not None: | |||||
| SubElement(root, 'upnp:playlist').text = self.playlist | |||||
| if self.playlist is not None: | |||||
| SubElement(root, 'upnp:playlist').text = self.playlist | |||||
| if self.storageMedium is not None: | |||||
| SubElement(root, 'upnp:storageMedium').text = self.storageMedium | |||||
| if self.storageMedium is not None: | |||||
| SubElement(root, 'upnp:storageMedium').text = self.storageMedium | |||||
| if self.contributor is not None: | |||||
| SubElement(root, 'dc:contributor').text = self.contributor | |||||
| if self.contributor is not None: | |||||
| SubElement(root, 'dc:contributor').text = self.contributor | |||||
| if self.date is not None: | |||||
| SubElement(root, 'dc:date').text = self.date | |||||
| if self.date is not None: | |||||
| SubElement(root, 'dc:date').text = self.date | |||||
| return root | |||||
| return root | |||||
| class AudioBroadcast(AudioItem): | class AudioBroadcast(AudioItem): | ||||
| klass = AudioItem.klass + '.audioBroadcast' | |||||
| klass = AudioItem.klass + '.audioBroadcast' | |||||
| class AudioBook(AudioItem): | class AudioBook(AudioItem): | ||||
| klass = AudioItem.klass + '.audioBook' | |||||
| klass = AudioItem.klass + '.audioBook' | |||||
| class VideoItem(Item): | class VideoItem(Item): | ||||
| klass = Item.klass + '.videoItem' | |||||
| klass = Item.klass + '.videoItem' | |||||
| class Movie(VideoItem): | class Movie(VideoItem): | ||||
| klass = VideoItem.klass + '.movie' | |||||
| klass = VideoItem.klass + '.movie' | |||||
| class VideoBroadcast(VideoItem): | class VideoBroadcast(VideoItem): | ||||
| klass = VideoItem.klass + '.videoBroadcast' | |||||
| klass = VideoItem.klass + '.videoBroadcast' | |||||
| class MusicVideoClip(VideoItem): | class MusicVideoClip(VideoItem): | ||||
| klass = VideoItem.klass + '.musicVideoClip' | |||||
| klass = VideoItem.klass + '.musicVideoClip' | |||||
| class PlaylistItem(Item): | class PlaylistItem(Item): | ||||
| klass = Item.klass + '.playlistItem' | |||||
| klass = Item.klass + '.playlistItem' | |||||
| class TextItem(Item): | class TextItem(Item): | ||||
| klass = Item.klass + '.textItem' | |||||
| klass = Item.klass + '.textItem' | |||||
| class Container(Object, list): | class Container(Object, list): | ||||
| """An object that can contain other objects.""" | |||||
| """An object that can contain other objects.""" | |||||
| klass = Object.klass + '.container' | |||||
| klass = Object.klass + '.container' | |||||
| elementName = 'container' | |||||
| childCount = property(lambda x: len(x)) | |||||
| createClass = None | |||||
| searchClass = None | |||||
| searchable = None | |||||
| elementName = 'container' | |||||
| childCount = property(lambda x: len(x)) | |||||
| createClass = None | |||||
| searchClass = None | |||||
| searchable = None | |||||
| def __init__(self, cd, id, parentID, title, restricted = 0, creator = None): | |||||
| Object.__init__(self, cd, id, parentID, title, restricted, creator) | |||||
| def __init__(self, cd, id, parentID, title, restricted = 0, creator = None): | |||||
| Object.__init__(self, cd, id, parentID, title, restricted, creator) | |||||
| list.__init__(self) | list.__init__(self) | ||||
| def toElement(self): | |||||
| def toElement(self): | |||||
| root = Object.toElement(self) | |||||
| root = Object.toElement(self) | |||||
| root.attrib['childCount'] = str(self.childCount) | |||||
| root.attrib['childCount'] = str(self.childCount) | |||||
| if self.createClass is not None: | |||||
| SubElement(root, 'upnp:createclass').text = self.createClass | |||||
| if self.searchClass is not None: | |||||
| if not isinstance(self.searchClass, (list, tuple)): | |||||
| self.searchClass = ['searchClass'] | |||||
| for i in searchClass: | |||||
| SubElement(root, 'upnp:searchclass').text = i | |||||
| if self.createClass is not None: | |||||
| SubElement(root, 'upnp:createclass').text = self.createClass | |||||
| if self.searchable is not None: | |||||
| root.attrib['searchable'] = str(self.searchable) | |||||
| if self.searchClass is not None: | |||||
| if not isinstance(self.searchClass, (list, tuple)): | |||||
| self.searchClass = ['searchClass'] | |||||
| for i in searchClass: | |||||
| SubElement(root, 'upnp:searchclass').text = i | |||||
| return root | |||||
| if self.searchable is not None: | |||||
| root.attrib['searchable'] = str(self.searchable) | |||||
| return root | |||||
| class Person(Container): | class Person(Container): | ||||
| klass = Container.klass + '.person' | |||||
| klass = Container.klass + '.person' | |||||
| class MusicArtist(Person): | class MusicArtist(Person): | ||||
| klass = Person.klass + '.musicArtist' | |||||
| klass = Person.klass + '.musicArtist' | |||||
| class PlaylistContainer(Container): | class PlaylistContainer(Container): | ||||
| klass = Container.klass + '.playlistContainer' | |||||
| klass = Container.klass + '.playlistContainer' | |||||
| class Album(Container): | class Album(Container): | ||||
| klass = Container.klass + '.album' | |||||
| klass = Container.klass + '.album' | |||||
| class MusicAlbum(Album): | class MusicAlbum(Album): | ||||
| klass = Album.klass + '.musicAlbum' | |||||
| klass = Album.klass + '.musicAlbum' | |||||
| class PhotoAlbum(Album): | class PhotoAlbum(Album): | ||||
| klass = Album.klass + '.photoAlbum' | |||||
| klass = Album.klass + '.photoAlbum' | |||||
| class Genre(Container): | class Genre(Container): | ||||
| klass = Container.klass + '.genre' | |||||
| klass = Container.klass + '.genre' | |||||
| class MusicGenre(Genre): | class MusicGenre(Genre): | ||||
| klass = Genre.klass + '.musicGenre' | |||||
| klass = Genre.klass + '.musicGenre' | |||||
| class MovieGenre(Genre): | class MovieGenre(Genre): | ||||
| klass = Genre.klass + '.movieGenre' | |||||
| klass = Genre.klass + '.movieGenre' | |||||
| class StorageSystem(Container): | class StorageSystem(Container): | ||||
| klass = Container.klass + '.storageSystem' | |||||
| klass = Container.klass + '.storageSystem' | |||||
| total = -1 | |||||
| used = -1 | |||||
| free = -1 | |||||
| maxpartition = -1 | |||||
| medium = 'UNKNOWN' | |||||
| total = -1 | |||||
| used = -1 | |||||
| free = -1 | |||||
| maxpartition = -1 | |||||
| medium = 'UNKNOWN' | |||||
| def toElement(self): | |||||
| def toElement(self): | |||||
| root = Container.toElement(self) | |||||
| root = Container.toElement(self) | |||||
| SubElement(root, 'upnp:storageTotal').text = str(self.total) | |||||
| SubElement(root, 'upnp:storageUsed').text = str(self.used) | |||||
| SubElement(root, 'upnp:storageFree').text = str(self.free) | |||||
| SubElement(root, 'upnp:storageMaxPartition').text = str(self.maxpartition) | |||||
| SubElement(root, 'upnp:storageMedium').text = self.medium | |||||
| SubElement(root, 'upnp:storageTotal').text = str(self.total) | |||||
| SubElement(root, 'upnp:storageUsed').text = str(self.used) | |||||
| SubElement(root, 'upnp:storageFree').text = str(self.free) | |||||
| SubElement(root, 'upnp:storageMaxPartition').text = str(self.maxpartition) | |||||
| SubElement(root, 'upnp:storageMedium').text = self.medium | |||||
| return root | |||||
| return root | |||||
| class StorageVolume(Container): | class StorageVolume(Container): | ||||
| klass = Container.klass + '.storageVolume' | |||||
| klass = Container.klass + '.storageVolume' | |||||
| total = -1 | |||||
| used = -1 | |||||
| free = -1 | |||||
| medium = 'UNKNOWN' | |||||
| total = -1 | |||||
| used = -1 | |||||
| free = -1 | |||||
| medium = 'UNKNOWN' | |||||
| def toElement(self): | |||||
| def toElement(self): | |||||
| root = Container.toElement(self) | |||||
| SubElement(root, 'upnp:storageTotal').text = str(self.total) | |||||
| SubElement(root, 'upnp:storageUsed').text = str(self.used) | |||||
| SubElement(root, 'upnp:storageFree').text = str(self.free) | |||||
| SubElement(root, 'upnp:storageMedium').text = self.medium | |||||
| root = Container.toElement(self) | |||||
| return root | |||||
| SubElement(root, 'upnp:storageTotal').text = str(self.total) | |||||
| SubElement(root, 'upnp:storageUsed').text = str(self.used) | |||||
| SubElement(root, 'upnp:storageFree').text = str(self.free) | |||||
| SubElement(root, 'upnp:storageMedium').text = self.medium | |||||
| return root | |||||
| class StorageFolder(Container): | class StorageFolder(Container): | ||||
| klass = Container.klass + '.storageFolder' | |||||
| klass = Container.klass + '.storageFolder' | |||||
| used = -1 | |||||
| used = -1 | |||||
| def toElement(self): | |||||
| def toElement(self): | |||||
| root = Container.toElement(self) | |||||
| root = Container.toElement(self) | |||||
| if self.used is not None: | |||||
| SubElement(root, 'upnp:storageUsed').text = str(self.used) | |||||
| if self.used is not None: | |||||
| SubElement(root, 'upnp:storageUsed').text = str(self.used) | |||||
| return root | |||||
| return root | |||||
| class DIDLElement(_ElementInterface): | class DIDLElement(_ElementInterface): | ||||
| def __init__(self): | |||||
| _ElementInterface.__init__(self, 'DIDL-Lite', {}) | |||||
| self.attrib['xmlns'] = 'urn:schemas-upnp-org:metadata-1-0/DIDL-Lite' | |||||
| self.attrib['xmlns:dc'] = 'http://purl.org/dc/elements/1.1/' | |||||
| self.attrib['xmlns:upnp'] = 'urn:schemas-upnp-org:metadata-1-0/upnp' | |||||
| def __init__(self): | |||||
| _ElementInterface.__init__(self, 'DIDL-Lite', {}) | |||||
| self.attrib['xmlns'] = 'urn:schemas-upnp-org:metadata-1-0/DIDL-Lite' | |||||
| self.attrib['xmlns:dc'] = 'http://purl.org/dc/elements/1.1/' | |||||
| self.attrib['xmlns:upnp'] = 'urn:schemas-upnp-org:metadata-1-0/upnp' | |||||
| def addContainer(self, id, parentID, title, restricted = False): | |||||
| e = Container(id, parentID, title, restricted, creator = '') | |||||
| self.append(e.toElement()) | |||||
| def addContainer(self, id, parentID, title, restricted = False): | |||||
| e = Container(id, parentID, title, restricted, creator = '') | |||||
| self.append(e.toElement()) | |||||
| def addItem(self, item): | |||||
| self.append(item.toElement()) | |||||
| def addItem(self, item): | |||||
| self.append(item.toElement()) | |||||
| def numItems(self): | |||||
| return len(self) | |||||
| def numItems(self): | |||||
| return len(self) | |||||
| def toString(self): | |||||
| return tostring(self) | |||||
| def toString(self): | |||||
| return tostring(self) | |||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||
| root = DIDLElement() | |||||
| root.addContainer('0\Movie\\', '0\\', 'Movie') | |||||
| root.addContainer('0\Music\\', '0\\', 'Music') | |||||
| root.addContainer('0\Photo\\', '0\\', 'Photo') | |||||
| root.addContainer('0\OnlineMedia\\', '0\\', 'OnlineMedia') | |||||
| root = DIDLElement() | |||||
| root.addContainer('0\Movie\\', '0\\', 'Movie') | |||||
| root.addContainer('0\Music\\', '0\\', 'Music') | |||||
| root.addContainer('0\Photo\\', '0\\', 'Photo') | |||||
| root.addContainer('0\OnlineMedia\\', '0\\', 'OnlineMedia') | |||||
| print tostring(root) | |||||
| print tostring(root) | |||||
| @@ -26,167 +26,155 @@ SSDP_ADDR = '239.255.255.250' | |||||
| # class to handle services etc. | # class to handle services etc. | ||||
| class SSDPServer(DatagramProtocol): | class SSDPServer(DatagramProtocol): | ||||
| """A class implementing a SSDP server. The notifyReceived and | |||||
| searchReceived methods are called when the appropriate type of | |||||
| datagram is received by the server.""" | |||||
| """A class implementing a SSDP server. The notifyReceived and | |||||
| searchReceived methods are called when the appropriate type of | |||||
| datagram is received by the server.""" | |||||
| # not used yet | |||||
| stdheaders = [ ('Server', 'Twisted, UPnP/1.0, python-upnp'), ] | |||||
| elements = {} | |||||
| known = {} | |||||
| # not used yet | |||||
| stdheaders = [ ('Server', 'Twisted, UPnP/1.0, python-upnp'), ] | |||||
| elements = {} | |||||
| known = {} | |||||
| def doStop(self): | |||||
| def doStop(self): | |||||
| '''Make sure we send out the byebye notifications.''' | '''Make sure we send out the byebye notifications.''' | ||||
| self.transport.write('foobar', (SSDP_ADDR, SSDP_PORT)) | |||||
| for st in self.known: | |||||
| self.doByebye(st) | |||||
| DatagramProtocol.doStop(self) | |||||
| def datagramReceived(self, data, (host, port)): | |||||
| """Handle a received multicast datagram.""" | |||||
| # Break up message in to command and headers | |||||
| # TODO: use the email module after trimming off the request line.. | |||||
| # This gets us much better header support. | |||||
| header, payload = data.split('\r\n\r\n') | |||||
| lines = header.split('\r\n') | |||||
| cmd = string.split(lines[0], ' ') | |||||
| lines = map(lambda x: x.replace(': ', ':', 1), lines[1:]) | |||||
| lines = filter(lambda x: len(x) > 0, lines) | |||||
| headers = [string.split(x, ':', 1) for x in lines] | |||||
| headers = dict(map(lambda x: (x[0].lower(), x[1]), headers)) | |||||
| if cmd[0] == 'M-SEARCH' and cmd[1] == '*': | |||||
| # SSDP discovery | |||||
| self.discoveryRequest(headers, (host, port)) | |||||
| elif cmd[0] == 'NOTIFY' and cmd[1] == '*': | |||||
| # SSDP presence | |||||
| self.notifyReceived(headers, (host, port)) | |||||
| else: | |||||
| log.msg('Unknown SSDP command %s %s' % cmd) | |||||
| def discoveryRequest(self, headers, (host, port)): | |||||
| """Process a discovery request. The response must be sent to | |||||
| the address specified by (host, port).""" | |||||
| log.msg('Discovery request for %s' % headers['st']) | |||||
| # Do we know about this service? | |||||
| if headers['st'] == 'ssdp:all': | |||||
| for i in self.known: | |||||
| hcopy = dict(headers.iteritems()) | |||||
| hcopy['st'] = i | |||||
| self.discoveryRequest(hcopy, (host, post)) | |||||
| return | |||||
| if not self.known.has_key(headers['st']): | |||||
| return | |||||
| # Generate a response | |||||
| response = [] | |||||
| response.append('HTTP/1.1 200 OK') | |||||
| for k, v in self.known[headers['st']].items(): | |||||
| response.append('%s: %s' % (k, v)) | |||||
| response.extend(('', '')) | |||||
| delay = random.randint(0, int(headers['mx'])) | |||||
| reactor.callLater(delay, self.transport.write, | |||||
| '\r\n'.join(response), (host, port)) | |||||
| def register(self, usn, st, location): | |||||
| """Register a service or device that this SSDP server will | |||||
| respond to.""" | |||||
| log.msg('Registering %s' % st) | |||||
| self.known[st] = {} | |||||
| self.known[st]['USN'] = usn | |||||
| self.known[st]['LOCATION'] = location | |||||
| self.known[st]['ST'] = st | |||||
| self.known[st]['EXT'] = '' | |||||
| self.known[st]['SERVER'] = 'Twisted, UPnP/1.0, python-upnp' | |||||
| self.known[st]['CACHE-CONTROL'] = 'max-age=1800' | |||||
| self.doNotify(st) | |||||
| def doByebye(self, st): | |||||
| """Do byebye""" | |||||
| log.msg('Sending byebye notification for %s' % st) | |||||
| resp = [ 'NOTIFY * HTTP/1.1', | |||||
| 'Host: %s:%d' % (SSDP_ADDR, SSDP_PORT), | |||||
| 'NTS: ssdp:byebye', | |||||
| ] | |||||
| stcpy = dict(self.known[st].iteritems()) | |||||
| stcpy['NT'] = stcpy['ST'] | |||||
| del stcpy['ST'] | |||||
| resp.extend(map(lambda x: ': '.join(x), stcpy.iteritems())) | |||||
| resp.extend(('', '')) | |||||
| self.transport.write('\r\n'.join(resp), (SSDP_ADDR, SSDP_PORT)) | |||||
| def doNotify(self, st): | |||||
| """Do notification""" | |||||
| log.msg('Sending alive notification for %s' % st) | |||||
| resp = [ 'NOTIFY * HTTP/1.1', | |||||
| 'Host: %s:%d' % (SSDP_ADDR, SSDP_PORT), | |||||
| 'NTS: ssdp:alive', | |||||
| ] | |||||
| stcpy = dict(self.known[st].iteritems()) | |||||
| stcpy['NT'] = stcpy['ST'] | |||||
| del stcpy['ST'] | |||||
| resp.extend(map(lambda x: ': '.join(x), stcpy.iteritems())) | |||||
| resp.extend(('', '')) | |||||
| self.transport.write('\r\n'.join(resp), (SSDP_ADDR, SSDP_PORT)) | |||||
| def notifyReceived(self, headers, (host, port)): | |||||
| """Process a presence announcement. We just remember the | |||||
| details of the SSDP service announced.""" | |||||
| if headers['nts'] == 'ssdp:alive': | |||||
| if not self.elements.has_key(headers['nt']): | |||||
| # Register device/service | |||||
| self.elements[headers['nt']] = {} | |||||
| self.elements[headers['nt']]['USN'] = headers['usn'] | |||||
| self.elements[headers['nt']]['host'] = (host, port) | |||||
| log.msg('Detected presence of %s' % headers['nt']) | |||||
| elif headers['nts'] == 'ssdp:byebye': | |||||
| if self.elements.has_key(headers['nt']): | |||||
| # Unregister device/service | |||||
| del(self.elements[headers['nt']]) | |||||
| log.msg('Detected absence for %s' % headers['nt']) | |||||
| else: | |||||
| log.msg('Unknown subtype %s for notification type %s' % | |||||
| (headers['nts'], headers['nt'])) | |||||
| def findService(self, name): | |||||
| """Return information about a service registered over SSDP.""" | |||||
| # TODO: Implement me. | |||||
| # TODO: Send out a discovery request if we haven't registered | |||||
| # a presence announcement. | |||||
| def findDevice(self, name): | |||||
| """Return information about a device registered over SSDP.""" | |||||
| # TODO: Implement me. | |||||
| # TODO: Send out a discovery request if we haven't registered | |||||
| # a presence announcement. | |||||
| self.transport.write('foobar', (SSDP_ADDR, SSDP_PORT)) | |||||
| for st in self.known: | |||||
| self.doByebye(st) | |||||
| DatagramProtocol.doStop(self) | |||||
| def datagramReceived(self, data, (host, port)): | |||||
| """Handle a received multicast datagram.""" | |||||
| # Break up message in to command and headers | |||||
| # TODO: use the email module after trimming off the request line.. | |||||
| # This gets us much better header support. | |||||
| header, payload = data.split('\r\n\r\n') | |||||
| lines = header.split('\r\n') | |||||
| cmd = string.split(lines[0], ' ') | |||||
| lines = map(lambda x: x.replace(': ', ':', 1), lines[1:]) | |||||
| lines = filter(lambda x: len(x) > 0, lines) | |||||
| headers = [string.split(x, ':', 1) for x in lines] | |||||
| headers = dict(map(lambda x: (x[0].lower(), x[1]), headers)) | |||||
| if cmd[0] == 'M-SEARCH' and cmd[1] == '*': | |||||
| # SSDP discovery | |||||
| self.discoveryRequest(headers, (host, port)) | |||||
| elif cmd[0] == 'NOTIFY' and cmd[1] == '*': | |||||
| # SSDP presence | |||||
| self.notifyReceived(headers, (host, port)) | |||||
| else: | |||||
| log.msg('Unknown SSDP command %s %s' % cmd) | |||||
| def discoveryRequest(self, headers, (host, port)): | |||||
| """Process a discovery request. The response must be sent to | |||||
| the address specified by (host, port).""" | |||||
| log.msg('Discovery request for %s' % headers['st']) | |||||
| # Do we know about this service? | |||||
| if headers['st'] == 'ssdp:all': | |||||
| for i in self.known: | |||||
| hcopy = dict(headers.iteritems()) | |||||
| hcopy['st'] = i | |||||
| self.discoveryRequest(hcopy, (host, post)) | |||||
| return | |||||
| if not self.known.has_key(headers['st']): | |||||
| return | |||||
| # Generate a response | |||||
| response = [] | |||||
| response.append('HTTP/1.1 200 OK') | |||||
| for k, v in self.known[headers['st']].items(): | |||||
| response.append('%s: %s' % (k, v)) | |||||
| response.extend(('', '')) | |||||
| delay = random.randint(0, int(headers['mx'])) | |||||
| reactor.callLater(delay, self.transport.write, | |||||
| '\r\n'.join(response), (host, port)) | |||||
| def register(self, usn, st, location): | |||||
| """Register a service or device that this SSDP server will | |||||
| respond to.""" | |||||
| log.msg('Registering %s' % st) | |||||
| self.known[st] = {} | |||||
| self.known[st]['USN'] = usn | |||||
| self.known[st]['LOCATION'] = location | |||||
| self.known[st]['ST'] = st | |||||
| self.known[st]['EXT'] = '' | |||||
| self.known[st]['SERVER'] = 'Twisted, UPnP/1.0, python-upnp' | |||||
| self.known[st]['CACHE-CONTROL'] = 'max-age=1800' | |||||
| self.doNotify(st) | |||||
| def doByebye(self, st): | |||||
| """Do byebye""" | |||||
| log.msg('Sending byebye notification for %s' % st) | |||||
| resp = [ 'NOTIFY * HTTP/1.1', | |||||
| 'Host: %s:%d' % (SSDP_ADDR, SSDP_PORT), | |||||
| 'NTS: ssdp:byebye', | |||||
| ] | |||||
| stcpy = dict(self.known[st].iteritems()) | |||||
| stcpy['NT'] = stcpy['ST'] | |||||
| del stcpy['ST'] | |||||
| resp.extend(map(lambda x: ': '.join(x), stcpy.iteritems())) | |||||
| resp.extend(('', '')) | |||||
| self.transport.write('\r\n'.join(resp), (SSDP_ADDR, SSDP_PORT)) | |||||
| def doNotify(self, st): | |||||
| """Do notification""" | |||||
| log.msg('Sending alive notification for %s' % st) | |||||
| resp = [ 'NOTIFY * HTTP/1.1', | |||||
| 'Host: %s:%d' % (SSDP_ADDR, SSDP_PORT), | |||||
| 'NTS: ssdp:alive', | |||||
| ] | |||||
| stcpy = dict(self.known[st].iteritems()) | |||||
| stcpy['NT'] = stcpy['ST'] | |||||
| del stcpy['ST'] | |||||
| resp.extend(map(lambda x: ': '.join(x), stcpy.iteritems())) | |||||
| resp.extend(('', '')) | |||||
| self.transport.write('\r\n'.join(resp), (SSDP_ADDR, SSDP_PORT)) | |||||
| def notifyReceived(self, headers, (host, port)): | |||||
| """Process a presence announcement. We just remember the | |||||
| details of the SSDP service announced.""" | |||||
| if headers['nts'] == 'ssdp:alive': | |||||
| if not self.elements.has_key(headers['nt']): | |||||
| # Register device/service | |||||
| self.elements[headers['nt']] = {} | |||||
| self.elements[headers['nt']]['USN'] = headers['usn'] | |||||
| self.elements[headers['nt']]['host'] = (host, port) | |||||
| log.msg('Detected presence of %s' % headers['nt']) | |||||
| elif headers['nts'] == 'ssdp:byebye': | |||||
| if self.elements.has_key(headers['nt']): | |||||
| # Unregister device/service | |||||
| del(self.elements[headers['nt']]) | |||||
| log.msg('Detected absence for %s' % headers['nt']) | |||||
| else: | |||||
| log.msg('Unknown subtype %s for notification type %s' % | |||||
| (headers['nts'], headers['nt'])) | |||||
| def findService(self, name): | |||||
| """Return information about a service registered over SSDP.""" | |||||
| # TODO: Implement me. | |||||
| # TODO: Send out a discovery request if we haven't registered | |||||
| # a presence announcement. | |||||
| def findDevice(self, name): | |||||
| """Return information about a device registered over SSDP.""" | |||||
| # TODO: Implement me. | |||||
| # TODO: Send out a discovery request if we haven't registered | |||||
| # a presence announcement. | |||||
| @@ -12,8 +12,8 @@ from twisted.internet import reactor, error | |||||
| from twisted.internet.protocol import Protocol, ClientFactory | from twisted.internet.protocol import Protocol, ClientFactory | ||||
| class Send(Protocol): | class Send(Protocol): | ||||
| def connectionMade(self): | |||||
| self.transport.write('''POST /ContentDirectory/control HTTP/1.1\r | |||||
| def connectionMade(self): | |||||
| self.transport.write('''POST /ContentDirectory/control HTTP/1.1\r | |||||
| Host: 192.168.126.1:80\r | Host: 192.168.126.1:80\r | ||||
| User-Agent: POSIX, UPnP/1.0, Intel MicroStack/1.0.1423\r | User-Agent: POSIX, UPnP/1.0, Intel MicroStack/1.0.1423\r | ||||
| SOAPACTION: "urn:schemas-upnp-org:service:ContentDirectory:1#Browse"\r | SOAPACTION: "urn:schemas-upnp-org:service:ContentDirectory:1#Browse"\r | ||||
| @@ -35,16 +35,16 @@ Content-Length: 511\r | |||||
| </s:Body>\r | </s:Body>\r | ||||
| </s:Envelope>\r\n''') | </s:Envelope>\r\n''') | ||||
| def dataReceived(self, data): | |||||
| print(data) | |||||
| def dataReceived(self, data): | |||||
| print(data) | |||||
| def connectionLost(self, reason): | |||||
| if reason.type != error.ConnectionDone: | |||||
| print str(reason) | |||||
| reactor.stop() | |||||
| def connectionLost(self, reason): | |||||
| if reason.type != error.ConnectionDone: | |||||
| print str(reason) | |||||
| reactor.stop() | |||||
| class SendFactory(ClientFactory): | class SendFactory(ClientFactory): | ||||
| protocol = Send | |||||
| protocol = Send | |||||
| host = '192.168.126.128' | host = '192.168.126.128' | ||||
| port = 5643 | port = 5643 | ||||
| @@ -15,38 +15,38 @@ from twisted.python import usage | |||||
| import sys, string, SOAPpy | import sys, string, SOAPpy | ||||
| class UPnPSOAPProxy: | class UPnPSOAPProxy: | ||||
| """A proxy for making UPnP SOAP calls.""" | |||||
| """A proxy for making UPnP SOAP calls.""" | |||||
| def __init__(self, url): | |||||
| self.url = url | |||||
| def __init__(self, url): | |||||
| self.url = url | |||||
| def _cbGotResult(self, result): | |||||
| return SOAPpy.parseSOAPRPC(result) | |||||
| def _cbGotResult(self, result): | |||||
| return SOAPpy.parseSOAPRPC(result) | |||||
| def callRemote(self, method, *args, **kwargs): | |||||
| def callRemote(self, method, *args, **kwargs): | |||||
| payload = SOAPpy.buildSOAP(args = args, kw = kwargs, method = method) | |||||
| payload = string.replace(payload, '\n', '\r\n') | |||||
| action = \ | |||||
| '"urn:schemas-upnp-org:service:ContentDirectory:1#%s"' % method | |||||
| page = client.getPage(self.url, postdata = payload, method = 'POST', | |||||
| headers = {'Content-Type': 'text/xml', | |||||
| 'SOAPACTION': action}) | |||||
| payload = SOAPpy.buildSOAP(args = args, kw = kwargs, method = method) | |||||
| payload = string.replace(payload, '\n', '\r\n') | |||||
| action = \ | |||||
| '"urn:schemas-upnp-org:service:ContentDirectory:1#%s"' % method | |||||
| page = client.getPage(self.url, postdata = payload, | |||||
| method = 'POST', headers = {'Content-Type': 'text/xml', | |||||
| 'SOAPACTION': action}) | |||||
| return page.addCallback(self._cbGotResult) | |||||
| return page.addCallback(self._cbGotResult) | |||||
| class Options(usage.Options): | class Options(usage.Options): | ||||
| pass | |||||
| pass | |||||
| def printResult(value): | def printResult(value): | ||||
| print value | |||||
| reactor.stop() | |||||
| print value | |||||
| reactor.stop() | |||||
| def printError(error): | def printError(error): | ||||
| print 'error', error | |||||
| reactor.stop() | |||||
| print 'error', error | |||||
| reactor.stop() | |||||
| #proxy = UPnPSOAPProxy('http://192.168.126.128:5643/ContentDirectory/control') | #proxy = UPnPSOAPProxy('http://192.168.126.128:5643/ContentDirectory/control') | ||||
| proxy = UPnPSOAPProxy('http://127.0.0.1:8080/ContentDirectory/control') | proxy = UPnPSOAPProxy('http://127.0.0.1:8080/ContentDirectory/control') | ||||
| @@ -56,12 +56,12 @@ proxy = UPnPSOAPProxy('http://127.0.0.1:8080/ContentDirectory/control') | |||||
| #proxy.callRemote('GetSortCapabilities').addCallbacks(printResult, printError) | #proxy.callRemote('GetSortCapabilities').addCallbacks(printResult, printError) | ||||
| proxy.callRemote('Browse', | proxy.callRemote('Browse', | ||||
| ObjectID = '0\\Music\\Genres\\Others\\chimes.wav', | |||||
| # BrowseFlag = 'BrowseDirectChildren', | |||||
| BrowseFlag = 'BrowseMetadata', | |||||
| Filter = '*', | |||||
| StartingIndex = 0, | |||||
| RequestedCount = 700, | |||||
| SortCriteria = None).addCallbacks(printResult, printError) | |||||
| ObjectID = '0\\Music\\Genres\\Others\\chimes.wav', | |||||
| #BrowseFlag = 'BrowseDirectChildren', | |||||
| BrowseFlag = 'BrowseMetadata', | |||||
| Filter = '*', | |||||
| StartingIndex = 0, | |||||
| RequestedCount = 700, | |||||
| SortCriteria = None).addCallbacks(printResult, printError) | |||||
| reactor.run() | reactor.run() | ||||
| @@ -8,11 +8,9 @@ from twisted.web import soap | |||||
| import SOAPpy | import SOAPpy | ||||
| class UPnPPublisher(soap.SOAPPublisher): | class UPnPPublisher(soap.SOAPPublisher): | ||||
| """UPnP requires OUT parameters to be returned in a slightly | |||||
| different way than the SOAPPublisher class does.""" | |||||
| def _gotResult(self, result, request, methodName): | |||||
| response = SOAPpy.buildSOAP(kw=result, | |||||
| encoding=self.encoding) | |||||
| self._sendResponse(request, response) | |||||
| """UPnP requires OUT parameters to be returned in a slightly | |||||
| different way than the SOAPPublisher class does.""" | |||||
| def _gotResult(self, result, request, methodName): | |||||
| response = SOAPpy.buildSOAP(kw=result, encoding=self.encoding) | |||||
| self._sendResponse(request, response) | |||||
| @@ -12,7 +12,7 @@ from xml.dom import minidom | |||||
| str = "" | str = "" | ||||
| for s in sys.stdin.readlines(): | for s in sys.stdin.readlines(): | ||||
| str = str + s[:-1] # Eat trailing \n | |||||
| str = str + s[:-1] # Eat trailing \n | |||||
| doc = minidom.parseString(str) | doc = minidom.parseString(str) | ||||
| print doc.toprettyxml(indent = " ") | print doc.toprettyxml(indent = " ") | ||||