Difference between revisions of "Python API Examples"

From MythTV Official Wiki
Jump to: navigation, search
m (Fix module link)
(Switch shebangs to /usr/bin/python3)
 
(16 intermediate revisions by the same user not shown)
Line 1: Line 1:
 +
{{Basic Services API Access}}
 
=Python Services API Cookbook=
 
=Python Services API Cookbook=
 +
{{Note box|Starting with v31, Python version 3 is supported. Adjust references to the host's Python version below as required.}}
 +
<br/>
 +
What follows are some working ''Python'' programs that use
 +
the MythTV [[Services API]]. If the user's language of
 +
choice is Perl, then see: [[Perl API examples]]
  
Some working examples using the MythTV [[Services API]].
+
Unlike [[Python Bindings]], only basic access to
If the user's language of choice is Perl, then see:
+
the Services API is provided here. Users will need to retrieve
[[Perl API examples]]
+
data from the Services/endpoints of interest to them. N.B. not
 +
everything available in ''Python Bindings' is available in the API.
  
The following will be demonstrated using a Python
+
There are excellent links that list the available services
module (included at the bottom of this page.) Users
+
and endpoints. See these: [[API parameters 30]] and [[API parameters 31]].
can choose to place the module in the same
 
directory as programs that use it, or '''alongside'''
 
the existing MythTV Python bindings.
 
  
There are excellent links that list the available endpoints
+
The examples depend on a module which is available in v30 and above.
(the services and actions following the port number) here:
+
Users of earlier versions can install the source themselves. See:
[[API parameters 0.27]] and [[API parameters 0.28]].
+
https://code.mythtv.org/cgit/mythtv/tree/mythtv/bindings/python/MythTV/services_api
  
These examples were written and tested using 0.28-pre.
+
This page was written and tested using 0.28-pre under ''python'' 2.7 and 3.4.
===About the [[#The_module_used_in_the_examples_above|module]]===
+
===About the Module===
 
First, it's not necessary to use it! Users can write their own code
 
First, it's not necessary to use it! Users can write their own code
 
to send information to the back/frontend. The module provides a
 
to send information to the back/frontend. The module provides a
reasonable template of what might be done. Included are checking
+
reasonable template of things that should be done. Included are checking
of the parameters, some optional debugging statements, optional
+
of the arguments, some optional debugging statements, optional
 
header choices and error handling.
 
header choices and error handling.
  
Regarding the options. The module will set the value of any uninitialized
+
The module formats a ''JSON'' response from the server into a ''Python'' ''dict''.
option to False. But the caller should set them perhaps based on a
+
=== Installing  and ''import''ing the module ===
command line argument (or just hardcode them as appropriate
+
<B>This is for users running versions below v30.</B>
for their own use.) If any option isn't set, a warning messages will be printed.
 
  
Importantly, the module reformats the JSON or XML response from
+
In v30+, the module is part of the Python bindings and can be
the server into a Python ''dict'' or an ''Element Tree'' respectively.
+
imported like this <code>from MythTV.services_api import send as api</code>.
  
As with any Python module, built-in help is available. Try the following:
+
Where the module is installed is up to the user. The
 +
recommendation is to choose ''one'' of the following.
 +
But first, decide which host(s) the module is to be installed on.
 +
It could be a backend, but that isn't necessary.
 +
Install it on any host(s) that want to access the ''Services API''
 +
====Install in the user's bin directory (easiest)====
 +
If the module is installed in the same directory (typically: ''~/bin'') as the program(s) that
 +
use it, then include a line like this in those programs:
 
<pre>
 
<pre>
$ python
+
import services_api as api
>>> from MythTVServicesAPI import Utilities as util # Or:
+
</pre>
>>> import Utilities as util
+
The ''as api'' is optional, but convenient.
>>> help(util)
+
All examples in this page will assume this
>>> control-D # After the help prints
+
method.
 +
 
 +
====Install elsewhere and set PYTHONPATH====
 +
Put the module somewhere other than in the
 +
existing $PATH where programs using it are located.
 +
Then set ''PYTHONPATH'' to point to that directory.
 +
For example, the following line could be added to
 +
a ''.profile'' or ''.bashrc'' file:
 +
<pre>
 +
export PYTHONPATH=/home/services_api
 +
</pre>
 +
Import it as in the above.
 +
====Install in a ''dist-packages'' directory (hardest/preferred)====
 +
Create a new directory '''below''' the existing
 +
''MythTV'' directory that might be found in directories
 +
like these:
 +
<pre>
 +
/usr/local/lib/python2.7/dist-packages/
 +
/usr/lib/python2.7/dist-packages/
 +
/usr/lib/python3.4/dist-packages
 +
/usr/local/lib/python3.4/dist-packages
 
</pre>
 
</pre>
 +
Create a new directory named ''services_api''
 +
and put the ''__init__.py'', '' send.py'',  ''utilities.py'' and  ''_version.py''
 +
file under it. Savy users will use a ''Makefile'' and ''setup.py'' program to do
 +
this (and create ''.pyc'' files etc.)
  
The following examples will all request the response from the
+
''MacPorts'' users might find the bindings under:
back/frontend and return it in Python dictionary format.
 
=== Load the module ===
 
Choose one of the following. If the module is installed (for example)
 
in the system's dist-packages directory, use this:
 
<pre>from MythTVServicesAPI import Utilities as api</pre>
 
That assumes there is a MythTVServicesAPI directory with the
 
Utilities.py file underneath it (and an empty  __init__.py
 
file there too.) An example of the directory where the above
 
would be installed is: ''/usr/local/lib/python2.7/dist-packages''
 
which is where users would likely find the existing MythTV Python
 
bindings. ''MacPorts'' users might find the bindings under:
 
 
''/opt/dvr/bin/python2.7''.
 
''/opt/dvr/bin/python2.7''.
  
Or, if the module is installed in the same directory as the program(s) that
+
Importing the module would then be done like this:
use it, do this:
 
 
<pre>
 
<pre>
import Utilities as api
+
from MythTV.services_api import send as api
 
</pre>
 
</pre>
  
==''Myth/GetHostName'' endpoint example==
+
===Displaying the module's help information===
 +
As with any proper ''Python'' package, built-in help
 +
is available. Try the following:
 +
<pre>
 +
$ python3 # or python2
 +
>>> from MythTV.services_api import send as api
 +
</pre>
 +
Followed by:
 +
<pre>
 +
>>> help(api)
 +
>>> control-D # After the help prints
 +
</pre>
 +
 
 +
==Examples==
 +
Once the module is installed, the following examples will work.
 +
 
 +
Notice that in the following, ''localhost'' is used for
 +
the ''host'' argument to ''Send()''. That means these will be run on
 +
a ''MythTV'' backend. But that isn't required. The
 +
module and examples can  be  put on any host that has
 +
access to the backend and run from there. Just replace
 +
''host='' 'localhost' with ''host='' <backendHostNameOrIpAddress>.
 +
===''Myth/GetHostName'' endpoint example===
 
Getting the backend's hostname returns the simplest response
 
Getting the backend's hostname returns the simplest response
 
in the API. It's the easiest to parse and has the hostname
 
in the API. It's the easiest to parse and has the hostname
 
of the backend (really, it's the profile name used in the DB
 
of the backend (really, it's the profile name used in the DB
 
and honors the LocalHostName line in config.xml, but that's
 
and honors the LocalHostName line in config.xml, but that's
another Wiki.))
+
another Wiki.) It's a great place to start learning about the
 +
API.
  
Put the following in a file, perhaps simpleAPItest.py.
+
Put the following in a file, perhaps simple_api_test.py.
Make it executable and run it.
+
Make it executable and run it. Adjust the ''#!'' line
 +
as required. For example, ''MacPorts'' users might replace the 1st
 +
line below with ''#!/opt/dvr/bin/python2.7''
  
MacPorts users replace the 1st line below with ''#!/opt/dvr/bin/python2.7''
+
{{Python|simple_api_test.py|<pre>
 +
#!/usr/bin/python3
 +
from MythTV.services_api import send as api
  
{{Python|simpleAPItest.py|<pre>
+
BACKEND = api.Send(host='localhost')
#!/usr/bin/env python
+
resultDict = BACKEND.send(endpoint='Myth/GetHostName')
import Utilities as api
 
  
opts = {'debug':False, 'etree':False, 'nogzip':False, 'wrmi':False}
+
print 'Entire response:', resultDict
 
+
print 'Backend hostname (MythTV profile name) is: ', resultDict['String']
resultDict = api.Send(host='localhost', endpoint='Myth/GetHostName', opts=opts)
+
</pre>}}
  
print 'Dictionary contents:', resultDict
+
As mentioned above ''as api'' is optional. Calling ''send.Send(...)''
print 'My backend hostname (MythTV profile name) is: ', resultDict['String']
+
is also valid. There's nothing special about ''resultDict'' other than
</pre>}}
+
it reminds the user that the results are being returned in a ''Python''
 +
''dict'' format.
  
Look at the 2 lines of output.
+
Look at the two lines of output. The 1st is the complete response, a
 +
simple dictionary with one element, the ''key'' ''String'' contains
 +
the ''value'' ''someHostName''. This is important as later examples
 +
will be more complex dictionaries.
 
<pre>
 
<pre>
Dictionary contents: {u'String': u'backendHostName'}
+
Entire response: {u'String': u'backendHostName'}
My backend hostname (MythTV profile name) is: backendHostName
+
Backend hostname (MythTV profile name) is: someBackendHostName
 
</pre>
 
</pre>
==The ''Myth/GetTimeZone'' endpoint example==
 
A slightly more complex example. Import the module
 
and set the opts as above and then try:
 
  
{{Python|getTimeZoneInfo.py|<pre>
+
===The ''Myth/GetTimeZone'' endpoint example===
r = api.Send(host='backendHostName', endpoint='Myth/GetTimeZone')
+
A slightly more complex example:
 +
 
 +
{{Python|get_timezone_info.py|<pre>
 +
#!/usr/bin/python3
 +
from MythTV.services_api import send as api
 +
 
 +
BACKEND = api.Send(host='localhost')
 +
r = BACKEND.send(endpoint='Myth/GetTimeZone')
  
 
print r
 
print r
 +
print r['TimeZoneInfo']
 
print r['TimeZoneInfo']['TimeZoneID']
 
print r['TimeZoneInfo']['TimeZoneID']
 
print r['TimeZoneInfo']['UTCOffset']
 
print r['TimeZoneInfo']['UTCOffset']
 
print r['TimeZoneInfo']['CurrentDateTime']
 
print r['TimeZoneInfo']['CurrentDateTime']
 
</pre>}}
 
</pre>}}
Expect to see something like this:
+
The GetTimeZone JSON response contains a dictionary (''TimeZoneInfo'') with dictionary inside it
 +
that has three ''keys''. Expect to see something like this:
 
<pre>
 
<pre>
 
{u'TimeZoneInfo': {u'TimeZoneID': u'America/Chicago', u'UTCOffset': u'-21600', u'CurrentDateTime': u'2015-11-13T19:26:18Z'}}
 
{u'TimeZoneInfo': {u'TimeZoneID': u'America/Chicago', u'UTCOffset': u'-21600', u'CurrentDateTime': u'2015-11-13T19:26:18Z'}}
 +
{u'TimeZoneID': u'America/Chicago', u'UTCOffset': u'-21600', u'CurrentDateTime': u'2015-11-13T19:26:18Z'}}
 
America/Chicago
 
America/Chicago
 +
-21600
 
2015-11-13T19:26:18Z
 
2015-11-13T19:26:18Z
-21600
 
 
</pre>
 
</pre>
==Working program using the ''Dvr/GetUpcomingList'' endpoint==
+
===Myth/TestDBSettings (POST Example)===
Here's a simple script, with a bit of formatting, that
+
Take note that some endpoints require an HTTP POST (as opposed to the default GET.)
does a practical task.
+
This is detailed in the Wikis for each service. Anything that changes
 +
data must use a POST and the following is another example.
  
Put the following in a file, like: ''shortPrintUpcoming.py'' and
+
Try the following. Remember, ''backend_host'' and ''db_host'' are
type: ''./shortPrintUpcoming.py backendHostNameOrIPaddress''
+
frequently the same, but can be different.
  
Note the line: ''progs = resp_dict['ProgramList']['Programs']''
+
In opts, ''wrmi'' (We Really Mean It) must be ''True''. Try setting it to ''False''
below. Rather than referencing each upcoming program as:
+
and see the result (it's good for debugging before an endpoint that changes data is
''resp_dict['ProgramList']['Programs'][upcoming]'', they can be examined
+
used.)
as: ''progs[upcoming]...''.
+
 
 +
The expected response will end with: ''{"bool": "true"}''.  
 +
{{Python|simple_post.py|<pre>
 +
#!/usr/bin/python3
 +
 
 +
import os.path as path
 +
import sys
 +
from MythTV.services_api import send as api
  
Also note that there's yet another level under ''progs'', it's
+
try:
the ''['Recording']'' portion where just the ''StartTs'' is
+
    BEHOST = sys.argv[1]
used in this example.
+
    DBHOST = sys.argv[2]
{{Python|shortPrintUpcoming.py|<pre>
+
    DBUSER = sys.argv[3]
#!/usr/bin/env python
+
    DBPSWD = sys.argv[4]
# -*- coding: utf-8 -*-
+
except IndexError:
 +
    sys.exit('\nUsage: {} backend_host db_host db_user db_password'
 +
            .format(path.basename(sys.argv[0])))
  
# shortPrintUpcoming.py: Print Start Times, Titles and SubTitles
+
POSTDATA = {'HostName': DBHOST, 'UserName': DBUSER, 'Password': DBPSWD}
  
import os.path as path, sys, Utilities as api
+
BACKEND = api.Send(host=BEHOST)
 +
r = BACKEND.send(endpoint='Myth/TestDBSettings',
 +
            postdata=POSTDATA, opts={'debug': True, 'wrmi': True})
  
try:
+
print r
    host = sys.argv[1]
+
</pre>}}
except:
 
    sys.exit( '\nUsage: {} HostnameOrIP'.format(path.basename(sys.argv[0])))
 
  
resp_dict = api.Send(host=host, endpoint='Dvr/GetUpcomingList')
+
===Working program using the ''Dvr/GetUpcomingList'' endpoint===
 +
Here's a simple program, with a bit of formatting, that
 +
does a practical task. Refer to the tools in the beginning of
 +
this page to see the ''XML'' output of this endpoint. Viewing it
 +
with a browser may make the ''dict''s easier to understand.
  
count = int(resp_dict['ProgramList']['Count'])
+
Put the following in a file, like: ''short_print_upcoming.py'' and
progs = resp_dict['ProgramList']['Programs']
+
type: ''short_print_upcoming.py backendHostNameOrIPaddress''
  
if count < 1: sys.exit('\nNo upcoming recordings found.\n')
+
Note the line: ''progs = resp_dict['ProgramList']['Programs']''
 +
below. Rather than referencing each upcoming program as:
 +
''resp_dict['ProgramList']['Programs'][upcoming]'', they can be examined
 +
as: ''progs[upcoming]...''.
 +
''upcoming'' here is the ''index'' into a ''list'' contained in the ''Programs''
 +
''key''.
  
for upcoming in range(count):
+
Also note that there's yet another ''dict'' under ''progs'', it's
    print u'  {}  {:45.45}  {:15.15}'.format(
+
the ''['Recording']'' ''dict'' where only the ''StartTs'' is
        progs[upcoming]['Recording']['StartTs'],
+
used in this example.
        progs[upcoming]['Title'],
+
{{Python|short_print_upcoming.py|<pre>
        progs[upcoming]['SubTitle']).encode('utf-8')
+
#!/usr/bin/python3
 +
# -*- coding: utf-8 -*-                                                       
 +
                                                                               
 +
"""Print Start Times, Titles and SubTitles"""                                 
 +
                                                                               
 +
from __future__ import print_function                                         
 +
import sys                                                                     
 +
from MythTV.services_api import send as api                                   
 +
                                                                               
 +
                                                                               
 +
def main():                                                                   
 +
    """Check host, request upcomings and print"""                             
 +
                                                                               
 +
    try:                                                                       
 +
        host = sys.argv[1]                                                     
 +
    except IndexError:                                                         
 +
        host = 'localhost'                                                     
 +
                                                                               
 +
    backend = api.Send(host=host)                                             
 +
                                                                               
 +
    try:                                                                       
 +
        resp_dict = backend.send(endpoint='Dvr/GetUpcomingList')               
 +
    except RuntimeError as error:                                             
 +
        sys.exit('\nFatal error: "{}"'.format(error))                         
 +
                                                                               
 +
    count = int(resp_dict['ProgramList']['Count'])                             
 +
    progs = resp_dict['ProgramList']['Programs']                               
 +
                                                                               
 +
    if count < 1:                                                             
 +
        sys.exit('\nNo upcoming recordings found.\n')                         
 +
                                                                               
 +
    for upcoming in range(count):                                              
 +
        print('  {}  {:45.45}  {:15.15}'.format(                              
 +
            progs[upcoming]['Recording']['StartTs'],                          
 +
            progs[upcoming]['Title'],                                          
 +
            progs[upcoming]['SubTitle']))                                     
 +
                                                                               
 +
                                                                               
 +
if __name__ == '__main__':                                                     
 +
    main()                                                                    
 +
                                                                               
 +
# vim: set expandtab tabstop=4 shiftwidth=4 smartindent noai colorcolumn=80:
 
</pre>}}
 
</pre>}}
  
 
To see the way the response is returned, try adding lines like
 
To see the way the response is returned, try adding lines like
 +
these to the end of the program:
 
<pre>
 
<pre>
 
print resp_dict
 
print resp_dict
Line 159: Line 279:
 
the 2nd just the 0th program returned and the last
 
the 2nd just the 0th program returned and the last
 
is the same as the 2nd.
 
is the same as the 2nd.
==Full example using ''Dvr/GetUpcomingList''==
+
 
 +
===Full example using ''Dvr/GetUpcomingList''===
 
Using the ideas above, the following is a complete program
 
Using the ideas above, the following is a complete program
 
with command line arguments used to set some of the opts values,
 
with command line arguments used to set some of the opts values,
and set a variable host etc. Type: ''./printUpcoming --help''
+
and set a variable host etc. Type: ''./print_upcoming.py --help''
 
for details. Try the ''--debug'' argument to see the  
 
for details. Try the ''--debug'' argument to see the  
 
URLs generated by the program.
 
URLs generated by the program.
{{Python|printUpcoming.py|<pre>
+
 
#!/usr/bin/env python
+
The concepts here can serve as a guide to creating your own
 +
''Python'' programs.
 +
{{Python|print_upcoming.py|<pre>
 +
!/usr/bin/python3
 
# -*- coding: utf-8 -*-
 
# -*- coding: utf-8 -*-
  
#
+
'''
# printUpcoming.py: Print Start Times, Titles and SubTitles
+
Print Start Times, Titles and SubTitles.
# (optionally filtering by Titles/ChanId or limited by days.)
 
#
 
# This is an 'overly-commented', and simple example of using the Services
 
# API to do a task. It also forms a template for working with the API.
 
#
 
# Good Start:          printUpcoming.py --help
 
# Watch what it sends: printUpcoming.py --host=Name/IP --debug
 
#
 
 
 
##############################################################
 
# MythTVServicesAPI.Utilities handles backend connections    #
 
# re is used to allow things like --title to be partial and  #
 
#    case agnostic                                          #
 
# argparse is used in the following section                  #
 
##############################################################
 
from  datetime import datetime
 
from  MythTVServicesAPI import Utilities as API
 
import argparse, re, sys, time
 
 
 
##############################################################
 
# Add command arguments, as required, here. --host is likely #
 
# a minimum and mandatory unless the host is hardcoded into  #
 
# the script. Examples include string, integer and bool type #
 
# arguments. Printing the default values is also shown.      #
 
##############################################################
 
 
 
parser = argparse.ArgumentParser(description='Print Upcoming Programs',
 
    epilog='Default values are in ()s')
 
  
mandatory = parser.add_argument_group('requrired arguments')
+
Optionally filter by Titles/ChanId or limited by days. Optionally print
 +
the cast members for each recording. This is an 'overly-commented', and
 +
simple example of using the Services API to do a task. It also forms a
 +
template for working with the API.
  
parser.add_argument('--chanid', type=int, required=False, metavar='<chanid>',
+
Good Start:          print_upcoming.py --help
    help='filter on MythTV chanid, e.g. 1091 (none).')
+
Watch what it sends: print_upcoming.py --host=Name/IP --debug
 +
'''
  
parser.add_argument('--days', type=int, required=False, metavar='<days>',
+
from __future__ import print_function
    default=7, help='number of days of programs to print (%(default)s)')
+
from datetime import datetime
 +
import argparse
 +
import logging
 +
import re
 +
import sys
 +
import time
 +
from MythTV.services_api import send as api
 +
from MythTV.services_api import utilities as util
  
parser.add_argument('--debug', action='store_true',
+
DAY_IN_SECONDS = 24 * 60 * 60
    help='turn on debug messages (%(default)s)')
+
TIME_NOW = time.mktime(datetime.now().timetuple())
 
+
WHITE = '\033[0m'
mandatory.add_argument('--host', type=str, required=True,
 
    metavar='<hostname>', help='backend hostname')
 
 
 
parser.add_argument('--nogzip', action='store_true',
 
    help='tell the backend not to return gzipped data (%(default)s)')
 
 
 
parser.add_argument('--port', type=int, default=6544, metavar='<port>',
 
    help='port number of the Services API (%(default)s)')
 
 
 
parser.add_argument('--title', type=str, default='', required=False,
 
    metavar='<prog>', help='filter by title (none)')
 
 
 
args = parser.parse_args()
 
 
 
##############################################################
 
# Prepare the data to send to the backend via the API and    #
 
# then do it.                                                #
 
##############################################################
 
 
 
endpoint = 'Dvr/GetUpcomingList'
 
opts = { 'debug':False, 'etree':False, 'nogzip':False, 'wrmi':False }
 
rest    = 'ShowAll=false'
 
 
 
resp_dict = API.Send(host=args.host, port=args.port, opts=opts,
 
                        endpoint=endpoint, rest=rest)
 
 
 
##############################################################
 
# And also get the backend's timezone offset so that the    #
 
# start times in the response can be converted to local time.#
 
##############################################################
 
 
 
tz_dict = API.Send(host=args.host, port=args.port,
 
                  endpoint='Myth/GetTimeZone', opts=opts)
 
 
 
tzoffset = int(tz_dict['TimeZoneInfo']['UTCOffset'])
 
 
 
##############################################################
 
# Finally, loop through the results and select the items    #
 
# of interest. Ignore Titles that don't match the reqular    #
 
# expression if the --title argument was used. Same for      #
 
# chanids. The results are sorted by time, so this makes an  #
 
# attempt to only print --days worth of data.                #
 
##############################################################
 
 
 
WHITE = '\033[0m'
 
 
YELLOW = '\033[93m'
 
YELLOW = '\033[93m'
headerPrinted = False
 
one_day_in_seconds = 24 * 60 * 60
 
time_now = time.mktime(datetime.now().timetuple())
 
  
count = int(resp_dict['ProgramList']['Count'])
 
progs = resp_dict['ProgramList']['Programs']
 
  
if args.debug:
+
def flags_to_strings(flgs):
     print 'Debug: Upcoming recording count =', count
+
    '''
 +
    Convert the decimal flags to a printable string. From:
 +
    libs/libmyth/programtypes.h. The </> print for values that
 +
    haven't been defined here yet (and maybe should be.)
 +
    '''
 +
    strlst = list()
 +
    if flgs & (0x00fff):
 +
        strlst.append('<')
 +
    if flgs & (1 << 12):
 +
        strlst.append('Rerun')
 +
    if flgs & (1 << 13):
 +
        strlst.append('Dup')
 +
     if flgs & (1 << 14):
 +
        strlst.append('React')
 +
    if flgs & (0xf8000):
 +
        strlst.append('>')
 +
    return ', '.join(strlst)
  
if count < 1:
 
    sys.exit('\nNo upcoming recordings found.\n')
 
  
print '\nUpcoming programs sorted by StartTime'
+
def process_command_line():
print '\n  {}{:19}  {:45.45}  {}{}' \
+
    '''
     .format(YELLOW, 'StartTime', 'Title', 'SubTitle', WHITE)
+
    Add command arguments, as required, here. --host is likely
 +
     a minimum and mandatory unless it has a default value below.
 +
    Examples include string, integer and bool type arguments.
 +
    Printing the default values is also shown.
 +
    '''
  
for upcoming in range(count):
+
    parser = argparse.ArgumentParser(description='Print Upcoming Programs',
 +
                                    epilog='Default values are in ()s')
  
     title    = progs[upcoming]['Title']
+
     parser.add_argument('--cast', action='store_true',
    subtitle = progs[upcoming]['SubTitle']
+
                        help='include cast member names (%(default)s)')
    chanid  = int(progs[upcoming]['Channel']['ChanId'])
 
    startts  = API.UTCToLocal(progs[upcoming]['Recording']['StartTs'],
 
                                tzoffset=tzoffset)
 
  
     future_time = time.mktime(datetime.strptime(startts,
+
     parser.add_argument('--chanid', type=int, required=False,
                              "%Y-%m-%d %H:%M:%S").timetuple())
+
                        metavar='<chanid>',
    futureDays  = int((future_time - time_now ) / one_day_in_seconds)
+
                        help='filter on MythTV chanid, e.g. 1091 (none).')
  
     if futureDays >= args.days:
+
     parser.add_argument('--days', type=int, required=False, metavar='<days>',
        break
+
                        default=7,
 +
                        help='days of programs to print (%(default)s)')
  
     if (args.title  == '' or re.search(args.title, title, re.IGNORECASE)) and \
+
     parser.add_argument('--debug', action='store_true',
        (args.chanid == None or args.chanid == chanid):
+
                        help='turn on debug messages (%(default)s)')
  
        print u' {}  {:45.45}  {:15.15}' \
+
    parser.add_argument('--digest', type=str, metavar='<user:pass>',
                .format(startts, title, subtitle).encode('utf-8')
+
                        help='digest username:password')
  
# vim: set expandtab tabstop=4 shiftwidth=4 :
+
    parser.add_argument('--host', type=str, required=False,
</pre>}}
+
                        default='localhost',
By getting the time zone information from the backend, each start time
+
                        metavar='<hostname>',
in the output is converted to local time rather than the UTC returned
+
                        help='backend hostname (%(default)s)')
in the Services API. Another important concept in the above is the use
 
of ''print u'....'' followed by ''.encode('utf-8)''. That's critical
 
for Titles/SubTitles for example where there are characters like: ó.
 
==The module used in the examples above==
 
{{Python|Utilities.py|<pre>
 
# -*- coding: utf-8 -*-
 
  
""" Basic access utilities. """
+
    parser.add_argument('--port', type=int, default=6544, metavar='<port>',
 +
                        help='port number of the Services API (%(default)s)')
  
from datetime  import datetime, timedelta
+
    parser.add_argument('--showall', action='store_true',
from StringIO  import StringIO
+
                        help='include conflicts etc. (%(default)s)')
from xml.etree import cElementTree as ET
 
  
import gzip, httplib, json, re, sys, urllib2
+
    parser.add_argument('--title', type=str, default='', required=False,
 +
                        metavar='<title>', help='filter by title (none)')
  
__version__ = '0.28.a1'
+
    parser.add_argument('--version', action='version', version='%(prog)s 0.10')
  
ServerVersion = 'Set to back/frontend version after calls to Send()'
+
    return parser.parse_args()
  
def Send(method='GET', host='', port=6544, endpoint='', rest='', opts={}):
 
    """
 
  
     Form a URL and send it to the back/frontend. Error handling is done
+
def print_details(program, args):
     here too.
+
    '''
 +
     Print a single program's information. Apply the --title and --chanid
 +
    filters to select limited sets of recordings. Exit False if the
 +
    --days limit is reached.
 +
     '''
  
     Input:
+
     title = program['Title']
     ======
+
     subtitle = program['SubTitle']
 +
    flags = int(program['ProgramFlags'])
 +
    chanid = int(program['Channel']['ChanId'])
 +
    startts = util.utc_to_local(program['Recording']['StartTs'])
 +
    # status = int(program['Recording']['Status'])
 +
    start_time = time.mktime(datetime.strptime(startts, "%Y-%m-%d %H:%M")
 +
                            .timetuple())
  
     method:   Set to 'POST' for endpoints that change data. Omit method (or
+
     if int((start_time - TIME_NOW) / DAY_IN_SECONDS) >= args.days:
              set it to 'GET' for all other endpoints. Defaults to 'GET'.
+
        return -1
  
     host:    Must be set and is the hostname or IP of the back/frontend.
+
     if (args.title is None or re.search(args.title, title, re.IGNORECASE)) \
 +
            and (args.chanid is None or args.chanid == chanid):
  
    port:     Used if the backend is using a different port (unlikely) or
+
        print(u'  {:16.16}  {:40.40}  {:25.25}  {}'
               set to 6547 for frontend endpoints. Defaults to 6544.
+
               .format(startts, title, subtitle,
 +
                      flags_to_strings(flags)).encode('utf-8'))
  
    endpoint: Should be set. Example: Myth/GetHostName
+
        if args.cast:
 +
            cast = program['Cast']['CastMembers']
 +
            for i in range(100):
 +
                try:
 +
                    print(u'\t{} ({})'.format(cast[i]['Name'],
 +
                                              cast[i]['TranslatedRole'])
 +
                          .encode('utf-8'))
 +
                except IndexError:
 +
                    break
  
    rest:    May be set if the endpoint allows it. Example for
+
        return 1
              Myth/GetRecordedList: rest='Count=10&StorageGroup=Sports'
 
  
     opts      A dictionary of options that may be set in the calling
+
     return 0
              program. Default values will be used if callers don't
 
              pass their own.
 
  
              The defaults (typically changed by command line arguments) are
 
              all False.
 
  
     opts['debug']:  Set to True and the developed URL will be printed (for
+
def main():
                    debugging.)
+
     '''
 +
    Get the upcoming recordings from the backend and process them
 +
    based on the filters and number of days of interest.
 +
    '''
  
     opts['etree']:  If True, the response returned TO THIS MODULE will be
+
    args = process_command_line()
                    in XML. If False, JSON will be returned.
+
     opts = dict()
  
                    N.B. JSON will then be loaded into Python Dictionary
+
    logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
                    format. XML will be in an Element Tree.
+
    logging.getLogger('requests.packages.urllib3').setLevel(logging.WARNING)
 +
    logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING)
  
     opts['nogzip']: Don't request the back/frontend to gzip it's response
+
     try:
 
+
        opts['user'], opts['pass'] = args.digest.split(':')
    opts['wrmi']:  If True and method='POST', the URL is actually sent
+
     except (AttributeError, ValueError):
                    to the server.
+
         pass
 
 
                    If opts['wrmi']=False and method='POST' ***Nothing*** is
 
                    sent to the server.
 
                   
 
                    This is a failsafe that allows testing. You can examine
 
                    what's about to be sent before doing it (wrmi = We Really
 
                    Mean It.)
 
 
 
                        The response is formatted based on the value of
 
                        opts['etree'] (assume the caller chooses to
 
                        put the response in r.)
 
 
 
                        True;  r.tag = 'Warning', r.text = 'wrmi=False'
 
                        False; r['Warning'] = 'wrmi=False'
 
 
 
     Output:
 
    ======
 
 
 
    An error message and program exit, or the JSON response loaded into
 
    a dictionary or the XML response in ElementTree format.
 
 
 
    Whenever Send() is used, the global 'ServerVersion' is set to the value
 
    returned by the back/frontend in the HTTP Server: header. It is returned
 
    as just the version, e.g. 0.28. Callers can check if and *may* choose to
 
    adjust their code work with other versions.
 
 
 
    IF USING method='POST', TAKE EXTREME CAUTION!!! Use opts['wrmi']=False
 
    1st.
 
    """
 
 
 
    optionList = [ 'debug', 'etree', 'nogzip', 'wrmi' ]
 
 
 
    for option in optionList:
 
        try:
 
            opts[option]
 
         except:
 
            print 'Warning: opts["{}"] is not set, using: False'.format(option)
 
            opts[option] = False
 
 
 
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
 
    # The version should never be changed without testing.      #
 
    # If you're just getting data, no harm will be done. But if  #
 
    # you Add/Delete/Update anything, then all bets are off!    #
 
    # Anything requiring an HTTP POST is potentially dangerous.  #
 
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
 
 
 
    global ServerVersion
 
    version = '0.28'
 
  
 
     ##############################################################
 
     ##############################################################
     # If there's no endpoint leave now; otherwise, form the URL. #
+
     # Request the upcoming list from the backend and check the  #
 +
    # response ...                                               #
 
     ##############################################################
 
     ##############################################################
  
     if endpoint == '':
+
     endpoint = 'Dvr/GetUpcomingList'
        sys.exit('\nAbort! No endpoint (e.g. Myth/GetHostName.)')
 
  
     if rest == '':  qmark = ''
+
     if args.showall:
     else:           qmark = '?'
+
        rest = 'ShowAll=true'
 +
     else:
 +
        rest = ''
  
     url='http://{}:{}/{}{}{}'.format(host, port, endpoint, qmark, rest)
+
     backend = api.Send(host=args.host)
  
     ##############################################################
+
     try:
    # Form the request to the API. Unless specified, an HTTP GET #
+
         resp_dict = backend.send(endpoint=endpoint, rest=rest, opts=opts)
    # will be used, change it to a POST if asked. The debugging  #
+
     except RuntimeError as error:
    # provides the URL used and can be copied and tested with a  #
+
         sys.exit('\nFatal error: "{}"'.format(error))
    # browser or curl/wget if needed. Unless specified, requests #
 
    # will get a JSON response from the server. The User-Agent  #
 
    # header will show up in server logs with the value of the  #
 
    # version set earlier included. And finally, responses will  #
 
    # be gzipped by the server unless the nogzip option is True. #
 
    ##############################################################
 
 
 
    request = urllib2.Request(url)
 
    request.get_method = lambda: method
 
 
 
    if opts['debug']:
 
         print '\nDebug: URL = {} [{}]'.format(url, method)
 
 
 
    if method == 'POST' and not opts['wrmi']:
 
        if opts['etree']:
 
            tree = ET.ElementTree(
 
                    ET.fromstring('<Warning>wrmi=False</Warning>'))
 
            return tree.getroot()
 
        else:
 
            return { "Warning": "wrmi=False" }
 
 
 
    request = urllib2.Request(url)
 
 
 
     if not opts['etree']:
 
         request.add_header('Accept', 'application/json')
 
 
 
    request.add_header('User-Agent', '{} Python Services API'.format(version))
 
 
 
    if not opts['nogzip']:
 
        request.add_header('Accept-Encoding', 'gzip,deflate')
 
  
 
     ##############################################################
 
     ##############################################################
     # Actually try to get the data and handle errors. Get the    #
+
     # ... and also set the module's timezone offset so that the #
    # Server: header and see if the MythTV version is recognized.#
+
     # start times in the response can be converted to local time.#
    # Contents of server for 0.27 and 0.28 as of this writing:  #
 
    # MythTV/0.28-pre-3094-g349d3a4 Linux/3.13.0-66-generic UPnP/1.0
 
    # Linux 3.13.0-65-generic, UPnP/1.0, MythTV 0.27.20150622-1 #
 
     # Then decompress the gzipped response if need be. Finally,  #
 
    # load the data into a dictionary (if JSON) or an Element    #
 
    # Tree (if XML.)                                            #
 
 
     ##############################################################
 
     ##############################################################
  
     try:
+
     util.get_utc_offset(backend=backend, opts=opts)
        response = urllib2.urlopen(request)
 
    except urllib2.HTTPError, e:
 
        sys.exit('\nHTTP Error: {}. URL was:\n\t{}'.format(e.code, url))
 
    except urllib2.URLError, e:
 
        sys.exit('\nError: {}. URL was:\n\t{}'.format(e.args, url))
 
    except httplib.UnknownProtocol, e:
 
        sys.exit('Unknown Protocol: {}'.format(e.args))
 
    except:
 
        sys.exit('\nUndefined error. Is the backend running? URL was:\n\t{}.' \
 
            .format(url))
 
  
     server = response.info().get('Server')
+
     count = int(resp_dict['ProgramList']['TotalAvailable'])
 +
    programs = resp_dict['ProgramList']['Programs']
  
     if server == None:
+
     if args.debug:
        sys.exit('\nWarning: No HTTP "Server:" header returned.')
+
         print('Debug: Upcoming recording count = {}'.format(count))
    else:
 
         if re.search(version, server):
 
            ServerVersion = '0.28'
 
        elif re.search('0.27', server):
 
            ServerVersion = '0.27'
 
            print '\nWarning: {} Services API module may not work with {}.' \
 
                .format(version, ServerVersion)
 
        else:
 
            sys.exit('Abort: Module only tested on 0.27 and 0.28, not: {}.' \
 
                .format(server))
 
 
 
    if response.info().get('Content-Encoding') == 'gzip':
 
        response_string = gzip.GzipFile(fileobj = StringIO(response.read()))
 
        unzipped_response = response_string.read()
 
        if opts['debug']:
 
            print 'Debug: Received bytes = {}, Unzipped = {}' \
 
                .format(int(response.info().getheaders("Content-Length")[0]),
 
                    len(unzipped_response))
 
 
 
        if opts['etree']:
 
            return ET.fromstring(unzipped_response)
 
        else:
 
            return json.loads(unzipped_response)
 
    else:
 
        if opts['etree']:
 
            return ET.fromstring(request)
 
        else:
 
            return json.load(response)
 
  
def URLEncode(value=''):
+
    if count < 1:
    """
+
        print('\nNo upcoming recordings found.\n')
    Input:  A string. E.g a program's title or anything that has
+
        sys.exit(0)
            special characters like ?, & and special UTF characters.
 
  
     Output: The URL encoded string. E.g. ó becomes: %C3%B3 or ?
+
     print('\nPrinting {} day{} of upcoming programs sorted by StartTime'
            becomes %3F.
+
          .format(args.days, 's'[args.days == 1:]))
    """
+
    print(u'\n  {}{:16}  {:40.40}  {:25.25}  {}  {}'
 +
          .format(YELLOW, 'StartTime', 'Title', 'SubTitle',
 +
                  'Flags', WHITE).encode('utf-8'))
  
     if value == '':
+
     matched_upcoming = 0
        print 'Warning: MythTVServicesAPI.URLEncode called without any value'
 
        return value
 
  
     return '{}'.format(urllib2.quote(value).encode('utf-8'))
+
     for program in programs:
  
def CreateFindTime(host='', port='', time='', opts={}):
+
        retval = print_details(program, args)
    """
 
    Normally be used to take a timestamp and convert it for use in adding
 
    new recordings. This always gets the UTCOffset, which the caller should
 
    probably do once and pass it to this function. But, since it's called
 
    infrequently, just do it here every time.
 
  
    Input:  Full UTC timestamp, e.g. 2014-08-12T22:00:00 (with or without
+
        if retval == -1:
             the trailing 'Z'.)
+
             break
  
    Output: Time portion of the above in local time.
+
        matched_upcoming += retval
    """
 
  
     if time == '':
+
     print('\n  Total Upcoming Programs: {}'.format(matched_upcoming))
        print 'Warning: MythTVServicesAPI.CreateFindTime called without any time'
 
        return None
 
  
    time = time.replace('Z', '')
 
  
    opts['etree'] = False
+
if __name__ == '__main__':
 +
    main()
  
    resp_dict = Send(host=host, port=6544, endpoint='Myth/GetTimeZone', \
+
# vim: set expandtab tabstop=4 shiftwidth=4 smartindent colorcolumn=80:
                            opts=opts)
 
 
 
    tzoffset = resp_dict['TimeZoneInfo']['UTCOffset']
 
    tzoffset = int(tzoffset)
 
 
 
    dt = datetime.strptime(time, '%Y-%m-%dT%H:%M:%S')
 
 
 
    return (dt + timedelta(seconds = tzoffset)).strftime('%H:%M:%S')
 
 
 
def UTCToLocal(utctime='', tzoffset='', omityear=False):
 
    """
 
    Does exactly that conversion. This is likely to be called frequently
 
    (for example if printing guide timestamps) so force the caller to get
 
    the TZ offset and pass it.
 
 
 
    Inputs:  utctime  = Full UTC timestamp, e.g. 2014-08-12T22:00:00[Z].
 
            tzoffset = Offset from UTC. Probably from Myth/GetTimeZone.
 
            omityear = If True, then drop the 4 digit year and following -.
 
 
 
    Output: Local time, also a string. Possibly without the year- and always
 
            without the T between the data/time and trailing Z.
 
    """
 
 
 
    if utctime == '' or tzoffset == '':
 
        sys.exit('Abort, utctime and/or tzoffset are empty!.')
 
 
 
    utctime = utctime.replace('Z', '').replace('T', ' ')
 
 
 
    dt = datetime.strptime(utctime, '%Y-%m-%d %H:%M:%S')
 
 
 
    if omityear: fromstring = '%m-%d %H:%M:%S'
 
    else:        fromstring = '%Y-%m-%d %H:%M:%S'
 
 
 
    return (dt + timedelta(seconds = tzoffset)).strftime(fromstring)
 
 
</pre>}}
 
</pre>}}
 +
By getting the time zone information from the backend, each start time
 +
in the output is converted to local time rather than the UTC returned
 +
by the Services API. Another note about the above is the use
 +
of ''print(u'....)'' followed by ''.encode('utf-8)''. That's critical
 +
for Titles/SubTitles for example where there are characters like: ó.
 +
In Python3, this isn't requried.
  
[[Category:Python Scripts]]
 
[[Category:Developer Documentation]]
 
 
[[Category:Services API]]
 
[[Category:Services API]]

Latest revision as of 18:14, 2 February 2020


Existing sources of information

  • Local backend: e.g. http://<backendHostNameOrIP>:6544/Channel/wsdl
  • The 'old' protocol version. Use of this mechanism is discouraged but its pages are a still a good source for (say) program data. Myth Protocol
  • The the MythTV forum has a Services API section – fast and helpful responses!
  • Source code (if desperate!): For example, the Dvr service

Getting information from the backend

The backend can be read using a standard web interface and there are a number of ways of doing this..

Web browser

In its simplest form, a browser will show the data e.g.

http:/<backendHostNameOrIP>:6544/Dvr/GetRecordedList?StartIndex=2&Count=1&Descending=true

will show a single entry from the recorded list.

This is a useful development aid but note that the browser helpfully inserts newlines or tabs for readability but these will not be present if the same page is read with perl code.

Notice that empty values like <XMLTVID></XMLTVID> will display as: <XMLTV/>.

curl

From the shell, curl can do the same, e.g.

curl <backendHostNameOrIP>:6544/Dvr/GetRecordedList?StartIndex=2\&Count=1\&Descending=true | sed "s/></>\n</g"

Note the \ to prevent the shell from spinning off the command at the &. The sed inserts newlines for readability.


Python Services API Cookbook

Important.png Note: Starting with v31, Python version 3 is supported. Adjust references to the host's Python version below as required.


What follows are some working Python programs that use the MythTV Services API. If the user's language of choice is Perl, then see: Perl API examples

Unlike Python Bindings, only basic access to the Services API is provided here. Users will need to retrieve data from the Services/endpoints of interest to them. N.B. not everything available in Python Bindings' is available in the API.

There are excellent links that list the available services and endpoints. See these: API parameters 30 and API parameters 31.

The examples depend on a module which is available in v30 and above. Users of earlier versions can install the source themselves. See: https://code.mythtv.org/cgit/mythtv/tree/mythtv/bindings/python/MythTV/services_api

This page was written and tested using 0.28-pre under python 2.7 and 3.4.

About the Module

First, it's not necessary to use it! Users can write their own code to send information to the back/frontend. The module provides a reasonable template of things that should be done. Included are checking of the arguments, some optional debugging statements, optional header choices and error handling.

The module formats a JSON response from the server into a Python dict.

Installing and importing the module

This is for users running versions below v30.

In v30+, the module is part of the Python bindings and can be imported like this from MythTV.services_api import send as api.

Where the module is installed is up to the user. The recommendation is to choose one of the following. But first, decide which host(s) the module is to be installed on. It could be a backend, but that isn't necessary. Install it on any host(s) that want to access the Services API

Install in the user's bin directory (easiest)

If the module is installed in the same directory (typically: ~/bin) as the program(s) that use it, then include a line like this in those programs:

import services_api as api

The as api is optional, but convenient. All examples in this page will assume this method.

Install elsewhere and set PYTHONPATH

Put the module somewhere other than in the existing $PATH where programs using it are located. Then set PYTHONPATH to point to that directory. For example, the following line could be added to a .profile or .bashrc file:

export PYTHONPATH=/home/services_api

Import it as in the above.

Install in a dist-packages directory (hardest/preferred)

Create a new directory below the existing MythTV directory that might be found in directories like these:

/usr/local/lib/python2.7/dist-packages/
/usr/lib/python2.7/dist-packages/
/usr/lib/python3.4/dist-packages
/usr/local/lib/python3.4/dist-packages

Create a new directory named services_api and put the __init__.py, send.py, utilities.py and _version.py file under it. Savy users will use a Makefile and setup.py program to do this (and create .pyc files etc.)

MacPorts users might find the bindings under: /opt/dvr/bin/python2.7.

Importing the module would then be done like this:

from MythTV.services_api import send as api

Displaying the module's help information

As with any proper Python package, built-in help is available. Try the following:

$ python3 # or python2
>>> from MythTV.services_api import send as api

Followed by:

>>> help(api)
>>> control-D # After the help prints

Examples

Once the module is installed, the following examples will work.

Notice that in the following, localhost is used for the host argument to Send(). That means these will be run on a MythTV backend. But that isn't required. The module and examples can be put on any host that has access to the backend and run from there. Just replace host= 'localhost' with host= <backendHostNameOrIpAddress>.

Myth/GetHostName endpoint example

Getting the backend's hostname returns the simplest response in the API. It's the easiest to parse and has the hostname of the backend (really, it's the profile name used in the DB and honors the LocalHostName line in config.xml, but that's another Wiki.) It's a great place to start learning about the API.

Put the following in a file, perhaps simple_api_test.py. Make it executable and run it. Adjust the #! line as required. For example, MacPorts users might replace the 1st line below with #!/opt/dvr/bin/python2.7


PythonIcon.png simple_api_test.py
#!/usr/bin/python3
from MythTV.services_api import send as api

BACKEND = api.Send(host='localhost')
resultDict = BACKEND.send(endpoint='Myth/GetHostName')

print 'Entire response:', resultDict
print 'Backend hostname (MythTV profile name) is: ', resultDict['String']

As mentioned above as api is optional. Calling send.Send(...) is also valid. There's nothing special about resultDict other than it reminds the user that the results are being returned in a Python dict format.

Look at the two lines of output. The 1st is the complete response, a simple dictionary with one element, the key String contains the value someHostName. This is important as later examples will be more complex dictionaries.

Entire response: {u'String': u'backendHostName'}
Backend hostname (MythTV profile name) is: someBackendHostName

The Myth/GetTimeZone endpoint example

A slightly more complex example:


PythonIcon.png get_timezone_info.py
#!/usr/bin/python3
from MythTV.services_api import send as api

BACKEND = api.Send(host='localhost')
r = BACKEND.send(endpoint='Myth/GetTimeZone')

print r
print r['TimeZoneInfo']
print r['TimeZoneInfo']['TimeZoneID']
print r['TimeZoneInfo']['UTCOffset']
print r['TimeZoneInfo']['CurrentDateTime']

The GetTimeZone JSON response contains a dictionary (TimeZoneInfo) with dictionary inside it that has three keys. Expect to see something like this:

{u'TimeZoneInfo': {u'TimeZoneID': u'America/Chicago', u'UTCOffset': u'-21600', u'CurrentDateTime': u'2015-11-13T19:26:18Z'}}
{u'TimeZoneID': u'America/Chicago', u'UTCOffset': u'-21600', u'CurrentDateTime': u'2015-11-13T19:26:18Z'}}
America/Chicago
-21600
2015-11-13T19:26:18Z

