Browse Source

import a python media server based upon twisted among other things..

from: http://frungy.org/~tpot/weblog/2006/01/19#python-upnp-abandoned

[git-p4: depot-paths = "//depot/": change = 717]
replace/5b80aeb26dc425aaddcd5182126c969e5cc04cbb
John-Mark Gurney 19 years ago
commit
ed587e94d3
19 changed files with 1053 additions and 0 deletions
  1. +19
    -0
      ConnectionManager.py
  2. +202
    -0
      ContentDirectory.py
  3. +304
    -0
      DIDLLite.py
  4. +9
    -0
      MediaServer.py
  5. +8
    -0
      README
  6. +143
    -0
      SSDP.py
  7. +53
    -0
      browse-zms.py
  8. +67
    -0
      cdsclient
  9. +6
    -0
      connection-manager-scpd.xml
  10. +7
    -0
      content-directory-scpd.xml
  11. +77
    -0
      pymediaserv
  12. +35
    -0
      root-device.xml
  13. +18
    -0
      upnp.py
  14. +32
    -0
      xml/browsemusic.xml
  15. +11
    -0
      xml/browsemusicgenres.xml
  16. +14
    -0
      xml/browsephotos.xml
  17. +22
    -0
      xml/browsetoplevel.xml
  18. +8
    -0
      xml/browsetrackmetadata.xml
  19. +18
    -0
      xmlpretty

+ 19
- 0
ConnectionManager.py View File

@@ -0,0 +1,19 @@
# Licensed under the MIT license
# http://opensource.org/licenses/mit-license.php

# (c) 2005, Tim Potter <tpot@samba.org>

# Connection Manager service

from twisted.python import log
from twisted.web import resource, static, soap
from upnp import UPnPPublisher

class ConnectionManagerControl(UPnPPublisher):
pass

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())

+ 202
- 0
ContentDirectory.py View File

@@ -0,0 +1,202 @@
# Licensed under the MIT license
# http://opensource.org/licenses/mit-license.php

# (c) 2005, Tim Potter <tpot@samba.org>

#
# This module implements the Content Directory Service (CDS) service
# type as documented in the ContentDirectory:1 Service Template
# Version 1.01
#

#
# TODO: Figure out a nicer pattern for debugging soap server calls as
# twisted swallows the tracebacks. At the moment I'm going:
#
# try:
# ....
# except:
# traceback.print_exc(file = log.logfile)
#

from twisted.python import log
from twisted.web import resource, static

from elementtree.ElementTree import Element, SubElement, tostring
from upnp import UPnPPublisher
from DIDLLite import DIDLElement, Container, Movie, Resource, MusicTrack

import traceback
from urllib import quote

class ContentDirectoryControl(UPnPPublisher):
"""This class implements the CDS actions over SOAP."""

# Required actions

def soap_GetSearchCapabilities(self, *args, **kwargs):
"""Return the searching capabilities supported by the device."""

log.msg('GetSearchCapabilities()')

def soap_GetSortCapabilities(self, *args, **kwargs):
"""Return the CSV list of meta-data tags that can be used in
sortCriteria."""

log.msg('GetSortCapabilities()')
def soap_GetSystemUpdateID(self, *args, **kwargs):
"""Return the current value of state variable SystemUpdateID."""

log.msg('GetSystemUpdateID()')

BrowseFlags = ('BrowseMetaData', 'BrowseDirectChildren')

def soap_Browse(self, *args):
"""Incrementally browse the native heirachy of the Content
Directory objects exposed by the Content Directory Service."""

(ObjectID, BrowseFlag, Filter, StartingIndex, RequestedCount,
SortCriteria) = args

log.msg('Browse(ObjectID=%s, BrowseFlags=%s, Filter=%s, '
'StartingIndex=%s RequestedCount=%s SortCriteria=%s)' %
(`ObjectID`, `BrowseFlag`, `Filter`, `StartingIndex`,
`RequestedCount`, `SortCriteria`))

didl = DIDLElement()

try:

if ObjectID == '0\\Music\\':
c = Container('0\\Music\\Spotty\\', '0\\Music\\', 'Spotty')
c.childCount = 6
c.searchable = 0
didl.addItem(c)

c = Container('0\\Music\\Foot\\', '0\\Music\\', 'Foot')
c.childCount = 1
c.searchable = 0
didl.addItem(c)


if ObjectID == '0\\Music\\Spotty\\':

m = MusicTrack('0\\Music\\media\\foo.mp3', '0\\Music\\', 'foo.mp3')

m.res = Resource('http://192.168.1.105:8080/media/foo.mp3', 'http-get:*:audio/mpeg:*')
m.res.size = 1234
m.res.bitrate = 256
m.genre = 'Others'
m.artist = 'Others'
m.album = 'Others'
didl.addItem(m)

result = {'BrowseResponse': {'Result': didl.toString() ,
'NumberReturned': didl.numItems(),
'TotalMatches': didl.numItems(),
'UpdateID': 0}}

except:
traceback.print_exc(file=log.logfile)

log.msg('Returning: %s' % result)

return result

# Optional actions

def soap_Search(self, *args, **kwargs):
"""Search for objects that match some search criteria."""

(ContainerID, SearchCriteria, Filter, StartingIndex,
RequestedCount, SortCriteria) = args

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, Elements) = args

log.msg('CreateObject(ContainerID=%s, Elements=%s)' %
(`ContainerID`, `Elements`))

def soap_DestroyObject(self, *args, **kwargs):
"""Destroy the specified object."""

(ObjectID) = args

log.msg('DestroyObject(ObjectID=%s)' % `ObjectID`)

def soap_UpdateObject(self, *args, **kwargs):
"""Modify, delete or insert object metadata."""

(ObjectID, CurrentTagValue, NewTagValue) = args

log.msg('UpdateObject(ObjectID=%s, CurrentTagValue=%s, ' \
'NewTagValue=%s)' % (`ObjectID`, `CurrentTagValue`,
`NewTagValue`))

def soap_ImportResource(self, *args, **kwargs):
"""Transfer a file from a remote source to a local
destination in the Content Directory Service."""

(SourceURI, DestinationURI) = args

log.msg('ImportResource(SourceURI=%s, DestinationURI=%s)' %
(`SourceURI`, `DestinationURI`))

def soap_ExportResource(self, *args, **kwargs):
"""Transfer a file from a local source to a remote
destination."""

(SourceURI, DestinationURI) = args

log.msg('ExportResource(SourceURI=%s, DestinationURI=%s)' %
(`SourceURI`, `DestinationURI`))

def soap_StopTransferResource(self, *args, **kwargs):
"""Stop a file transfer initiated by ImportResource or
ExportResource."""

(TransferID) = args

log.msg('StopTransferResource(TransferID=%s)' % TransferID)

def soap_GetTransferProgress(self, *args, **kwargs):
"""Query the progress of a file transfer initiated by
an ImportResource or ExportResource action."""

(TransferID, TransferStatus, TransferLength, TransferTotal) = args

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."""

(ResourceURI) = args

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):
def __init__(self):
resource.Resource.__init__(self)
self.putChild('scpd.xml', static.File('content-directory-scpd.xml'))
self.putChild('control', ContentDirectoryControl())

+ 304
- 0
DIDLLite.py View File

@@ -0,0 +1,304 @@
# Licensed under the MIT license
# http://opensource.org/licenses/mit-license.php

# (c) 2005, Tim Potter <tpot@samba.org>

from elementtree.ElementTree import Element, SubElement, tostring, _ElementInterface

class 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):

root = Element('res')
root.attrib['protocolInfo'] = self.protocolInfo
root.text = self.data

if self.bitrate is not None:
root.attrib['bitrate'] = str(self.bitrate)

if self.size is not None:
root.attrib['size'] = str(self.size)

return root

class Object:
"""The root class of the entire content directory class heirachy."""

klass = 'object'
creator = None
res = None
writeStatus = None

def __init__(self, id, parentID, title, restricted = False,
creator = None):

self.id = id
self.parentID = parentID
self.title = title
self.creator = creator
if restricted:
self.restricted = '1'
else:
self.restricted = '0'

def toElement(self):

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['restricted'] = self.restricted

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.writeStatus is not None:
SubElement(root, 'upnp:writeStatus').text = self.writeStatus

return root
def toString(self):
return tostring(self.toElement())

class Item(Object):
"""A class used to represent atomic (non-container) content
objects."""

klass = Object.klass + '.item'
elementName = 'item'
refID = None

def toElement(self):

root = Object.toElement(self)

if self.refID is not None:
SubElement(root, 'refID').text = self.refID

return root

class ImageItem(Item):
klass = Item.klass + '.imageItem'

class Photo(ImageItem):
klass = ImageItem.klass + '.photo'

class AudioItem(Item):
"""A piece of content that when rendered generates some audio."""

klass = Item.klass + '.audioItem'

genre = None
description = None
longDescription = None
publisher = None
language = None
relation = None
rights = None

def toElement(self):

root = Item.toElement(self)
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.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.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.rights is not None:
SubElement(root, 'dc:rights').text = self.rights

return root

class MusicTrack(AudioItem):
"""A discrete piece of audio that should be interpreted as music."""

klass = AudioItem.klass + '.musicTrack'

artist = None
album = None
originalTrackNumber = None
playlist = None
storageMedium = None
contributor = None
date = None

def toElement(self):

root = AudioItem.toElement(self)

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.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.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.date is not None:
SubElement(root, 'dc:date').text = self.date

return root

class AudioBroadcast(AudioItem):
klass = AudioItem.klass + '.audioBroadcast'

class AudioBook(AudioItem):
klass = AudioItem.klass + '.audioBook'

class VideoItem(Item):
klass = Item.klass + '.videoItem'

class Movie(VideoItem):
klass = VideoItem.klass + '.movie'

class VideoBroadcast(VideoItem):
klass = VideoItem.klass + '.videoBroadcast'

class MusicVideoClip(VideoItem):
klass = VideoItem.klass + '.musicVideoClip'

class PlaylistItem(Item):
klass = Item.klass + '.playlistItem'

class TextItem(Item):
klass = Item.klass + '.textItem'

class Container(Object):
"""An object that can contain other objects."""

klass = Object.klass + '.container'

elementName = 'container'
childCount = 0
createClass = None
searchClass = None
searchable = None

def __init__(self, id, parentID, title, restricted = 0, creator = None):
Object.__init__(self, id, parentID, title, restricted, creator)

def toElement(self):

root = Object.toElement(self)

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.searchable is not None:
root.attrib['searchable'] = str(self.searchable)

return root

class Person(Container):
klass = Container.klass + '.person'

class MusicArtist(Person):
klass = Person.klass + '.musicArtist'

class PlaylistContainer(Container):
klass = Container.klass + '.playlistContainer'

class Album(Container):
klass = Container.klass + '.album'

class MusicAlbum(Album):
klass = Album.klass + '.musicAlbum'

class PhotoAlbum(Album):
klass = Album.klass + '.photoAlbum'

class Genre(Container):
klass = Container.klass + '.genre'

class MusicGenre(Genre):
klass = Genre.klass + '.musicGenre'

class MovieGenre(Genre):
klass = Genre.klass + '.movieGenre'

class StorageSystem(Container):
klass = Container.klass + '.storageSystem'

class StorageVolume(Container):
klass = Container.klass + '.storageVolume'

class StorageFolder(Container):
klass = Container.klass + '.storageFolder'

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 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 numItems(self):
return len(self)

def toString(self):
return tostring(self)

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')

print tostring(root)

+ 9
- 0
MediaServer.py View File

@@ -0,0 +1,9 @@
# Licensed under the MIT license
# http://opensource.org/licenses/mit-license.php

# (c) 2005, Tim Potter <tpot@samba.org>

from twisted.web import static

class MediaServer(static.File):
pass

+ 8
- 0
README View File

@@ -0,0 +1,8 @@
This code has been abandoned, but released in the hope that it might
be useful for someone. It is licensed under the MIT license at:

http://opensource.org/licenses/mit-license.php


Tim Potter <tpot@samba.org>
2006-01-19

+ 143
- 0
SSDP.py View File

@@ -0,0 +1,143 @@
# Licensed under the MIT license
# http://opensource.org/licenses/mit-license.php

# (c) 2005, Tim Potter <tpot@samba.org>

#
# Implementation of SSDP server under Twisted Python.
#

import string

from twisted.python import log
from twisted.internet.protocol import DatagramProtocol

# TODO: Is there a better way of hooking the SSDPServer into a reactor
# without having to know the default SSDP port and multicast address?
# There must be a twisted idiom for doing this.

SSDP_PORT = 1900
SSDP_ADDR = '239.255.255.250'

# TODO: Break out into a HTTPOverUDP class and implement
# process_SEARCH(), process_NOTIFY() methods. Create UPNP specific
# class to handle services etc.

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."""
elements = {}
known = {}

def datagramReceived(self, data, (host, port)):
"""Handle a received multicast datagram."""

# Break up message in to command and headers

data = string.replace(data, '\r', '')
data = string.replace(data, ': ', ':')
lines = string.split(data, '\n')
lines = filter(lambda x: len(x) > 0, lines)
cmd = string.split(lines[0], ' ')

headers = dict([string.split(x, ':', 1) for x in lines[1:]])

# TODO: datagram may contain a payload, i.e Content-Length
# header is > 0. Maybe use some twisted HTTP object to do the
# parsing for us.

# SSDP discovery

if cmd[0] == 'M-SEARCH' and cmd[1] == '*':
self.discoveryRequest(headers, (host, port))

# SSDP presence

if cmd[0] == 'NOTIFY' and cmd[1] == '*':
self.notifyReceived(headers, (host, port))

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 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))

log.msg('responding with: %s' % response)

self.transport.write(
string.join(response, '\r\n') + '\r\n\r\n', (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'

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 for %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.

+ 53
- 0
browse-zms.py View File

@@ -0,0 +1,53 @@
#!/usr/bin/python
#
# Small client for sending text to a socket and displaying the result.
#

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

# (c) 2005, Tim Potter <tpot@samba.org>

from twisted.internet import reactor, error
from twisted.internet.protocol import Protocol, ClientFactory

class Send(Protocol):
def connectionMade(self):
self.transport.write('''POST /ContentDirectory/control HTTP/1.1\r
Host: 192.168.126.1:80\r
User-Agent: POSIX, UPnP/1.0, Intel MicroStack/1.0.1423\r
SOAPACTION: "urn:schemas-upnp-org:service:ContentDirectory:1#Browse"\r
Content-Type: text/xml\r
Content-Length: 511\r
\r
\r
<?xml version="1.0" encoding="utf-8"?>\r
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">\r
<s:Body>\r
<u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">\r
<ObjectID>0\OnlineMedia\Internet radio\</ObjectID>\r
<BrowseFlag>BrowseDirectChildren</BrowseFlag>\r
<Filter>*</Filter>\r
<StartingIndex>0</StartingIndex>\r
<RequestedCount>7</RequestedCount>\r
<SortCriteria></SortCriteria>\r
</u:Browse>\r
</s:Body>\r
</s:Envelope>\r\n''')

def dataReceived(self, data):
print(data)

def connectionLost(self, reason):
if reason.type != error.ConnectionDone:
print str(reason)
reactor.stop()

class SendFactory(ClientFactory):
protocol = Send

host = '192.168.126.128'
port = 5643

reactor.connectTCP(host, port, SendFactory())
reactor.run()

+ 67
- 0
cdsclient View File

@@ -0,0 +1,67 @@
#!/usr/bin/python
#
# Test harness for UPnP ContentDirectory:1 service.
#

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

# (c) 2005, Tim Potter <tpot@samba.org>

from twisted.web import client
from twisted.internet import reactor
from twisted.python import usage

import sys, string, SOAPpy

class UPnPSOAPProxy:
"""A proxy for making UPnP SOAP calls."""

def __init__(self, url):
self.url = url

def _cbGotResult(self, result):
return SOAPpy.parseSOAPRPC(result)

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})

return page.addCallback(self._cbGotResult)

class Options(usage.Options):
pass

def printResult(value):
print value
reactor.stop()

def printError(error):
print 'error', error
reactor.stop()

#proxy = UPnPSOAPProxy('http://192.168.126.128:5643/ContentDirectory/control')
proxy = UPnPSOAPProxy('http://127.0.0.1:8080/ContentDirectory/control')
#proxy = UPnPSOAPProxy('http://16.176.65.48:5643/ContentDirectory/control')

#proxy.callRemote('GetSearchCapabilities').addCallbacks(printResult, printError)
#proxy.callRemote('GetSortCapabilities').addCallbacks(printResult, printError)

proxy.callRemote('Browse',
ObjectID = '0\\Music\\Genres\\Others\\chimes.wav',
# BrowseFlag = 'BrowseDirectChildren',
BrowseFlag = 'BrowseMetadata',
Filter = '*',
StartingIndex = 0,
RequestedCount = 700,
SortCriteria = None).addCallbacks(printResult, printError)

reactor.run()

+ 6
- 0
connection-manager-scpd.xml View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?><scpd xmlns="urn:schemas-upnp-org:service-1-0"><specVersion><major>1</major><minor>0</minor></specVersion><actionList><action><name>GetCurrentConnectionInfo</name><argumentList><argument><name>ConnectionID</name><direction>in</direction><relatedStateVariable>A_ARG_TYPE_ConnectionID</relatedStateVariable></argument><argument><name>RcsID</name><direction>out</direction><relatedStateVariable>A_ARG_TYPE_RcsID</relatedStateVariable></argument><argument><name>AVTransportID</name><direction>out</direction><relatedStateVariable>A_ARG_TYPE_AVTransportID</relatedStateVariable></argument><argument><name>ProtocolInfo</name><direction>out</direction><relatedStateVariable>A_ARG_TYPE_ProtocolInfo</relatedStateVariable></argument><argument><name>PeerConnectionManager</name><direction>out</direction><relatedStateVariable>A_ARG_TYPE_ConnectionManager</relatedStateVariable></argument><argument
><name>PeerConnectionID</name><direction>out</direction><relatedStateVariable>A_ARG_TYPE_ConnectionID</relatedStateVariable></argument><argument><name>Direction</name><direction>out</direction><relatedStateVariable>A_ARG_TYPE_Direction</relatedStateVariable></argument><argument><name>Status</name><direction>out</direction><relatedStateVariable>A_ARG_TYPE_ConnectionStatus</relatedStateVariable></argument></argumentList></
action><action><name>GetProtocolInfo</name><argumentList><argument><name>Source</name><direction>out</direction><relatedStateVariable>SourceProtocolInfo</relatedStateVariable></argument><argument><name>Sink</name><direction>out</direction><relatedStateVariable>SinkProtocolInfo</relatedStateVariable></argument></argumentList></action><action><name>GetCurrentConnectionIDs</name><argumentList><argument><name>ConnectionIDs</name><direction>out</direction><relatedStateVariable>CurrentConnectionIDs</relatedStateVariable></argument></argumentList></action></actionList><serviceStateTable><stateVariable sendEvents="no"><name>A_ARG_TYPE_ProtocolInfo</name><dataType>string</dataType></stateVariable><stateVariable sendEvents="no"><name>A_ARG_TYPE_ConnectionStatus</name><dataType>string</dataType><allowedValueList><allowedValue>OK</allowedValue><allowedValue>ContentFormatMismatch</allowedValue><allowedValue>InsufficientBandwidth</allowedValue><allowedValue>UnreliableChannel</allowedValue><allowedValue>Unknown</allowedValu
e></allowedValueList></stateVariable><stateVariable sendEvents="no"><name>A_ARG_TYPE_AVTransportID</name><dataType>i4</dataType></stateVariable><stateVariable sendEvents="no"><name>A_ARG_TYPE_RcsID</name><dataType>i4</dataType></stateVariable><stateVariable sendEvents="no"><name>A_ARG_TYPE_ConnectionID</name><dataType>i4</dataType></stateVariable><stateVariable sendEvents="no"><name>A_ARG_TYPE_ConnectionManager</name><da
taType>string</dataType>
</stateVariable><stateVariable sendEvents="yes"><name>SourceProtocolInfo</name><dataType>string</dataType></stateVariable><stateVariable sendEvents="yes"><name>SinkProtocolInfo</name><dataType>string</dataType></stateVariable><stateVariable sendEvents="no"><name>A_ARG_TYPE_Direction</name><dataType>string</dataType><allowedValueList><allowedValue>Input</allowedValue><allowedValue>Output</allowedValue></allowedValueList></stateVariable><stateVariable sendEvents="yes"><name>CurrentConnectionIDs</name><dataType>string</dataType></stateVariable></serviceStateTable></scpd>

+ 7
- 0
content-directory-scpd.xml View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?><scpd xmlns="urn:schemas-upnp-org:service-1-0"><specVersion><major>1</major><minor>0</minor></specVersion><actionList><action><name>Browse</name><argumentList><argument><name>ObjectID</name><direction>in</direction><relatedStateVariable>A_ARG_TYPE_ObjectID</relatedStateVariable></argument><argument><name>BrowseFlag</name><direction>in</direction><relatedStateVariable>A_ARG_TYPE_BrowseFlag</relatedStateVariable></argument><argument><name>Filter</name><direction>in</direction><relatedStateVariable>A_ARG_TYPE_Filter</relatedStateVariable></argument><argument><name>StartingIndex</name><direction>in</direction><relatedStateVariable>A_ARG_TYPE_Index</relatedStateVariable></argument><argument><name>RequestedCount</name><direction>in</direction><relatedStateVariable>A_ARG_TYPE_Count</relatedStateVariable></argument><argument><name>SortCriteria</name><direction>in</direction><related
StateVariable>A_ARG_TYPE_SortCriteria</relatedStateVariable></argument><argument><name>Result</name><direction>out</direction><relatedStateVariable>A_ARG_TYPE_Result</relatedStateVariable></argument><argument><name>NumberReturned</name><direction>out</direction><relatedStateVariable>A_ARG_TYPE_Count</relatedStateVariable></argument><argument><name>TotalMatches</name><direction>out</direction><relatedStateVariable>A_ARG_T
YPE_Count</relatedStateVariable></argument><argument><name>UpdateID</name><direction>out</direction><relatedStateVariable>A_ARG_TYPE_UpdateID</relatedStateVariable></argument></argumentList></action><action><name>GetSortCapabilities</name><argumentList><argument><name>SortCaps</name><direction>out</direction><relatedStateVariable>SortCapabilities</relatedStateVariable></argument></argumentList></action><action><name>GetSystemUpdateID</name><argumentList><argument><name>Id</name><direction>out</direction><relatedStateVariable>SystemUpdateID</relatedStateVariable></argument></argumentList></action><action><name>Search</name><argumentList><argument><name>ContainerID</name><direction>in</direction><relatedStateVariable>A_ARG_TYPE_ObjectID</relatedStateVariable></argument><argument><name>SearchCriteria</name><direction>in</direction><relatedStateVariable>A_ARG_TYPE_SearchCriteria</relatedStateVariable></argument><argument><name>Filter</name><direction>in</direction><relatedStateVariable>A_ARG_TYPE_Filter</relatedS
tateVariable></argument><argument><name>StartingIndex</name><direction>in</direction><relatedStateVariable>A_ARG_TYPE_Index</relatedStateVariable></argument><argument><name>RequestedCount</name><direction>in</direction><relatedStateVariable>A_ARG_TYPE_Count</relatedStateVariable></argument><argument><name>SortCriteria</name><direction>in</direction><relatedStateVariable>A_ARG_TYPE_SortCriteria</relatedStateVariable></arg
ument><argument><name>Result</name><direction>out</direction><relatedStateVariable>A_ARG_TYPE_Result</relatedStateVariable></argument><argument><name>NumberReturned</name><direction>out</direction><relatedStateVariable>A_ARG_TYPE_Count</relatedStateVariable></argument><argument><name>TotalMatches</name><direction>out</direction><relatedStateVariable>A_ARG_TYPE_Count</relatedStateVariable></argument><argument><name>UpdateID</name><direction>out</direction><relatedStateVariable>A_ARG_TYPE_UpdateID</relatedStateVariable></argument></argumentList></action><action><name>GetSearchCapabilities</name><argumentList><argument><name>SearchCaps</name><direction>out</direction><relatedStateVariable>SearchCapabilities</relatedStateVariable></argument></argumentList></action></actionList><serviceStateTable><stateVariable sendEvents="no"><name>A_ARG_TYPE_BrowseFlag</name><dataType>string</dataType><allowedValueList><allowedValue>BrowseMetadata</allowedValue><allowedValue>BrowseDirectChildren</allowedValue></allowedValueList>
</stateVariable><stateVariable sendEvents="no"><name>A_ARG_TYPE_SearchCriteria</name><dataType>string</dataType></stateVariable><stateVariable sendEvents="yes"><name>SystemUpdateID</name><dataType>ui4</dataType></stateVariable><stateVariable sendEvents="no"><name>A_ARG_TYPE_Count</name><dataType>ui4</dataType></stateVariable><stateVariable sendEvents="no"><name>A_ARG_TYPE_SortCriteria</name><dataType>string</dataType></s
tateVariable><stateVariable sendEvents="no"><name>SortCapabilities</name><dataType>string</dataType></stateVariable><stateVariable sendEvents="no"><name>A_ARG_TYPE_Index</name><dataType>ui4</dataType></stateVariable><stateVariable sendEvents="no"><name>A_ARG_TYPE_ObjectID</name><dataType>string</dataType></stateVariable><stateVariable sendEvents="no"><name>A_ARG_TYPE_UpdateID</name><dataType>ui4</dataType></stateVariable><stateVariable sendEvents="no"><name>A_ARG_TYPE_Result</name><dataType>string</dataType></stateVariable><stateVariable sendEvents="no"><name>SearchCapabilities</name><dataType>string</dataType></stateVariable><stateVariable sendEvents="no"><name>A_ARG_TYPE_Filter</name><dataType>string</dataType></stateVariable></serviceStateTable></scpd>

+ 77
- 0
pymediaserv View File

@@ -0,0 +1,77 @@
#!/usr/bin/python

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

# (c) 2005, Tim Potter <tpot@samba.org>

import sys
from twisted.python import log
from twisted.internet import reactor

listenAddr = sys.argv[1]

