diff --git a/README.md b/README.md index 46fa626..14fce7f 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ A Python implementation of ETSI TS 102 818 v3.1 Hybrid Radio SPI, including an X This can be used by broadcasters for producing or parsing Hybrid Radio SPI documents over IP and/or DAB, or for general service and programme information interfacing. +Please make sure you select the Python2 branch if you want to use on Python 2. + # TODO * GI Files diff --git a/setup.py b/setup.py index 85c034e..b7eaa85 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ #!/usr/bin/env python -from distutils.core import setup +import sys +from setuptools import setup setup(name='hybridspi', version='0.2.0', @@ -13,6 +14,6 @@ keywords=['dab', 'spi', 'hybrid', 'radio'], packages=['spi', 'spi.xml', 'spi.binary'], package_dir = {'' : 'src'}, - install_requires = ['python-dateutil', 'isodate', 'bitarray', 'asciitree'], - scripts=['dump_binary'] + install_requires = ['python-dateutil', 'isodate', 'bitarray', 'asciitree','jdcal'], + scripts=['bin/dump_binary'] ) diff --git a/src/spi/__init__.py b/src/spi/__init__.py index 2a1db52..e7bb6da 100644 --- a/src/spi/__init__.py +++ b/src/spi/__init__.py @@ -26,6 +26,7 @@ MAX_SHORTCRID = 16777215 DEFAULT_LANGUAGE = "en" +DEFAULT_ENCODING = "utf-8" class Text: """Abstract class for textual information""" @@ -200,7 +201,7 @@ def __init__(self, ecc, eid, sid, scids=0, content=DAB_PLUS, xpad=None, cost=Non dab:.... in hex :: - Where ``gcc`` is a combination of the first nibble of the EId and the ECC. + Where ``gcc`` is a combination of the first nibble of the SId and the ECC. For example: :: @@ -211,8 +212,8 @@ def __init__(self, ecc, eid, sid, scids=0, content=DAB_PLUS, xpad=None, cost=Non :type ecc: int :param eid: Ensemble ID :type eid: int - :param ecc: Service ID - :type ecc: int + :param sid: Service ID + :type sid: int :param scids: Service Component ID within the Service :type scids: int :param xpad: X-PAD application @@ -238,7 +239,7 @@ def fromstring(cls, string): Parse a DAB Bearer URI from its string representation """ - pattern = re.compile("^dab:(.{3})\.(.{4})\.(.{4})\.(.{1})[\.(.+?)]{0,1}$") + pattern = re.compile("^dab:([0-9a-f]{3})\.([0-9a-f]{4})\.([0-9a-f]{4,8})\.([0-9a-f]{1})[\.(.+?)]{0,1}$") matcher = pattern.search(string) if not matcher: raise ValueError('bearer %s does not match the pattern: %s' % (string, pattern.pattern)) ecc = int(matcher.group(1)[1:], 16) @@ -251,13 +252,22 @@ def fromstring(cls, string): return DabBearer(ecc, eid, sid, scids, xpad) def __str__(self): - uri = 'dab:{gcc:03x}.{eid:04x}.{sid:04x}.{scids:01x}'.format(gcc=(self.eid >> 4 & 0xf00) + self.ecc, eid=self.eid, sid=self.sid, scids=self.scids) + if self.sid>65535: # this is a long SId which contains both ECC (first two nibbles) and CC (third nibble) + gcc = (self.sid >> 12 & 0xf00) + (self.sid >> 24) + uri = 'dab:{gcc:03x}.{eid:04x}.{sid:08x}.{scids:01x}'.format(gcc=gcc, eid=self.eid, sid=self.sid, scids=self.scids) + else: # this is a short SId which contains only the CC (first nibble) + gcc = (self.sid >> 4 & 0xf00) + self.ecc + uri = 'dab:{gcc:03x}.{eid:04x}.{sid:04x}.{scids:01x}'.format(gcc=gcc, eid=self.eid, sid=self.sid, scids=self.scids) + if self.xpad is not None: uri += '.{xpad:04x}'.format(xpad=self.xpad) return uri def __repr__(self): return '' % str(self) + + def __eq__(self, other): + return str(self) == str(other) class HdBearer(DigitalBearer): @@ -292,9 +302,14 @@ def __str__(self): def __repr__(self): return '' % str(self) + def __eq__(self, other): + return str(self) == str(other) + class FmBearer(Bearer): - def __init__(self, ecc, pi, frequency, cost=None, offset=None): + BEARER_PATTERN=re.compile("fm:(\w{2}|[0-9a-f]{3})\.([0-9a-f]{4})\.(\*|.{5})", re.IGNORECASE) + + def __init__(self, ecc, pi, frequency=None, cost=None, offset=None): """ FM Service Bearer @@ -307,11 +322,16 @@ def __init__(self, ecc, pi, frequency, cost=None, offset=None): fm:ce1.c479.09580 :: + Wildcards can also be used for frequency, indicating that *any* frequency will match the bearer + + NOTE: the legacy format using the country code instead of GCC is supported for the time being + but may be removed in a future version. + :param ecc: Extended Country Code :type ecc: int :param pi: Programme Info code :type pi: int - :param frequency: Service frequency (Hz) + :param frequency: Service frequency (Hz), None for wildcard :type frequency: int """ @@ -322,7 +342,7 @@ def __init__(self, ecc, pi, frequency, cost=None, offset=None): if not isinstance(ecc, int): raise ValueError("ECC must be an integer") if not isinstance(pi, int): raise ValueError("PI must be an integer") - if not isinstance(frequency, int): raise ValueError("Frequency must be an integer") + if frequency is not None and not isinstance(frequency, int): raise ValueError("Frequency must be an integer") @classmethod def fromstring(cls, string): @@ -330,16 +350,23 @@ def fromstring(cls, string): Parse a FM Bearer URI from its string representation """ - pattern = re.compile("fm:(.{3})\.(.{4})\.(.{5})") - matcher = pattern.search(string) - if not matcher: raise ValueError('bearer %s does not match the pattern: %s' % (string, pattern)) - ecc = int(matcher.group(1)[1:], 16) + matcher = FmBearer.BEARER_PATTERN.search(string) + if not matcher: raise ValueError('bearer %s does not match the pattern: %s' % (string, FmBearer.BEARER_PATTERN)) + if len(matcher.group(1)) == 3: + ecc = int(matcher.group(1)[1:], 16) + else: # we assume the country code for legacy support and map to the ECC + ecc = int(map_countrycode_to_ecc(matcher.group(1)), 16) pi = int(matcher.group(2), 16) - frequency = int(matcher.group(3)) * 10 - return FmBearer(ecc, pi, frequency) + if matcher.group(3) == '*': + return FmBearer(ecc, pi) + else: + return FmBearer(ecc, pi, int(matcher.group(3)) * 10) def __str__(self): - uri = 'fm:{gcc:03x}.{pi:04x}.{frequency:05d}'.format(gcc=(self.pi >> 4 & 0xf00) + self.ecc, pi=self.pi, frequency=int(self.frequency/10)) + if self.frequency is None: + uri = 'fm:{gcc:03x}.{pi:04x}.*'.format(gcc=(self.pi >> 4 & 0xf00) + self.ecc, pi=self.pi) + else: + uri = 'fm:{gcc:03x}.{pi:04x}.{frequency:05d}'.format(gcc=(self.pi >> 4 & 0xf00) + self.ecc, pi=self.pi, frequency=int(self.frequency/10)) return uri def __repr__(self): @@ -348,9 +375,38 @@ def __repr__(self): def __eq__(self, other): return str(self) == str(other) +""" +map the legacy country code to an ECC +""" +COUNTRY_CODE_MAP = { + "al": "e0", + "dz": "e0", + "ad": "e0", + "am": "e4", + "at": "e0", + "az": "e3", + "by": "e3", + "ba": "e4", + "bg": "e1", + "hr": "e3", + "cy": "e1", + "dk": "e1", + "ee": "e4", + "fi": "e1", + "fr": "e1", + "ge": "e4", + "de": "e1", + "gr": "e1", + "hu": "e0", + "ie": "e3", + "il": "e0", # tbc +} +def map_countrycode_to_ecc(countrycode): + return COUNTRY_CODE_MAP.get(countrycode.lower()) + class IpBearer(DigitalBearer): - def __init__(self, uri, content, cost=None, offset=None, bitrate=None): + def __init__(self, uri, content=None, cost=None, offset=None, bitrate=None): """ IP Service Bearer @@ -367,7 +423,9 @@ def __str__(self): def __repr__(self): return '' % str(self) - + + def __eq__(self, other): + return str(self) == str(other) class ProgrammeInfo: """The root of a PI document""" @@ -631,7 +689,7 @@ class Scope: Contains the scope of the proposed schedule, in terms of time and bearers """ - def __init__(self, start, end, bearers=[]): + def __init__(self, start=None, end=None, bearers=[]): """ :param start: Scope start time, if not specified this is calculated from the programmes :type start: datetime @@ -649,7 +707,7 @@ class Schedule: Contains programmes within a given time period. """ - def __init__(self, scope : Scope=None, created=datetime.datetime.now(tzlocal()), version=1, originator=None): + def __init__(self, scope=None, created=datetime.datetime.now(tzlocal()), version=1, originator=None): """x :param scope: Defined scope, otherwise proposed from the schedule :type scope: Scope @@ -660,6 +718,7 @@ def __init__(self, scope : Scope=None, created=datetime.datetime.now(tzlocal()), :param originator: Originator of the schedule :type originator: string """ + if scope==None and type(scope) is not Scope: scope=Scope() if type(scope) is not Scope: raise ValueError("scope must be a Scope object") self.scope = scope self.created = created diff --git a/src/spi/binary/__init__.py b/src/spi/binary/__init__.py index 0a0fd1c..1eabb66 100644 --- a/src/spi/binary/__init__.py +++ b/src/spi/binary/__init__.py @@ -5,6 +5,7 @@ import datetime, dateutil.tz import logging import sys +from jdcal import gcal2jd from datetime import timedelta logger = logging.getLogger("spi.binary") @@ -21,10 +22,10 @@ class Ensemble: :type version: integer """ - def __init__(self, ecc, eid, version=1): + def __init__(self, ecc, eid, version=1, names=[]): self.ecc = ecc self.eid = eid - self.names = [] + self.names = names self.descriptions = [] self.media = [] self.keywords = [] @@ -42,11 +43,12 @@ def __repr__(self): class Element: - def __init__(self, tag, attributes=None, children=None, cdata=None): + def __init__(self, tag, attributes=None, children=None, cdata=None, rawbits=None): self.tag = tag self.attributes = (attributes if attributes is not None else []) self.children = (children if children is not None else []) self.cdata = cdata + self.rawbits = rawbits logger.debug('created new element: %s', self) def tobytes(self): @@ -72,12 +74,17 @@ def tobytes(self): if self.cdata is not None: logger.debug('rendering cdata: %s', self.cdata) data += self.cdata.tobytes() - + + # encode Rawbits + if self.rawbits is not None: + logger.debug('rendering rawbits: %s', self.rawbits) + data += self.rawbits + # b0-b7: element tag bits = encode_number(self.tag, 8) # b8-15: element data length (0-253 bytes) - # b16-31: extended element length (256-65536 bytes) + # b16-31: extended element length (254-65536 bytes) # b16-39: extended element length (65537-16777216 bytes) datalength = len(data)/8 if datalength == 0: @@ -137,8 +144,8 @@ def frombits(bits): i = 0 e = Element(tag) logger.debug('parsing data of length %d bytes for element with tag 0x%02x', datalength, tag) - while i < data.length(): - logger.debug('now parsing at offset %d / %d', i/8, data.length()/8) + while i < len(data): + logger.debug('now parsing at offset %d / %d', i/8, len(data)/8) child_tag = int(data[i:i+8].to01(), 2) child_datalength = int(data[i+8:i+16].to01(), 2) start = 16 @@ -150,10 +157,10 @@ def frombits(bits): start = 40 logger.debug('child with tag 0x%02x for parent tag 0x%02x at offset %d has data length of %d bytes', child_tag, tag, i/8, child_datalength) end = start + (child_datalength * 8) - if i + end > data.length(): - raise ValueError('end of data for element with tag 0x%02x at offset %d requested is beyond length: %d > %d: %s' % (child_tag, i/8, (i + end)/8, data.length() / 8, bitarray_to_hex(data[i:i+64]))) + if i + end > len(data): + raise ValueError('end of data for element with tag 0x%02x at offset %d requested is beyond length: %d > %d: %s' % (child_tag, i/8, (i + end)/8, len(data) / 8, bitarray_to_hex(data[i:i+64]))) child_data = data[i + start : i + end] - if child_data.length() < 16*8: logger.debug('child element with tag 0x%02x for parent 0x%02x has data: %s', child_tag, tag, bitarray_to_hex(child_data)) + if len(child_data) < 16*8: logger.debug('child element with tag 0x%02x for parent 0x%02x has data: %s', child_tag, tag, bitarray_to_hex(child_data)) # attributes if child_tag >= 0x80 and child_tag <= 0x87: @@ -175,6 +182,7 @@ def frombits(bits): # default language elif child_tag == 0x06: logger.debug('parsing child as a default language (not yet implemented)') + print('trying to decode the language tag') pass # children elif child_tag >= 0x02 and child_tag <= 0x36: @@ -228,17 +236,17 @@ def tobytes(self): # b8-15: element data length (0-253 bytes) # b16-31: extended element length (256-65536 bytes) # b16-39: extended element length (65537-16777216 bytes) - datalength = bits2bytes(data.length()) + datalength = bits2bytes(len(data)) if datalength <= 253: bits += encode_number(datalength, 8) elif datalength >= 254 and datalength <= 1<<16: tmp = bitarray() - tmp.fromstring('\xfe') + tmp.fromstring(b'\xfe') bits += tmp bits += encode_number(datalength, 16) elif datalength > 1<<16 and datalength <= 1<<24: tmp = bitarray() - tmp.fromstring('\xff') + tmp.fromstring(b'\xff') bits += tmp bits += encode_number(datalength, 24) else: raise ValueError('element data length exceeds the maximum allowed by the extended element length (24bits): %s > %s' + datalength + " > " + (1<<24)) @@ -318,7 +326,8 @@ def __repr__(self): genre_map = dict( IntentionCS=1, FormatCS=2, - ContentCS=3, # what happened to 4?! + ContentCS=3, + IntendedAudienceCS=4, OriginationCS=5, ContentAlertCS=6, MediaTypeCS=7, @@ -334,6 +343,7 @@ def encode_genre(genre): bits.setall(False) # b0-3: RFU(0) + bits += encode_number(0,4) # b4-7: CS cs = segments[4] if cs in list(genre_map.keys()): cs_val = genre_map[cs] @@ -358,9 +368,9 @@ def decode_genre(bits): level = '%d' % cs_val # optional schema levels - if bits.length() > 8: + if len(bits) > 8: i = 8 - while i < bits.length(): + while i < len(bits): sublevel = int(bits[i:i+8].to01(), 2) level += '.%d' % sublevel i += 8 @@ -387,12 +397,7 @@ def encode_timepoint(timepoint): # b0: RFA(0) # b1-17: Date - a = (14 - timepoint.month) / 12 - y = timepoint.year + 4800 - a - m = timepoint.month + (12 * a) - 3 - jdn = timepoint.day + ((153 * m) + 2) / 5 + (365 * y) + (y / 4) - (y / 100) + (y / 400) - 32045 - jd = jdn + timepoint.hour / 24 + timepoint.minute / 1440 + timepoint.second / 86400 - mjd = (int)(jd - 2400000.5) + mjd = int((gcal2jd(timepoint.year,timepoint.month,timepoint.day))[1]) bits += encode_number(mjd, 17) # b18: RFA(0) @@ -536,7 +541,7 @@ def decode_contentid(bits): xpad = None try: - if bits.length() == 24: # EnsembleId + if len(bits) == 24: # EnsembleId # ECC, EId ecc = int(bits[0:8].to01(), 2) eid = int(bits[8:24].to01(), 2) @@ -577,7 +582,7 @@ def decode_tokentable(bits): tokens = {} i = 0 - while i < bits.length(): + while i < len(bits): tag = int(bits[i:i+8].to01(), 2) length = int(bits[i+8:i+16].to01(), 2) data = bits[i+16:i+16+(length*8)].tobytes().decode(DEFAULT_ENCODING) @@ -600,7 +605,7 @@ def decode_tokentable(bits): def decode_enum(parent_tag, tag, bits): - if bits.length() != 8: raise ValueError('enum data for parent/attribute 0x%02x/0x%02x is of incorrect length: %d bytes' % (parent_tag, tag, bits.length()/8)) + if len(bits) != 8: raise ValueError('enum data for parent/attribute 0x%02x/0x%02x is of incorrect length: %d bytes' % (parent_tag, tag, len(bits)/8)) value = int(bits.to01(), 2) key = (parent_tag, tag, value) @@ -626,21 +631,23 @@ def tobytes(self): bits.frombytes(b'\x01') # b8-15: element data length (0-253 bytes) - # b16-31: extended element length (256-65536 bytes) + # b16-31: extended element length (254-65536 bytes) # b16-39: extended element length (65537-16777216 bytes) + datalength = len(self.value.encode()) # ensure we get the right count for the encoding + if datalength <= 253: tmp = encode_number(datalength, 8) bits += tmp elif datalength >= 254 and datalength <= 1<<16: tmp = bitarray() - tmp.frombytes('\xfe') + tmp.frombytes(b'\xfe') bits += tmp tmp = encode_number(datalength, 16) bits += tmp elif datalength > 1<<16 and datalength <= 1<<24: tmp = bitarray() - tmp.frombytes('\xff') + tmp.frombytes(b'\xff') bits += tmp tmp = encode_number(datalength, 24) bits += tmp @@ -691,7 +698,9 @@ def marshall_serviceinfo(info, ensemble): # default language default_language_element = Element(0x06) - default_language_element.attributes.append(Attribute(0x80, DEFAULT_LANGUAGE, encode_string)) # TODO make this configurable in a better way + language = bitarray() + language.frombytes(DEFAULT_LANGUAGE.encode('utf-8')) + default_language_element.rawbits = language info_element.children.append(default_language_element) # ensemble @@ -728,10 +737,10 @@ def build_schedule(schedule): if schedule.originator is not None: schedule_element.attributes.append(Attribute(0x82, schedule.originator, encode_string)) - # schedule scope TODO - #scope = schedule.get_scope() - #if scope is not None: - # schedule_element.children.append(build_scope(scope)) + # schedule scope + scope = schedule.scope + if scope is not None and scope.start is not None and scope.end is not None: + schedule_element.children.append(build_scope(scope)) # programmes for programme in schedule.programmes: @@ -756,7 +765,8 @@ def build_schedule(schedule): child = build_location(location) programme_element.children.append(child) # media - if programme.media: programme_element.children.append(build_mediagroup(programme.media)) + for media in programme.media: + programme_element.children.append(build_mediagroup(media)) # genre for genre in programme.genres: child = build_genre(genre) @@ -842,30 +852,29 @@ def build_description(description): mediagroup_element.children.append(description_element) return mediagroup_element -def build_mediagroup(all_media): +def build_mediagroup(media): + mediagroup_element = Element(0x13) - - for media in all_media : - - if not isinstance(media, Multimedia): - raise ValueError('object must be of type %s (is %s)' % (Multimedia.__name__, type(media))) - - media_element = Element(0x2b) + + if not isinstance(media, Multimedia): + raise ValueError('object must be of type %s (is %s)' % (Multimedia.__name__, type(media))) + + media_element = Element(0x2b) - if media.content is not None: - media_element.attributes.append(Attribute(0x80, media.content, encode_string)) - if media.url is not None: - media_element.attributes.append(Attribute(0x82, media.url, encode_string)) - if media.type == Multimedia.LOGO_UNRESTRICTED: - media_element.attributes.append(Attribute(0x83, 0x02, encode_number, 8)) - if media.width: media_element.attributes.append(Attribute(0x84, media.width, encode_number, 16)) - if media.height: media_element.attributes.append(Attribute(0x85, media.height, encode_number, 16)) - if media.type == Multimedia.LOGO_COLOUR_SQUARE: - media_element.attributes.append(Attribute(0x83, 0x04, encode_number, 8)) - if media.type == Multimedia.LOGO_COLOUR_RECTANGLE: - media_element.attributes.append(Attribute(0x83, 0x06, encode_number, 8)) + if media.content is not None: + media_element.attributes.append(Attribute(0x80, media.content, encode_string)) + if media.url is not None: + media_element.attributes.append(Attribute(0x82, media.url, encode_string)) + if media.type == Multimedia.LOGO_UNRESTRICTED: + media_element.attributes.append(Attribute(0x83, 0x02, encode_number, 8)) + if media.width: media_element.attributes.append(Attribute(0x84, media.width, encode_number, 16)) + if media.height: media_element.attributes.append(Attribute(0x85, media.height, encode_number, 16)) + if media.type == Multimedia.LOGO_COLOUR_SQUARE: + media_element.attributes.append(Attribute(0x83, 0x04, encode_number, 8)) + if media.type == Multimedia.LOGO_COLOUR_RECTANGLE: + media_element.attributes.append(Attribute(0x83, 0x06, encode_number, 8)) - mediagroup_element.children.append(media_element) + mediagroup_element.children.append(media_element) return mediagroup_element @@ -913,7 +922,8 @@ def build_programme_event(event): for location in event.locations: event_element.children.append(build_location(location)) # media - if event.media: event_element.children.append(build_mediagroup(event.media)) + for media in event.media: + event_element.children.append(build_mediagroup(media)) # genre for genre in event.genres: event_element.children.append(build_genre(genre)) @@ -951,8 +961,9 @@ def build_service(service): service_element.children.append(build_description(description)) # media - if service.media: service_element.children.append(build_mediagroup(service.media)) - + for media in service.media: + service_element.children.append(build_mediagroup(media)) + # genre for genre in service.genres: service_element.children.append(build_genre(genre)) @@ -967,11 +978,10 @@ def build_service(service): # radiodns lookup if service.lookup: - from urllib.parse import urlparse - url = urlparse(service.lookup) + radiodns = str(service.lookup).partition('/') lookup_element = Element(0x31) - lookup_element.attributes.append(Attribute(0x80, url.netloc, encode_string)) - lookup_element.attributes.append(Attribute(0x81, url.path[1:], encode_string)) + lookup_element.attributes.append(Attribute(0x80, radiodns[0], encode_string)) + lookup_element.attributes.append(Attribute(0x81, radiodns[2], encode_string)) service_element.children.append(lookup_element) return service_element @@ -997,7 +1007,8 @@ def build_ensemble(ensemble, services): event_element.children.append(build_description(description)) # media - if ensemble.media: ensemble_element.children.append(build_mediagroup(ensemble.media)) + for media in ensemble.media: + ensemble_element.children.append(build_mediagroup(media)) # keywords diff --git a/src/spi/test/BearerTest.py b/src/spi/test/BearerTest.py new file mode 100644 index 0000000..6b30952 --- /dev/null +++ b/src/spi/test/BearerTest.py @@ -0,0 +1,16 @@ +from spi import * + +import unittest + +class BearerTest(unittest.TestCase): + + def test_fm_parse_valid(self): + print('here') + FmBearer.fromstring('fm:ce1.c985.09580') + FmBearer.fromstring('fm:ce1.c985.*') + + @unittest.expectedFailure + def test_fm_parse_invalid(self): + FmBearer.fromstring('fm:cee1.c85.095802') + FmBearer.fromstring('fm:ce1.*.09580') + diff --git a/src/spi/xml/__init__.py b/src/spi/xml/__init__.py index 34b90cd..aba152b 100644 --- a/src/spi/xml/__init__.py +++ b/src/spi/xml/__init__.py @@ -29,8 +29,6 @@ SCHEMA_XSD = 'spi_31.xsd' XSI_NS = 'http://www.w3.org/2001/XMLSchema-instance' -namespaces = { "spi" : SCHEMA_NS } - logger = logging.getLogger('spi.xml') class MarshallListener: @@ -551,22 +549,22 @@ def parse_time(timeElement): else: raise ValueError('unknown time element: %s' % timeElement) -def parse_bearer(bearer_element, listener): - listener.on_element(bearer_element) +def parse_bearer(bearer_element): uri = bearer_element.attrib['id'] - try: - if uri.startswith('dab'): - bearer = DabBearer.fromstring(uri) - elif uri.startswith('fm'): - bearer = FmBearer.fromstring(uri) - elif uri.startswith('http'): + if uri.startswith('dab'): + bearer = DabBearer.fromstring(uri) + elif uri.startswith('fm'): + bearer = FmBearer.fromstring(uri) + elif uri.startswith('http') or uri.startswith('https'): + if "mimeValue" not in bearer_element.attrib: bearer = IpBearer(uri) else: - raise ValueError('bearer %s not recognised' % uri) - except: + bearer = IpBearer(uri, content=bearer_element.attrib.get('mimeValue')) + elif uri.startswith('hd'): bearer = IpBearer("http://null/") - logger.debug('bearer %s is malformed', uri) - + logger.debug('bearer %s is not useful for DAB SPI', uri) + else: + raise ValueError('bearer %s not recognised' % uri) if 'cost' in bearer_element.attrib: bearer.cost = int(bearer_element.attrib['cost']) if 'offset' in bearer_element.attrib: @@ -640,21 +638,36 @@ def parse_programme(programmeElement, listener): return programme -def parse_schedule(scheduleElement): +def parse_schedule(scheduleElement, listener): schedule = Schedule() if 'creationTime' in scheduleElement.attrib: schedule.created = isodate.parse_datetime(scheduleElement.attrib['creationTime']) if 'version' in scheduleElement.attrib: schedule.version = int(scheduleElement.attrib['version']) if 'originator' in scheduleElement.attrib: schedule.originator = scheduleElement.attrib['originator'] + scope = Scope() + scopeElement = scheduleElement.find('spi:scope', namespaces) + if scopeElement is not None: + if 'startTime' in scopeElement.attrib and 'endTime' in scopeElement.attrib: + scope.start = isodate.parse_datetime(scopeElement.attrib['startTime']) + scope.end = isodate.parse_datetime(scopeElement.attrib['endTime']) + else: + scope.start = None + scope.end = None + + for serviceScope in scopeElement.findall('spi:serviceScope'): + scope.bearers.append(serviceScope) + + schedule.scope = scope + for programmeElement in scheduleElement.findall('spi:programme', namespaces): - schedule.programmes.append(parse_programme(programmeElement)) + schedule.programmes.append(parse_programme(programmeElement, listener)) return schedule -def parse_programmeinfo(root): +def parse_programmeinfo(root, listener): logger.debug('parsing programme info from root: %s', root) schedules = [] for schedule_element in root.findall('spi:schedule', namespaces): - schedule = parse_schedule(schedule_element) + schedule = parse_schedule(schedule_element, listener) schedules.append(schedule) info = ProgrammeInfo(schedules=schedules) return info @@ -732,8 +745,7 @@ def parse_service(service_element, listener): # bearers for child in service_element.findall("spi:bearer", namespaces): - if child.attrib.has_key("id"): service.bearers.append(parse_bearer(child)) - service.bearers.append(parse_bearer(child, listener)) + if "id" in child.attrib: service.bearers.append(parse_bearer(child)) # media for media_element in service_element.findall("spi:mediaDescription", namespaces): @@ -750,7 +762,7 @@ def parse_service(service_element, listener): # links for child in service_element.findall("spi:link", namespaces): - if child.attrib.has_key("uri"): service.links.append(parse_link(child)) + if "uri" in child.attrib: service.links.append(parse_link(child)) # keywords for child in service_element.findall("spi:keywords", namespaces): @@ -790,16 +802,57 @@ def unmarshall(i, listener=UnmarshallListener()): doc = parse(d) root = doc.getroot() logger.debug('got root element: %s', root) - + + global SCHEMA_NS, namespaces + + #test first for spi 31 + SCHEMA_NS = 'http://www.worlddab.org/schemas/spi/31' + namespaces = { "spi" : SCHEMA_NS } + logger.debug('Now parsing the namespace %s', SCHEMA_NS) + if root.tag == '{%s}serviceInformation' % SCHEMA_NS: return parse_serviceinfo(root, listener) elif root.tag == '{%s}epg' % SCHEMA_NS: if len(root.findall("spi:schedule", namespaces)): - return parse_programmeinfo(root) + return parse_programmeinfo(root, listener) if len(root.findall("spi:programmeGroups", namespaces)): - return parse_groupinfo(root) + return parse_groupinfo(root, listener) else: raise Exception('epg element does not contain either schedules or programme groups') - else: - raise Exception('Arrgh! this be neither serviceInformation nor epg - to Davy Jones\' locker with ye!') - + + #test next for spi 33 + SCHEMA_NS = 'http://www.worlddab.org/schemas/spi/33' + namespaces = { "spi" : SCHEMA_NS } + logger.debug('Now parsing the namespace %s', SCHEMA_NS) + + if root.tag == '{%s}serviceInformation' % SCHEMA_NS: + return parse_serviceinfo(root, listener) + elif root.tag == '{%s}epg' % SCHEMA_NS: + if len(root.findall("spi:schedule", namespaces)): + return parse_programmeinfo(root, listener) + if len(root.findall("spi:programmeGroups", namespaces)): + return parse_groupinfo(root, listener) + else: + raise Exception('epg element does not contain either schedules or programme groups') + + #test next for spi 34 + SCHEMA_NS = 'http://www.worlddab.org/schemas/spi' + namespaces = { "spi" : SCHEMA_NS } + logger.debug('Now parsing the namespace %s', SCHEMA_NS) + + if root.tag == '{%s}serviceInformation' % SCHEMA_NS: + logger.debug('Matched root %s and going into parse', SCHEMA_NS) + + return parse_serviceinfo(root, listener) + elif root.tag == '{%s}epg' % SCHEMA_NS: + if len(root.findall("spi:schedule", namespaces)): + return parse_programmeinfo(root, listener) + if len(root.findall("spi:programmeGroups", namespaces)): + return parse_groupinfo(root, listener) + else: + raise Exception('epg element does not contain either schedules or programme groups') + + + + raise Exception('Arrgh! this be neither serviceInformation nor epg - to Davy Jones\' locker with ye!') +