Myth/TestDBSettings (POST Example)

Take note that some endpoints require an HTTP POST (as opposed to the default GET.) This is detailed in the Wikis for each service. Anything that changes data must use a POST and the following is another example.

Try the following. Remember, backend_host and db_host are frequently the same, but can be different.

In opts, wrmi (We Really Mean It) must be True. Try setting it to False and see the result (it's good for debugging before an endpoint that changes data is used.)

The expected response will end with: {"bool": "true"}.

PythonIcon.png simple_post.py
#!/usr/bin/python3

import os.path as path
import sys
from MythTV.services_api import send as api

try:
    BEHOST = sys.argv[1]
    DBHOST = sys.argv[2]
    DBUSER = sys.argv[3]
    DBPSWD = sys.argv[4]
except IndexError:
    sys.exit('\nUsage: {} backend_host db_host db_user db_password'
             .format(path.basename(sys.argv[0])))

POSTDATA = {'HostName': DBHOST, 'UserName': DBUSER, 'Password': DBPSWD}

BACKEND = api.Send(host=BEHOST)
r = BACKEND.send(endpoint='Myth/TestDBSettings',
             postdata=POSTDATA, opts={'debug': True, 'wrmi': True})

print r

Working program using the Dvr/GetUpcomingList endpoint

Here's a simple program, with a bit of formatting, that does a practical task. Refer to the tools in the beginning of this page to see the XML output of this endpoint. Viewing it with a browser may make the dicts easier to understand.

Put the following in a file, like: short_print_upcoming.py and type: short_print_upcoming.py backendHostNameOrIPaddress

Note the line: progs = resp_dict['ProgramList']['Programs'] below. Rather than referencing each upcoming program as: resp_dict['ProgramList']['Programs'][upcoming], they can be examined as: progs[upcoming].... upcoming here is the index into a list contained in the Programs key.

Also note that there's yet another dict under progs, it's the ['Recording'] dict where only the StartTs is used in this example.

PythonIcon.png short_print_upcoming.py
#!/usr/bin/python3
# -*- coding: utf-8 -*-                                                         
                                                                                
"""Print Start Times, Titles and SubTitles"""                                   
                                                                                
from __future__ import print_function                                           
import sys                                                                      
from MythTV.services_api import send as api                                     
                                                                                
                                                                                
def main():                                                                     
    """Check host, request upcomings and print"""                               
                                                                                
    try:                                                                        
        host = sys.argv[1]                                                      
    except IndexError:                                                          
        host = 'localhost'                                                      
                                                                                
    backend = api.Send(host=host)                                               
                                                                                
    try:                                                                        
        resp_dict = backend.send(endpoint='Dvr/GetUpcomingList')                
    except RuntimeError as error:                                               
        sys.exit('\nFatal error: "{}"'.format(error))                           
                                                                                
    count = int(resp_dict['ProgramList']['Count'])                              
    progs = resp_dict['ProgramList']['Programs']                                
                                                                                
    if count < 1:                                                               
        sys.exit('\nNo upcoming recordings found.\n')                           
                                                                                
    for upcoming in range(count):                                               
        print('  {}  {:45.45}  {:15.15}'.format(                                
            progs[upcoming]['Recording']['StartTs'],                            
            progs[upcoming]['Title'],                                           
            progs[upcoming]['SubTitle']))                                       
                                                                                
                                                                                
if __name__ == '__main__':                                                      
    main()                                                                      
                                                                                
# vim: set expandtab tabstop=4 shiftwidth=4 smartindent noai colorcolumn=80:

To see the way the response is returned, try adding lines like these to the end of the program:

print resp_dict
print resp_dict['ProgramList']['Programs'][0]
print progs[0]

The 1st displays the entire response in a dictionary, the 2nd just the 0th program returned and the last is the same as the 2nd.

Full example using Dvr/GetUpcomingList

Using the ideas above, the following is a complete program with command line arguments used to set some of the opts values, and set a variable host etc. Type: ./print_upcoming.py --help for details. Try the --debug argument to see the URLs generated by the program.

The concepts here can serve as a guide to creating your own Python programs.

PythonIcon.png print_upcoming.py
!/usr/bin/python3
# -*- coding: utf-8 -*-

'''
Print Start Times, Titles and SubTitles.

Optionally filter by Titles/ChanId or limited by days. Optionally print
the cast members for each recording. This is an 'overly-commented', and
simple example of using the Services API to do a task. It also forms a
template for working with the API.

Good Start:          print_upcoming.py --help
Watch what it sends: print_upcoming.py --host=Name/IP --debug
'''

from __future__ import print_function
from datetime import datetime
import argparse
import logging
import re
import sys
import time
from MythTV.services_api import send as api
from MythTV.services_api import utilities as util

DAY_IN_SECONDS = 24 * 60 * 60
TIME_NOW = time.mktime(datetime.now().timetuple())
WHITE = '\033[0m'
YELLOW = '\033[93m'


def flags_to_strings(flgs):
    '''
    Convert the decimal flags to a printable string. From:
    libs/libmyth/programtypes.h. The </> print for values that
    haven't been defined here yet (and maybe should be.)
    '''
    strlst = list()
    if flgs & (0x00fff):
        strlst.append('<')
    if flgs & (1 << 12):
        strlst.append('Rerun')
    if flgs & (1 << 13):
        strlst.append('Dup')
    if flgs & (1 << 14):
        strlst.append('React')
    if flgs & (0xf8000):
        strlst.append('>')
    return ', '.join(strlst)


def process_command_line():
    '''
    Add command arguments, as required, here. --host is likely
    a minimum and mandatory unless it has a default value below.
    Examples include string, integer and bool type arguments.
    Printing the default values is also shown.
    '''

    parser = argparse.ArgumentParser(description='Print Upcoming Programs',
                                     epilog='Default values are in ()s')

    parser.add_argument('--cast', action='store_true',
                        help='include cast member names (%(default)s)')

    parser.add_argument('--chanid', type=int, required=False,
                        metavar='<chanid>',
                        help='filter on MythTV chanid, e.g. 1091 (none).')

    parser.add_argument('--days', type=int, required=False, metavar='<days>',
                        default=7,
                        help='days of programs to print (%(default)s)')

    parser.add_argument('--debug', action='store_true',
                        help='turn on debug messages (%(default)s)')

    parser.add_argument('--digest', type=str, metavar='<user:pass>',
                        help='digest username:password')

    parser.add_argument('--host', type=str, required=False,
                        default='localhost',
                        metavar='<hostname>',
                        help='backend hostname (%(default)s)')

    parser.add_argument('--port', type=int, default=6544, metavar='<port>',
                        help='port number of the Services API (%(default)s)')

    parser.add_argument('--showall', action='store_true',
                        help='include conflicts etc. (%(default)s)')

    parser.add_argument('--title', type=str, default='', required=False,
                        metavar='<title>', help='filter by title (none)')

    parser.add_argument('--version', action='version', version='%(prog)s 0.10')

    return parser.parse_args()


def print_details(program, args):
    '''
    Print a single program's information. Apply the --title and --chanid
    filters to select limited sets of recordings. Exit False if the
    --days limit is reached.
    '''

    title = program['Title']
    subtitle = program['SubTitle']
    flags = int(program['ProgramFlags'])
    chanid = int(program['Channel']['ChanId'])
    startts = util.utc_to_local(program['Recording']['StartTs'])
    # status = int(program['Recording']['Status'])
    start_time = time.mktime(datetime.strptime(startts, "%Y-%m-%d %H:%M")
                             .timetuple())

    if int((start_time - TIME_NOW) / DAY_IN_SECONDS) >= args.days:
        return -1

    if (args.title is None or re.search(args.title, title, re.IGNORECASE)) \
            and (args.chanid is None or args.chanid == chanid):

        print(u'  {:16.16}  {:40.40}  {:25.25}  {}'
              .format(startts, title, subtitle,
                      flags_to_strings(flags)).encode('utf-8'))

        if args.cast:
            cast = program['Cast']['CastMembers']
            for i in range(100):
                try:
                    print(u'\t{} ({})'.format(cast[i]['Name'],
                                              cast[i]['TranslatedRole'])
                          .encode('utf-8'))
                except IndexError:
                    break

        return 1

    return 0


def main():
    '''
    Get the upcoming recordings from the backend and process them
    based on the filters and number of days of interest.
    '''

    args = process_command_line()
    opts = dict()

    logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
    logging.getLogger('requests.packages.urllib3').setLevel(logging.WARNING)
    logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING)

    try:
        opts['user'], opts['pass'] = args.digest.split(':')
    except (AttributeError, ValueError):
        pass

    ##############################################################
    # Request the upcoming list from the backend and check the   #
    # response ...                                               #
    ##############################################################

    endpoint = 'Dvr/GetUpcomingList'

    if args.showall:
        rest = 'ShowAll=true'
    else:
        rest = ''

    backend = api.Send(host=args.host)

    try:
        resp_dict = backend.send(endpoint=endpoint, rest=rest, opts=opts)
    except RuntimeError as error:
        sys.exit('\nFatal error: "{}"'.format(error))

    ##############################################################
    # ... and also set the module's timezone offset so that the  #
    # start times in the response can be converted to local time.#
    ##############################################################

    util.get_utc_offset(backend=backend, opts=opts)

    count = int(resp_dict['ProgramList']['TotalAvailable'])
    programs = resp_dict['ProgramList']['Programs']

    if args.debug:
        print('Debug: Upcoming recording count = {}'.format(count))

    if count < 1:
        print('\nNo upcoming recordings found.\n')
        sys.exit(0)

    print('\nPrinting {} day{} of upcoming programs sorted by StartTime'
          .format(args.days, 's'[args.days == 1:]))
    print(u'\n  {}{:16}  {:40.40}  {:25.25}  {}  {}'
          .format(YELLOW, 'StartTime', 'Title', 'SubTitle',
                  'Flags', WHITE).encode('utf-8'))

    matched_upcoming = 0

    for program in programs:

        retval = print_details(program, args)

        if retval == -1:
            break

        matched_upcoming += retval

    print('\n  Total Upcoming Programs: {}'.format(matched_upcoming))


if __name__ == '__main__':
    main()

# vim: set expandtab tabstop=4 shiftwidth=4 smartindent colorcolumn=80:

By getting the time zone information from the backend, each start time in the output is converted to local time rather than the UTC returned by the Services API. Another note about the above is the use of print(u'....) followed by .encode('utf-8). That's critical for Titles/SubTitles for example where there are characters like: ó. In Python3, this isn't requried.