# Licensed under the MIT license # http://opensource.org/licenses/mit-license.php # Copyright 2005, Tim Potter # Copyright 2006-2009 John-Mark Gurney __version__ = '$Change$' # $Id$ from elementtree.ElementTree import Element, SubElement, tostring, _ElementInterface class Resource(object): """An object representing a resource.""" validattrs = { 'protocolinfo': 'protocolInfo', 'importuri': 'importUri', 'size': 'size', 'duration': 'duration', 'protection': 'protection', 'bitrate': 'bitrate', 'bitspersample': 'bitsPerSample', 'samplefrequency': 'sampleFrequence', 'nraudiochannels': 'nrAudioChannels', 'resolution': 'resolution', 'colordepth': 'colorDepth', 'tspec': 'tspec', 'alloweduse': 'allowedUse', 'validitystart': 'validityStart', 'validityend': 'validityEnd', 'remainingtime': 'remainingTime', 'usageinfo': 'usageInfo', 'rightsinfouri': 'rightsInfoURI', 'contentinfouri': 'contentInfoURI', 'recordquality': 'recordQuality', } def __init__(self, data, protocolInfo): object.__init__(self) # Use thses so setattr can be more simple object.__setattr__(self, 'data', data) object.__setattr__(self, 'attrs', {}) self.protocolInfo = protocolInfo def __getattr__(self, key): try: return self.attrs[key.lower()] except KeyError: raise AttributeError, key def __setattr__(self, key, value): key = key.lower() assert key in self.validattrs self.attrs[key] = value def toElement(self): root = Element('res') root.text = self.data for i in self.attrs: attr = self.validattrs[i] value = self.attrs[i] funname = 'format_%s' % attr if hasattr(self, funname): value = getattr(self, funname)(value) else: value = str(value) assert isinstance(value, basestring), \ 'value is not a string: %s' % `value` root.attrib[attr] = value return root @staticmethod def format_duration(s): if isinstance(s, basestring): return s # assume it is a number s = abs(s) secs = int(s) frac = s - secs minutes, secs = divmod(secs, 60) hours, minutes = divmod(minutes, 60) if frac: frac = ('%.2f' % frac)[1:] else: frac = '' return '%d:%02d:%02d%s' % (hours, minutes, secs, frac) class ResourceList(list): '''Special class to not overwrite mimetypes that already exist.''' def __init__(self, *args, **kwargs): self._mt = {} list.__init__(self, *args, **kwargs) def append(self, k): assert isinstance(k, Resource) mt = k.protocolInfo.split(':')[2] if self._mt.has_key(mt): return list.append(self, k) self._mt[mt] = k class Object(object): """The root class of the entire content directory class heirachy.""" klass = 'object' _optionattrs = { 'creator': 'dc', 'writeStatus': 'upnp', 'artist': 'upnp', 'actor': 'upnp', 'author': 'upnp', 'producer': 'upnp', 'director': 'upnp', 'publisher': 'dc', 'contributor': 'dc', 'genre': 'upnp', 'album': 'upnp', 'playlist': 'upnp', 'albumArtURI': 'upnp', 'artistDiscographyURI': 'upnp', 'lyricsURI': 'upnp', 'relation': 'dc', 'storageMedium': 'upnp', 'description': 'dc', 'longDescription': 'upnp', 'icon': 'upnp', 'region': 'upnp', 'rights': 'dc', 'date': 'dc', 'language': 'dc', 'playbackCount': 'upnp', 'lastPlaybackTime': 'upnp', 'lastPlaybackPosition': 'upnp', 'recordedStartDateTime': 'upnp', 'recordedDuration': 'upnp', 'recordedDayOfWeek': 'upnp', 'srsRecordScheduleID': 'upnp', 'srsRecordTaskID': 'upnp', 'recordable': 'upnp', 'programTitle': 'upnp', 'seriesTitle': 'upnp', 'programID': 'upnp', 'seriesID': 'upnp', 'channelID': 'upnp', 'episodeCount': 'upnp', 'episodeNumber': 'upnp', 'programCode': 'upnp', 'rating': 'upnp', 'channelGroupName': 'upnp', 'callSign': 'upnp', 'networkAffiliation': 'upnp', 'serviceProvider': 'upnp', 'price': 'upnp', 'payPerView': 'upnp', 'epgProviderName': 'upnp', 'dateTimeRange': 'upnp', 'radioCallSign': 'upnp', 'radioStationID': 'upnp', 'radioBand': 'upnp', 'channelNr': 'upnp', 'channelName': 'upnp', 'scheduledStartTime': 'upnp', 'scheduledEndTime': 'upnp', 'signalStrength': 'upnp', 'signalLocked': 'upnp', 'tuned': 'upnp', 'neverPlayable': 'upnp', 'bookmarkID': 'upnp', 'bookmarkedObjectID': 'upnp', 'deviceUDN': 'upnp', 'stateVariableCollection': 'upnp', 'DVDRegionCode': 'upnp', 'originalTrackNumber': 'upnp', 'toc': 'upnp', 'userAnnoation': 'upnp', } res = None content = property(lambda x: x._content) needupdate = None # do we update before sending? (for res) def __init__(self, cd, id, parentID, title, restricted=False, creator=None, **kwargs): 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 kwargs.has_key('content'): self._content = kwargs.pop('content') for i in kwargs: if i not in self._optionattrs: raise TypeError('invalid keyword arg: %s' % `i`) setattr(self, i, kwargs[i]) def __cmp__(self, other): if not isinstance(other, self.__class__): return cmp(self.__class__.__name__, other.__class__.__name__) return cmp(self.id, other.id) def __repr__(self): cls = self.__class__ return '<%s.%s: id: %s, parent: %s, title: %s>' % \ (cls.__module__, cls.__name__, self.id, self.parentID, self.title) def checkUpdate(self): # It's tempting to call doUpdate here, but each object has to # decide that. pass 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 for i in (x for x in self.__dict__ if x in self._optionattrs): obj = getattr(self, i) if obj is None: continue SubElement(root, '%s:%s' % (self._optionattrs[i], i)).text = unicode(getattr(self, i)) if self.res is not None: try: resiter = iter(self.res) except TypeError, x: resiter = [ self.res ] for res in resiter: root.append(res.toElement()) 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 needupdate = True def doUpdate(self, child=False): # do NOT update parent container per 2.2.9 for # ContainerUpdateID changes # must be Container.didUpdate, otherwise we could update the # parent when we really just want to update the ID Container.didUpdate(self.cd[self.parentID]) 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' class MusicTrack(AudioItem): """A discrete piece of audio that should be interpreted as music.""" klass = AudioItem.klass + '.musicTrack' 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, list): """An object that can contain other objects.""" klass = Object.klass + '.container' elementName = 'container' childCount = property(lambda x: len(x)) searchable = None updateID = 0 needcontupdate = False _optionattrs = Object._optionattrs.copy() _optionattrs.update({ 'searchClass': 'upnp', 'createClass': 'upnp', 'storageTotal': 'upnp', 'storageUsed': 'upnp', 'storageFree': 'upnp', 'storageMaxPartition': 'upnp', }) def __init__(self, cd, id, parentID, title, **kwargs): Object.__init__(self, cd, id, parentID, title, **kwargs) list.__init__(self) self.doingUpdate = False self.needcontupdate = False self.oldchildren = {} def genCurrent(self): '''This function returns a tuple/list/generator that returns tuples of (id, name). name must match what is returned by genChildren. If name is used for the title directly, no override is necessary.''' return ((x.id, x.title) for x in self) def genChildren(self): '''This function returns a list or dict of names for new children.''' raise NotImplementedError def createObject(self, i, arg=None): '''This function returns the (class, name, *args, **kwargs) that will be passed to the addItem method of the ContentDirectory. arg will be passed the value of the dict keyed by i if genChildren is a dict.''' raise NotImplementedError def sort(self, fun=lambda x, y: cmp(x.title, y.title)): return list.sort(self, fun) def doUpdate(self): if self.doingUpdate: return self.doingUpdate = True self.needcontupdate = False children = self.genChildren() if isinstance(children, dict): oldchildren = self.oldchildren self.oldchildren = children isdict = True else: children = set(children) isdict = False names = {} #print 'i:', `self`, `self.genCurrent`, `self.__class__` for id, i in tuple(self.genCurrent()): if i not in children: didupdate = True # delete self.cd.delItem(id) else: names[i] = id for i in children: if i in names: if isdict: if oldchildren[i] == children[i]: continue self.cd.delItem(names[i]) else: # XXX - some sort of comparision? continue # new object if isdict: args = (children[i], ) else: args = () try: #print 'i:', `i`, `isdict`, `args`, `self` pass except UnicodeEncodeError: print 'i decode error' klass, name, args, kwargs = self.createObject(i, *args) if klass is not None: self.cd.addItem(self.id, klass, name, *args, **kwargs) self.needcontupdate = True # sort our children self.sort() self.doingUpdate = False if self.needcontupdate: self.didUpdate() def didUpdate(self): if self.doingUpdate: self.needcontupdate = True return if self.id == '0': self.updateID = (self.updateID + 1) else: self.updateID = (self.updateID + 1) % (1l << 32) Container.didUpdate(self.cd['0']) def _addSet(self, e, items): if items is not None: if not isinstance(items, (list, tuple)): items = [ items ] for i in items: el = SubElement(root, e) el.text = i # XXX - how to specify? el.attrib['includeDerived'] = '1' def toElement(self): root = Object.toElement(self) # only include if we have children, it's possible we don't # have our children yet, and childCount is optional. if self.childCount: root.attrib['childCount'] = str(self.childCount) if self.searchable is not None: root.attrib['searchable'] = str(self.searchable) return root def __repr__(self): cls = self.__class__ return '<%s.%s: id: %s, parent: %s, title: %s, cnt: %d>' % \ (cls.__module__, cls.__name__, self.id, self.parentID, self.title, len(self)) 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' storageTotal = -1 storageUsed = -1 storageFree = -1 storageMaxParition = -1 storageMedium = 'UNKNOWN' class StorageVolume(Container): klass = Container.klass + '.storageVolume' storageTotal = -1 storageUsed = -1 storageFree = -1 storageMedium = 'UNKNOWN' class StorageFolder(Container): klass = Container.klass + '.storageFolder' storageUsed = -1 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 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.addItem(Container(None, '0\Movie\\', '0\\', 'Movie')) root.addItem(Container(None, '0\Music\\', '0\\', 'Music')) root.addItem(Container(None, '0\Photo\\', '0\\', 'Photo')) root.addItem(Container(None, '0\OnlineMedia\\', '0\\', 'OnlineMedia')) print tostring(root)