log.startLogging(sys.stdout)

# Create SSDP server

from SSDP import SSDPServer, SSDP_PORT, SSDP_ADDR

s = SSDPServer()

port = reactor.listenMulticast(SSDP_PORT, s, SSDP_ADDR)
port.joinGroup(SSDP_ADDR, listenAddr)

uuid = 'uuid:XVKKBUKYRDLGJQDTPOT'

s.register('%s::upnp:rootdevice' % uuid,
'upnp:rootdevice',
'http://%s:8080/root-device.xml' % listenAddr)

s.register(uuid,
uuid,
'http://%s:8080/root-device.xml' % listenAddr)

s.register('%s::urn:schemas-upnp-org:device:MediaServer:1' % uuid,
'urn:schemas-upnp-org:device:MediaServer:1',
'http://%s:8080/root-device.xml' % listenAddr)

s.register('%s::urn:schemas-upnp-org:service:ConnectionManager:1' % uuid,
'urn:schemas-upnp-org:device:ConnectionManager:1',
'http://%s:8080/root-device.xml' % listenAddr)

s.register('%s::urn:schemas-upnp-org:service:ContentDirectory:1' % uuid,
'urn:schemas-upnp-org:device:ContentDirectory:1',
'http://%s:8080/root-device.xml' % listenAddr)

# Create SOAP server

from twisted.web import server, resource, static
from ContentDirectory import ContentDirectoryServer
from ConnectionManager import ConnectionManagerServer

class WebServer(resource.Resource):
def __init__(self):
resource.Resource.__init__(self)

class RootDevice(static.File):
def __init__(self):
static.File.__init__(self, 'root-device.xml', defaultType = 'text/xml')

root = WebServer()
root.putChild('ContentDirectory', ContentDirectoryServer())
root.putChild('ConnectionManager', ConnectionManagerServer())
root.putChild('root-device.xml', RootDevice())

# Area of server to serve media files from

from MediaServer import MediaServer

root.putChild('media', static.File('media'))

site = server.Site(root)
reactor.listenTCP(8080, site)

# Main loop

reactor.run()

+ 35
- 0
root-device.xml View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<device>
<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>
<INMPR03>1.0</INMPR03>
<friendlyName>Python Media Server</friendlyName>
<manufacturer>Zensonic</manufacturer>
<manufacturerURL>http://www.redsonic.com</manufacturerURL>
<modelDescription>UPnP/AV 1.0 Compliant Media Server</modelDescription>
<modelName>PC-MediaServer-DSM</modelName>
<modelNumber>103</modelNumber>
<serialNumber>0000001</serialNumber>
<UDN>uuid:XVKKBUKYRDLGJQDTPOT</UDN>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType>
<serviceId>urn:upnp-org:serviceId:CMGR_0-99</serviceId>
<SCPDURL>ConnectionManager/scpd.xml</SCPDURL>
<controlURL>ConnectionManager/control</controlURL>
<eventSubURL>ConnectionManager/event</eventSubURL>
</service>
<service>
<serviceType>urn:schemas-upnp-org:service:ContentDirectory:1</serviceType>
<serviceId>urn:upnp-org:serviceId:CDS_0-99</serviceId>
<SCPDURL>ContentDirectory/scpd.xml</SCPDURL>
<controlURL>ContentDirectory/control</controlURL>
<eventSubURL>ContentDirectory/event</eventSubURL>
</service>
</serviceList>
</device>
</root>

+ 18
- 0
upnp.py View File

@@ -0,0 +1,18 @@
# Licensed under the MIT license
# http://opensource.org/licenses/mit-license.php

# (c) 2005, Tim Potter <tpot@samba.org>

from twisted.web import soap

import SOAPpy

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)


+ 32
- 0
xml/browsemusic.xml View File

