|
|
- # -*- coding: utf-8 -*-
- """DIDL-Lite (Digital Item Declaration Language) tools for Python."""
-
- # Useful links:
- # http://upnp.org/specs/av/UPnP-av-ContentDirectory-v2-Service.pdf
- # http://www.upnp.org/schemas/av/didl-lite-v2.xsd
- # http://xml.coverpages.org/mpeg21-didl.html
-
- import re
-
- from typing import Any, Dict, List, Optional # noqa: F401 pylint: disable=unused-import
- from xml.etree import ElementTree as ET
-
-
- NAMESPACES = {
- 'didl_lite': 'urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/',
- 'dc': 'http://purl.org/dc/elements/1.1/',
- 'upnp': 'urn:schemas-upnp-org:metadata-1-0/upnp/',
- 'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
- }
-
-
- def _ns_tag(tag: str) -> str:
- """
- Expand namespace-alias to url.
-
- E.g.,
- _ns_tag('didl_lite:item') -> '{urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/}item'
- """
- if ':' not in tag:
- return tag
-
- namespace, tag = tag.split(':')
- namespace_uri = NAMESPACES[namespace]
- return '{{{0}}}{1}'.format(namespace_uri, tag)
-
-
- def _namespace_tag(namespaced_tag: str) -> str:
- """
- Extract namespace and tag from namespaced-tag.
-
- E.g., _namespace_tag('{urn:schemas-upnp-org:metadata-1-0/upnp/}class') ->
- 'urn:schemas-upnp-org:metadata-1-0/upnp/', 'class'
- """
- if '}' not in namespaced_tag:
- return None, namespaced_tag
-
- idx = namespaced_tag.index('}')
- namespace = namespaced_tag[1:idx]
- tag = namespaced_tag[idx + 1:]
- return namespace, tag
-
-
- def _to_camel_case(name):
- sub1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
- return re.sub('([a-z0-9])([A-Z])', r'\1_\2', sub1).lower()
-
-
- def _didl_property_def_key(didl_property_def):
- """Get Python property key for didl_property_def."""
- if didl_property_def[1].startswith('@'):
- return _to_camel_case(didl_property_def[1].replace('@', ''))
-
- return _to_camel_case(didl_property_def[1].replace('@', '_'))
-
-
- # region: DidlObjects
- class DidlObject:
- """DIDL Ojbect."""
-
- tag = None # type: Optional[str]
- upnp_class = 'object'
- didl_properties_defs = [
- ('didl_lite', '@id', 'R'),
- ('didl_lite', '@parentID', 'R'),
- ('didl_lite', '@restricted', 'R'),
- ('dc', 'title', 'R'),
- ('upnp', 'class', 'R'),
- ('dc', 'creator', 'O'),
- ('didl_lite', 'res', 'O'),
- ('upnp', 'writeStatus', 'O'),
- ]
-
- def __init__(self, id="", parent_id="", descriptors=None, **properties):
- """Initializer."""
- # pylint: disable=invalid-name,redefined-builtin
- properties['id'] = id
- properties['parent_id'] = parent_id
- properties['class'] = self.upnp_class
- self._ensure_required_properties(**properties)
- self._set_properties(**properties)
-
- self.resources = properties.get('resources') or []
- self.descriptors = descriptors if descriptors else []
-
- def _ensure_required_properties(self, **properties):
- """Check if all required properties are given."""
- for property_def in self.didl_properties_defs:
- key = _didl_property_def_key(property_def)
- if property_def[2] == 'R' and key not in properties:
- raise Exception(key + ' is mandatory')
-
- def _set_properties(self, **properties):
- """Set attributes from properties."""
- # ensure we have default/known slots
- for property_def in self.didl_properties_defs:
- key = _didl_property_def_key(property_def)
- setattr(self, key, None)
-
- for key, value in properties.items():
- setattr(self, key, value)
-
- @classmethod
- def from_xml(cls, xml_el: ET.Element):
- """
- Initialize from an XML node.
-
- I.e., parse XML and return instance.
- """
- # pylint: disable=too-many-locals
- properties = {} # type: Dict[str, Any]
-
- # attributes
- for attr_key, attr_value in xml_el.attrib.items():
- key = _to_camel_case(attr_key)
- properties[key] = attr_value
-
- # child-nodes
- for xml_child_node in xml_el:
- if xml_child_node.tag == _ns_tag('didl_lite:res'):
- continue
-
- _, tag = _namespace_tag(xml_child_node.tag)
- key = _to_camel_case(tag)
- value = xml_child_node.text
- properties[key] = value
-
- # attributes of child nodes
- parent_key = key
- for attr_key, attr_value in xml_child_node.attrib.items():
- key = parent_key + '_' + _to_camel_case(attr_key)
- properties[key] = attr_value
-
- # resources
- resources = []
- for res_el in xml_el.findall('./didl_lite:res', NAMESPACES):
- resource = Resource.from_xml(res_el)
- resources.append(resource)
- properties['resources'] = resources
-
- # descriptors
- descriptors = []
- for desc_el in xml_el.findall('./didl_lite:desc', NAMESPACES):
- descriptor = Descriptor.from_xml(desc_el)
- descriptors.append(descriptor)
-
- return cls(descriptors=descriptors, **properties)
-
- def to_xml(self) -> ET.Element:
- """Convert self to XML Element."""
- item_el = ET.Element(_ns_tag(self.tag))
- elements = {'': item_el}
-
- # properties
- for property_def in self.didl_properties_defs:
- if '@' in property_def[1]:
- continue
- key = _didl_property_def_key(property_def)
-
- if getattr(self, key) is None or \
- key == 'res': # no resources, handled later on
- continue
-
- tag = property_def[0] + ':' + property_def[1]
- property_el = ET.Element(_ns_tag(tag), {})
- property_el.text = getattr(self, key)
- item_el.append(property_el)
- elements[property_def[1]] = property_el
-
- # attributes and property@attributes
- for property_def in self.didl_properties_defs:
- if '@' not in property_def[1]:
- continue
-
- key = _didl_property_def_key(property_def)
- value = getattr(self, key)
- if value is None:
- continue
-
- el_name, attr_name = property_def[1].split('@')
- property_el = elements[el_name]
- property_el.attrib[attr_name] = value
-
- # resource
- for resource in self.resources:
- res_el = resource.to_xml()
- item_el.append(res_el)
-
- # descriptor
- for descriptor in self.descriptors:
- desc_el = descriptor.to_xml()
- item_el.append(desc_el)
-
- return item_el
-
-
- # region: items
- class Item(DidlObject):
- """DIDL Item."""
-
- # pylint: disable=too-few-public-methods
-
- tag = 'item'
- upnp_class = 'object.item'
- didl_properties_defs = DidlObject.didl_properties_defs + [
- ('didl_lite', '@refID', 'O'), # actually, R, but ignore for now
- ('upnp', 'bookmarkID', 'O'),
- ]
-
-
- class ImageItem(Item):
- """DIDL Image Item."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.item.imageItem'
- didl_properties_defs = Item.didl_properties_defs + [
- ('upnp', 'longDescription', 'O'),
- ('upnp', 'storageMedium', 'O'),
- ('upnp', 'rating', 'O'),
- ('dc', 'description', 'O'),
- ('dc', 'publisher', 'O'),
- ('dc', 'date', 'O'),
- ('dc', 'rights', 'O'),
- ]
-
-
- class Photo(ImageItem):
- """DIDL Photo."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.item.imageItem.photo'
- didl_properties_defs = ImageItem.didl_properties_defs + [
- ('upnp', 'album', 'O'),
- ]
-
-
- class AudioItem(Item):
- """DIDL Audio Item."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.item.audioItem'
- didl_properties_defs = Item.didl_properties_defs + [
- ('upnp', 'genre', 'O'),
- ('dc', 'description', 'O'),
- ('upnp', 'longDescription', 'O'),
- ('dc', 'publisher', 'O'),
- ('dc', 'language', 'O'),
- ('dc', 'relation', 'O'),
- ('dc', 'rights', 'O'),
- ]
-
-
- class MusicTrack(AudioItem):
- """DIDL Music Track."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.item.audioItem.musicTrack'
- didl_properties_defs = AudioItem.didl_properties_defs + [
- ('upnp', 'artist', 'O'),
- ('upnp', 'album', 'O'),
- ('upnp', 'originalTrackNumber', 'O'),
- ('upnp', 'playlist', 'O'),
- ('upnp', 'storageMedium', 'O'),
- ('dc', 'contributor', 'O'),
- ('dc', 'date', 'O'),
- ]
-
-
- class AudioBroadcast(AudioItem):
- """DIDL Audio Broadcast."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.item.audioItem.audioBroadcast'
- didl_properties_defs = AudioItem.didl_properties_defs + [
- ('upnp', 'region', 'O'),
- ('upnp', 'radioCallSign', 'O'),
- ('upnp', 'radioStationID', 'O'),
- ('upnp', 'radioBand', 'O'),
- ('upnp', 'channelNr', 'O'),
- ('upnp', 'signalStrength', 'O'),
- ('upnp', 'signalLocked', 'O'),
- ('upnp', 'tuned', 'O'),
- ('upnp', 'recordable', 'O'),
- ]
-
-
- class AudioBook(AudioItem):
- """DIDL Audio Book."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.item.audioItem.audioBook'
- didl_properties_defs = AudioItem.didl_properties_defs + [
- ('upnp', 'storageMedium', 'O'),
- ('upnp', 'producer', 'O'),
- ('dc', 'contributor', 'O'),
- ('dc', 'date', 'O'),
- ]
-
-
- class VideoItem(Item):
- """DIDL Video Item."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.item.videoItem'
- didl_properties_defs = Item.didl_properties_defs + [
- ('upnp', 'genre', 'O'),
- ('upnp', 'genre@id', 'O'),
- ('upnp', 'genre@type', 'O'),
- ('upnp', 'longDescription', 'O'),
- ('upnp', 'producer', 'O'),
- ('upnp', 'rating', 'O'),
- ('upnp', 'actor', 'O'),
- ('upnp', 'director', 'O'),
- ('dc', 'description', 'O'),
- ('dc', 'publisher', 'O'),
- ('dc', 'language', 'O'),
- ('dc', 'relation', 'O'),
- ('upnp', 'playbackCount', 'O'),
- ('upnp', 'lastPlaybackTime', 'O'),
- ('upnp', 'lastPlaybackPosition', 'O'),
- ('upnp', 'recordedDayOfWeek', 'O'),
- ('upnp', 'srsRecordScheduleID', 'O'),
- ]
-
-
- class Movie(VideoItem):
- """DIDL Movie."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.item.videoItem.movie'
- didl_properties_defs = VideoItem.didl_properties_defs + [
- ('upnp', 'storageMedium', 'O'),
- ('upnp', 'DVDRegionCode', 'O'),
- ('upnp', 'channelName', 'O'),
- ('upnp', 'scheduledStartTime', 'O'),
- ('upnp', 'scheduledEndTime', 'O'),
- ('upnp', 'programTitle', 'O'),
- ('upnp', 'seriesTitle', 'O'),
- ('upnp', 'episodeCount', 'O'),
- ('upnp', 'episodeNr', 'O'),
- ]
-
-
- class VideoBroadcast(VideoItem):
- """DIDL Video Broadcast."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.item.videoItem.videoBroadcast'
- didl_properties_defs = VideoItem.didl_properties_defs + [
- ('upnp', 'icon', 'O'),
- ('upnp', 'region', 'O'),
- ('upnp', 'channelNr', 'O'),
- ('upnp', 'signalStrength', 'O'),
- ('upnp', 'signalLocked', 'O'),
- ('upnp', 'tuned', 'O'),
- ('upnp', 'recordable', 'O'),
- ('upnp', 'callSign', 'O'),
- ('upnp', 'price', 'O'),
- ('upnp', 'payPerView', 'O'),
- ]
-
-
- class MusicVideoClip(VideoItem):
- """DIDL Music Video Clip."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.item.videoItem.musicVideoClip'
- didl_properties_defs = VideoItem.didl_properties_defs + [
- ('upnp', 'artist', 'O'),
- ('upnp', 'storageMedium', 'O'),
- ('upnp', 'album', 'O'),
- ('upnp', 'scheduledStartTime', 'O'),
- ('upnp', 'scheduledStopTime', 'O'),
- # ('upnp', 'director', 'O'), # duplicate in standard
- ('dc', 'contributor', 'O'),
- ('dc', 'date', 'O'),
- ]
-
-
- class PlaylistItem(Item):
- """DIDL Playlist Item."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.item.playlistItem'
- didl_properties_defs = Item.didl_properties_defs + [
- ('upnp', 'artist', 'O'),
- ('upnp', 'genre', 'O'),
- ('upnp', 'longDescription', 'O'),
- ('upnp', 'storageMedium', 'O'),
- ('dc', 'description', 'O'),
- ('dc', 'date', 'O'),
- ('dc', 'language', 'O'),
- ]
-
-
- class TextItem(Item):
- """DIDL Text Item."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.item.textItem'
- didl_properties_defs = Item.didl_properties_defs + [
- ('upnp', 'author', 'O'),
- ('upnp', 'res@protection', 'O'),
- ('upnp', 'longDescription', 'O'),
- ('upnp', 'storageMedium', 'O'),
- ('upnp', 'rating', 'O'),
- ('dc', 'description', 'O'),
- ('dc', 'publisher', 'O'),
- ('dc', 'contributor', 'O'),
- ('dc', 'date', 'O'),
- ('dc', 'relation', 'O'),
- ('dc', 'language', 'O'),
- ('dc', 'rights', 'O'),
- ]
-
-
- class BookmarkItem(Item):
- """DIDL Bookmark Item."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.item.bookmarkItem'
- didl_properties_defs = Item.didl_properties_defs + [
- ('upnp', 'bookmarkedObjectID', 'R'),
- ('upnp', 'neverPlayable', 'O'),
- ('upnp', 'deviceUDN', 'R'),
- ('upnp', 'serviceType', 'R'),
- ('upnp', 'serviceId', 'R'),
- ('dc', 'date', 'O'),
- ('dc', 'stateVariableCollection', 'R'),
- ]
-
-
- class EpgItem(Item):
- """DIDL EPG Item."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.item.epgItem'
- didl_properties_defs = Item.didl_properties_defs + [
- ('upnp', 'channelGroupName', 'O'),
- ('upnp', 'channelGroupName@id', 'O'),
- ('upnp', 'epgProviderName', 'O'),
- ('upnp', 'serviceProvider', 'O'),
- ('upnp', 'channelName', 'O'),
- ('upnp', 'channelNr', 'O'),
- ('upnp', 'programTitle', 'O'),
- ('upnp', 'seriesTitle', 'O'),
- ('upnp', 'programID', 'O'),
- ('upnp', 'programID@type', 'O'),
- ('upnp', 'seriesID', 'O'),
- ('upnp', 'seriesID@type', 'O'),
- ('upnp', 'channelID', 'O'),
- ('upnp', 'channelID@type', 'O'),
- ('upnp', 'episodeCount', 'O'),
- ('upnp', 'episodeNumber', 'O'),
- ('upnp', 'programCode', 'O'),
- ('upnp', 'programCode_type', 'O'),
- ('upnp', 'rating', 'O'),
- ('upnp', 'rating@type', 'O'),
- ('upnp', 'episodeType', 'O'),
- ('upnp', 'genre', 'O'),
- ('upnp', 'genre@id', 'O'),
- ('upnp', 'genre@extended', 'O'),
- ('upnp', 'artist', 'O'),
- ('upnp', 'artist@role', 'O'),
- ('upnp', 'actor', 'O'),
- ('upnp', 'actor@role', 'O'),
- ('upnp', 'author', 'O'),
- ('upnp', 'author@role', 'O'),
- ('upnp', 'producer', 'O'),
- ('upnp', 'director', 'O'),
- ('dc', 'publisher', 'O'),
- ('dc', 'contributor', 'O'),
- ('upnp', 'networkAffiliation', 'O'),
- # ('upnp', 'serviceProvider', 'O'), # duplicate in standard
- ('upnp', 'price', 'O'),
- ('upnp', 'price@currency', 'O'),
- ('upnp', 'payPerView', 'O'),
- # ('upnp', 'epgProviderName', 'O'), # duplicate in standard
- ('dc', 'description', 'O'),
- ('upnp', 'longDescription', 'O'),
- ('upnp', 'icon', 'O'),
- ('upnp', 'region', 'O'),
- ('dc', 'language', 'O'),
- ('dc', 'relation', 'O'),
- ('upnp', 'scheduledStartTime', 'O'),
- ('upnp', 'scheduledEndTime', 'O'),
- ('upnp', 'recordable', 'O'),
- ]
-
-
- class AudioProgram(EpgItem):
- """DIDL Audio Program."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.item.epgItem.audioProgram'
- didl_properties_defs = Item.didl_properties_defs + [
- ('upnp', 'radioCallSign', 'O'),
- ('upnp', 'radioStationID', 'O'),
- ('upnp', 'radioBand', 'O'),
- ]
-
-
- class VideoProgram(EpgItem):
- """DIDL Video Program."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.item.epgItem.videoProgram'
- didl_properties_defs = Item.didl_properties_defs + [
- ('upnp', 'price', 'O'),
- ('upnp', 'price@currency', 'O'),
- ('upnp', 'payPerView', 'O'),
- ]
- # endregion
-
-
- # region: containers
- class Container(DidlObject, list):
- """DIDL Container."""
-
- # pylint: disable=too-few-public-methods
-
- tag = 'container'
- upnp_class = 'object.container'
- didl_properties_defs = DidlObject.didl_properties_defs + [
- ('didl_lite', '@childCount', 'O'),
- ('upnp', 'createClass', 'O'),
- ('upnp', 'searchClass', 'O'),
- ('didl_lite', '@searchable', 'O'),
- ('didl_lite', '@neverPlayable', 'O'),
- ]
-
- @classmethod
- def from_xml(cls, xml_el: ET.Element):
- """
- Initialize from an XML node.
-
- I.e., parse XML and return instance.
- """
- instance = super().from_xml(xml_el)
-
- # add all children
- didl_objects = from_xml_el(xml_el)
- instance.extend(didl_objects) # pylint: disable=no-member
-
- return instance
-
- def to_xml(self) -> ET.Element:
- """Convert self to XML Element."""
- container_el = super().to_xml()
-
- for didl_object in self:
- didl_object_el = didl_object.to_xml()
- container_el.append(didl_object_el)
-
- return container_el
-
-
- class Person(Container):
- """DIDL Person."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.container.person'
- didl_properties_defs = Container.didl_properties_defs + [
- ('dc', 'language', 'O'),
- ]
-
-
- class MusicArtist(Person):
- """DIDL Music Artist."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.container.person.musicArtist'
- didl_properties_defs = Container.didl_properties_defs + [
- ('upnp', 'genre', 'O'),
- ('upnp', 'artistDiscographyURI', 'O'),
- ]
-
-
- class PlaylistContainer(Container):
- """DIDL Playlist Container."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.container.playlistContainer'
- didl_properties_defs = Container.didl_properties_defs + [
- ('upnp', 'artist', 'O'),
- ('upnp', 'genre', 'O'),
- ('upnp', 'longDescription', 'O'),
- ('upnp', 'producer', 'O'),
- ('upnp', 'storageMedium', 'O'),
- ('dc', 'description', 'O'),
- ('dc', 'contributor', 'O'),
- ('dc', 'date', 'O'),
- ('dc', 'language', 'O'),
- ('dc', 'rights', 'O'),
- ]
-
-
- class Album(Container):
- """DIDL Album."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.container.album'
- didl_properties_defs = Container.didl_properties_defs + [
- ('upnp', 'storageMedium', 'O'),
- ('dc', 'longDescription', 'O'),
- ('dc', 'description', 'O'),
- ('dc', 'publisher', 'O'),
- ('dc', 'contributor', 'O'),
- ('dc', 'date', 'O'),
- ('dc', 'relation', 'O'),
- ('dc', 'rights', 'O'),
- ]
-
-
- class MusicAlbum(Album):
- """DIDL Music Album."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.container.album.musicAlbum'
- didl_properties_defs = Container.didl_properties_defs + [
- ('upnp', 'artist', 'O'),
- ('upnp', 'genre', 'O'),
- ('upnp', 'producer', 'O'),
- ('upnp', 'albumArtURI', 'O'),
- ('upnp', 'toc', 'O'),
- ]
-
-
- class PhotoAlbum(Album):
- """DIDL Photo Album."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.container.album.photoAlbum'
- didl_properties_defs = Container.didl_properties_defs + [
- ]
-
-
- class Genre(Container):
- """DIDL Genre."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.container.genre'
- didl_properties_defs = Container.didl_properties_defs + [
- ('upnp', 'genre', 'O'),
- ('upnp', 'longDescription', 'O'),
- ('dc', 'description', 'O'),
- ]
-
-
- class MusicGenre(Genre):
- """DIDL Music Genre."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.container.genre.musicGenre'
- didl_properties_defs = Container.didl_properties_defs + [
- ]
-
-
- class MovieGenre(Genre):
- """DIDL Movie Genre."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.container.genre.movieGenre'
- didl_properties_defs = Container.didl_properties_defs + [
- ]
-
-
- class ChannelGroup(Container):
- """DIDL Channel Group."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.container.channelGroup'
- didl_properties_defs = Container.didl_properties_defs + [
- ('upnp', 'channelGroupName', 'O'),
- ('upnp', 'channelGroupName@id', 'O'),
- ('upnp', 'epgProviderName', 'O'),
- ('upnp', 'serviceProvider', 'O'),
- ('upnp', 'icon', 'O'),
- ('upnp', 'region', 'O'),
- ]
-
-
- class AudioChannelGroup(ChannelGroup):
- """DIDL Audio Channel Group."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.container.channelGroup.audioChannelGroup'
- didl_properties_defs = Container.didl_properties_defs + [
- ]
-
-
- class VideoChannelGroup(ChannelGroup):
- """DIDL Video Channel Group."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.container.channelGroup.videoChannelGroup'
- didl_properties_defs = Container.didl_properties_defs + [
- ]
-
-
- class EpgContainer(Container):
- """DIDL EPG Container."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.container.epgContainer'
- didl_properties_defs = Container.didl_properties_defs + [
- ('upnp', 'channelGroupName', 'O'),
- ('upnp', 'channelGroupName@id', 'O'),
- ('upnp', 'epgProviderName', 'O'),
- ('upnp', 'serviceProvider', 'O'),
- ('upnp', 'channelName', 'O'),
- ('upnp', 'channelNr', 'O'),
- ('upnp', 'channelID', 'O'),
- ('upnp', 'channelID@type', 'O'),
- ('upnp', 'radioCallSign', 'O'),
- ('upnp', 'radioStationID', 'O'),
- ('upnp', 'radioBand', 'O'),
- ('upnp', 'callSign', 'O'),
- ('upnp', 'networkAffiliation', 'O'),
- # ('upnp', 'serviceProvider', 'O'), # duplicate in standard
- ('upnp', 'price', 'O'),
- ('upnp', 'price@currency', 'O'),
- ('upnp', 'payPerView', 'O'),
- # ('upnp', 'epgProviderName', 'O'), # duplicate in standard
- ('upnp', 'icon', 'O'),
- ('upnp', 'region', 'O'),
- ('dc', 'language', 'O'),
- ('dc', 'relation', 'O'),
- ('upnp', 'dateTimeRange', 'O'),
- ]
-
-
- class StorageSystem(Container):
- """DIDL Storage System."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.container.storageSystem'
- didl_properties_defs = Container.didl_properties_defs + [
- ('upnp', 'storageTotal', 'R'),
- ('upnp', 'storageUsed', 'R'),
- ('upnp', 'storageFree', 'R'),
- ('upnp', 'storageMaxPartition', 'R'),
- ('upnp', 'storageMedium', 'R'),
- ]
-
-
- class StorageVolume(Container):
- """DIDL Storage Volume."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.container.storageVolume'
- didl_properties_defs = Container.didl_properties_defs + [
- ('upnp', 'storageTotal', 'R'),
- ('upnp', 'storageUsed', 'R'),
- ('upnp', 'storageFree', 'R'),
- ('upnp', 'storageMedium', 'R'),
- ]
-
-
- class StorageFolder(Container):
- """DIDL Storage Folder."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.container.storageFolder'
- didl_properties_defs = Container.didl_properties_defs + [
- ('upnp', 'storageUsed', 'R'),
- ]
-
-
- class BookmarkFolder(Container):
- """DIDL Bookmark Folder."""
-
- # pylint: disable=too-few-public-methods
-
- upnp_class = 'object.container.bookmarkFolder'
- didl_properties_defs = Container.didl_properties_defs + [
- ('upnp', 'genre', 'O'),
- ('upnp', 'longDescription', 'O'),
- ('dc', 'description', 'O'),
- ]
- # endregion
-
-
- class Resource:
- """DIDL Resource."""
-
- # pylint: disable=too-few-public-methods,too-many-instance-attributes
-
- def __init__(self, uri, protocol_info, import_uri=None, size=None, duration=None,
- bitrate=None, sample_frequency=None, bits_per_sample=None,
- nr_audio_channels=None, resolution=None, color_depth=None, protection=None):
- """Initializer."""
- # pylint: disable=too-many-arguments
- self.uri = uri
- self.protocol_info = protocol_info
- self.import_uri = import_uri
- self.size = size
- self.duration = duration
- self.bitrate = bitrate
- self.sample_frequency = sample_frequency
- self.bits_per_sample = bits_per_sample
- self.nr_audio_channels = nr_audio_channels
- self.resolution = resolution
- self.color_depth = color_depth
- self.protection = protection
-
- @classmethod
- def from_xml(cls, xml_node: ET.Element):
- """Initialize from an XML node."""
- uri = xml_node.text
- protocol_info = xml_node.attrib["protocolInfo"]
- import_uri = xml_node.attrib.get('importUri')
- size = xml_node.attrib.get('size')
- duration = xml_node.attrib.get('duration')
- bitrate = xml_node.attrib.get('bitrate')
- sample_frequency = xml_node.attrib.get('sampleFrequency')
- bits_per_sample = xml_node.attrib.get('bitsPerSample')
- nr_audio_channels = xml_node.attrib.get('nrAudioChannels')
- resolution = xml_node.attrib.get('resolution')
- color_depth = xml_node.attrib.get('colorDepth')
- protection = xml_node.attrib.get('protection')
- return cls(uri, protocol_info,
- import_uri=import_uri, size=size, duration=duration,
- bitrate=bitrate, sample_frequency=sample_frequency,
- bits_per_sample=bits_per_sample,
- nr_audio_channels=nr_audio_channels,
- resolution=resolution, color_depth=color_depth,
- protection=protection)
-
- def to_xml(self) -> ET.Element:
- """Convert self to XML."""
- attribs = {
- 'protocolInfo': self.protocol_info,
- }
- res_el = ET.Element(_ns_tag('res'), attribs)
- res_el.text = self.uri
- return res_el
-
-
- class Descriptor:
- """DIDL Descriptor."""
-
- def __init__(self, id, name_space, type=None, text=None):
- """Initializer."""
- # pylint: disable=invalid-name,redefined-builtin
- self.id = id
- self.name_space = name_space
- self.type = type
- self.text = text
-
- @classmethod
- def from_xml(cls, xml_node: ET.Element):
- """Initialize from an XML node."""
- id_ = xml_node.attrib['id']
- name_space = xml_node.attrib['nameSpace']
- type_ = xml_node.attrib.get('type')
- return cls(id_, name_space, type_, xml_node.text)
-
- def to_xml(self) -> ET.Element:
- """Convert self to XML."""
- attribs = {
- 'id': self.id,
- 'nameSpace': self.name_space,
- 'type': self.type,
- }
- desc_el = ET.Element(_ns_tag('desc'), attribs)
- desc_el.text = self.text
- return desc_el
- # endregion
-
-
- def to_xml_string(*objects) -> str:
- """Convert items to DIDL-Lite XML string."""
- root_el = ET.Element(_ns_tag('DIDL-Lite'), {})
- root_el.attrib['xmlns'] = NAMESPACES['didl_lite']
-
- for didl_object in objects:
- didl_object_el = didl_object.to_xml()
- root_el.append(didl_object_el)
-
- return ET.tostring(root_el)
-
-
- def from_xml_string(xml_string) -> List[DidlObject]:
- """Convert XML string to DIDL Objects."""
- xml_el = ET.fromstring(xml_string)
- return from_xml_el(xml_el)
-
-
- def from_xml_el(xml_el: ET.Element) -> List[DidlObject]:
- """Convert XML Element to DIDL Objects."""
- didl_objects = []
-
- # items and containers, in order
- for child_el in xml_el:
- if child_el.tag != _ns_tag('didl_lite:item') and \
- child_el.tag != _ns_tag('didl_lite:container'):
- continue
-
- # construct item
- upnp_class = child_el.find('./upnp:class', NAMESPACES)
- if upnp_class is None or not upnp_class.text:
- continue
- didl_object_type = type_by_upnp_class(upnp_class.text)
- didl_object = didl_object_type.from_xml(child_el)
- didl_objects.append(didl_object)
-
- # descriptors
- for desc_el in xml_el.findall('./didl_lite:desc', NAMESPACES):
- desc = Descriptor.from_xml(desc_el)
- didl_objects.append(desc)
-
- return didl_objects
-
-
- # upnp_class to python type mapping
- def type_by_upnp_class(upnp_class: str) -> type:
- """Get DidlObject-type by upnp_class."""
- queue = DidlObject.__subclasses__()
- while queue:
- type_ = queue.pop()
- queue.extend(type_.__subclasses__())
-
- if type_.upnp_class == upnp_class:
- return type_
|