You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

967 lines
28 KiB

# -*- 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_