@@ -0,0 +1,32 @@
<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp">
<container id="0\Music\All%20Tracks\" parentID="0\Music\" restricted="0" searchable="0" childCount="1">
<dc:title>All Tracks</dc:title>
<upnp:class>object.container.storageFolder</upnp:class>
<dc:creator></dc:creator>
</container>
<container id="0\Music\Playlists\" parentID="0\Music\" restricted="0" searchable="0" childCount="0">
<dc:title>Playlists</dc:title>
<upnp:class>object.container</upnp:class>
<dc:creator></dc:creator>
</container>
<container id="0\Music\Genres\" parentID="0\Music\" restricted="0" searchable="0" childCount="1">
<dc:title>Genres</dc:title>
<upnp:class>object.container</upnp:class>
<dc:creator></dc:creator>
</container>
<container id="0\Music\Artists\" parentID="0\Music\" restricted="0" searchable="0" childCount="1">
<dc:title>Artists</dc:title>
<upnp:class>object.container</upnp:class>
<dc:creator></dc:creator>
</container>
<container id="0\Music\Albums\" parentID="0\Music\" restricted="0" searchable="0" childCount="1">
<dc:title>Albums</dc:title>
<upnp:class>object.container</upnp:class>
<dc:creator></dc:creator>
</container>
<container id="0\Music\Folders\" parentID="0\Music\" restricted="0" searchable="0" childCount="1">
<dc:title>Folders</dc:title>
<upnp:class>object.container</upnp:class>
<dc:creator></dc:creator>
</container>
</DIDL-Lite>

+ 11
- 0
xml/browsemusicgenres.xml View File

@@ -0,0 +1,11 @@
<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp">
<item id="0\Music\Genres\Others\chimes.wav" parentID="0\Music\Genres\Others\" restricted="0">
<dc:title>chimes.wav</dc:title>
<upnp:class>object.item.audioItem.musicTrack</upnp:class>
<dc:creator></dc:creator>
<upnp:genre>Others</upnp:genre>
<upnp:artist>Others</upnp:artist>
<upnp:album>Others</upnp:album>
<res protocolInfo="http-get:*:audio/x-wav:*" bitrate="176" size="15920">http://16.176.65.48:5643/web/C:\temp\Resource%20Kit\kixtart\chimes.wav</res>
</item>
</DIDL-Lite>

+ 14
- 0
xml/browsephotos.xml View File

@@ -0,0 +1,14 @@
<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp">
<container id="0\Photo\All%20Photos\" parentID="0\Photo\" restricted="0" searchable="0" childCount="1">
<dc:title>All Photos</dc:title>
<upnp:class>object.container.album.photoAlbum</upnp:class>
<dc:creator></dc:creator>
<upnp:icon>http://16.176.65.48:5643/web/C:\temp\foo.jpg.thumb</upnp:icon>
</container>
<container id="0\Photo\Folders\" parentID="0\Photo\" restricted="0" searchable="0" childCount="1">
<dc:title>Folders</dc:title>
<upnp:class>object.container.album.photoAlbum</upnp:class>
<dc:creator></dc:creator>
<upnp:icon>http://16.176.65.48:5643/web/C:\temp\foo.jpg.thumb</upnp:icon>
</container>
</DIDL-Lite>

+ 22
- 0
xml/browsetoplevel.xml View File

@@ -0,0 +1,22 @@
<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp">
<container id="0\Movie\" parentID="0\" restricted="0" searchable="0" childCount="0">
<dc:title>Movie</dc:title>
<upnp:class>object.container</upnp:class>
<dc:creator></dc:creator>
</container>
<container id="0\Music\" parentID="0\" restricted="0" searchable="0" childCount="0">
<dc:title>Music</dc:title>
<upnp:class>object.container</upnp:class>
<dc:creator></dc:creator>
</container>
<container id="0\Photo\" parentID="0\" restricted="0" searchable="0" childCount="0">
<dc:title>Photo</dc:title>
<upnp:class>object.container</upnp:class>
<dc:creator></dc:creator>
</container>
<container id="0\OnlineMedia\" parentID="0\" restricted="0" searchable="0" childCount="0">
<dc:title>OnlineMedia</dc:title>
<upnp:class>object.container</upnp:class>
<dc:creator></dc:creator>
</container>
</DIDL-Lite>

+ 8
- 0
xml/browsetrackmetadata.xml View File

@@ -0,0 +1,8 @@
<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp">
<item id="0\\Music\\Genres\\Others\\chimes.wav" parentID="0\\Music\\Genres\\Others\\" restricted="0">
<dc:title>chimes.wav</dc:title>
<upnp:class>object.item.audioItem.musicTrack</upnp:class>
<dc:creator></dc:creator>
</item>
</DIDL-Lite>


+ 18
- 0
xmlpretty View File

@@ -0,0 +1,18 @@
#!/usr/bin/python

# (c) 2005, Tim Potter <tpot@samba.org>

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

"""Take XML on stdin and produce pretty-printed XML on stdout."""

import sys
from xml.dom import minidom

str = ""
for s in sys.stdin.readlines():
str = str + s[:-1] # Eat trailing \n

doc = minidom.parseString(str)
print doc.toprettyxml(indent = " ")

Loading…
Cancel
Save