Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
8f6b940
Reminder to select the python2 branch for python 2
nickpiggott Apr 21, 2021
8248e81
interim changes
May 12, 2021
ee8a2de
Update __init__.py
nickpiggott Mar 7, 2022
e049728
Update setup.py
nickpiggott Mar 7, 2022
0d90f0d
Fix bitarray version at 1.2.0
nickpiggott Mar 7, 2022
89eb9e4
Update setup.py
nickpiggott Mar 11, 2022
54aa625
Update __init__.py
nickpiggott Mar 11, 2022
52ccec3
Update setup.py
nickpiggott Mar 11, 2022
1aedf30
Update build_binary_programmeinfo.py
nickpiggott Mar 11, 2022
6003f3d
Lots of python3 tidy up
nickpiggott Mar 14, 2022
dab6c9c
Fixes to run under python 3
nickpiggott Mar 15, 2022
a0e043b
Fix ContentCS for value 4, and calculation of bit in Genre coding
nickpiggott Mar 15, 2022
0fbdafc
Update __init__.py
nickpiggott Mar 15, 2022
c603703
Fixed an incorrect byte range
nickpiggott Mar 15, 2022
1782feb
Force characters to by single byte additions
nickpiggott Mar 15, 2022
1d2e432
Force character additions to single byte
nickpiggott Mar 15, 2022
db9fd90
Merge pull request #7 from nickpiggott/master
nickpiggott Mar 15, 2022
bd8e679
amend setup.py to use setup tools
nickpiggott Mar 15, 2022
4ddced5
Merge pull request #5 from Opendigitalradio/master
nickpiggott Mar 15, 2022
bf97cb1
Change bitarray length calculation from var.length() to len(var)
nickpiggott Mar 16, 2022
bd63d34
Remove requirement for a specific (older) version of bitarray
nickpiggott Mar 16, 2022
cf34b81
Merge pull request #6 from Opendigitalradio/master
nickpiggott Mar 16, 2022
5e619a3
Added missing DEFAULT_ENCODING value
nickpiggott Apr 3, 2022
e51c83a
Merge pull request #7 from Opendigitalradio/master
nickpiggott Apr 3, 2022
af06b05
Merge branch 'interim_changes' into master
magicbadger Apr 3, 2022
9fedf07
Fix parsing of lookup into radiodns fqdn and serviceIdentifier attrib…
nickpiggott Apr 28, 2022
57fa70e
Merge pull request #8 from Opendigitalradio/master
nickpiggott Apr 28, 2022
e557608
Use jdcal to calculate Modified Julian Dates correctly
nickpiggott Apr 28, 2022
c31fb18
Merge pull request #9 from Opendigitalradio/master
nickpiggott Apr 28, 2022
fab7885
Minor change of code order
nickpiggott Apr 28, 2022
e176197
Added in scope handling
nickpiggott May 3, 2022
ff4aee6
Merge pull request #10 from Opendigitalradio/master
nickpiggott May 3, 2022
1c584cf
Updated the encoding of LANGUAGE on the root Service Information elem…
nickpiggott May 19, 2023
0f152e9
Merge pull request #9 from nickpiggott/master
nickpiggott May 19, 2023
665920a
Amended construction of mediaDescription elements so they only have
nickpiggott May 22, 2023
979066a
Builds mediaDescription elements with only a single media element
nickpiggott May 22, 2023
df4d2e4
Fixed broken bearer parsing from SI XML files
nickpiggott May 22, 2023
a74689b
Fixes to broken SI XML parsing
nickpiggott May 22, 2023
10d350d
Add ensemble label and shortlabel to binary encoding
nickpiggott May 22, 2023
a041dd0
Added the important word "not" for an HD bearer for DAB SPI
nickpiggott May 23, 2023
ab946d1
Merge pull request #10 from nickpiggott/master
nickpiggott May 23, 2023
46f738a
Support 3.1 and 3.3 SPI
nickpiggott Jul 4, 2024
6aa881f
Merge pull request #11 from nickpiggott/master
nickpiggott Jul 4, 2024
a370b59
Add support for 34 namespace
nickpiggott Jul 24, 2024
c95a762
Merge pull request #12 from nickpiggott/master
nickpiggott Jul 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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']
)
97 changes: 78 additions & 19 deletions src/spi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

MAX_SHORTCRID = 16777215
DEFAULT_LANGUAGE = "en"
DEFAULT_ENCODING = "utf-8"

class Text:
"""Abstract class for textual information"""
Expand Down Expand Up @@ -200,7 +201,7 @@ def __init__(self, ecc, eid, sid, scids=0, content=DAB_PLUS, xpad=None, cost=Non
dab:<gcc>.<eid>.<sid>.<scids>.<xpad> 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:
::
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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 '<DabBearer: %s>' % str(self)

def __eq__(self, other):
return str(self) == str(other)

class HdBearer(DigitalBearer):

Expand Down Expand Up @@ -292,9 +302,14 @@ def __str__(self):
def __repr__(self):
return '<DabBearer: %s>' % 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

Expand All @@ -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
"""

Expand All @@ -322,24 +342,31 @@ 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):
"""
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):
Expand All @@ -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
Expand All @@ -367,7 +423,9 @@ def __str__(self):

def __repr__(self):
return '<IpBearer: %s>' % str(self)


def __eq__(self, other):
return str(self) == str(other)

class ProgrammeInfo:
"""The root of a PI document"""
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading