Difference between revisions of "Python API Examples"

From MythTV Official Wiki
Jump to: navigation, search
(Update Utilities.py and printUpcoming.py including authentication)
(Switch shebangs to /usr/bin/python3)
 
(8 intermediate revisions by the same user not shown)
Line 1: Line 1:
 
{{Basic Services API Access}}
 
{{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
 
What follows are some working ''Python'' programs that use
 
the MythTV [[Services API]]. If the user's language of
 
the MythTV [[Services API]]. If the user's language of
Line 12: Line 13:
  
 
There are excellent links that list the available services
 
There are excellent links that list the available services
and endpoints. See these: [[API parameters 0.27]] and [[API parameters 0.28]].
+
and endpoints. See these: [[API parameters 30]] and [[API parameters 31]].
  
The examples depend on a module (included at the bottom of
+
The examples depend on a module which is available in v30 and above.
this page.)
+
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.
 
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
Line 27: Line 29:
 
The module formats a ''JSON'' response from the server into a ''Python'' ''dict''.
 
The module formats a ''JSON'' response from the server into a ''Python'' ''dict''.
 
=== Installing  and ''import''ing the module ===
 
=== Installing  and ''import''ing the module ===
 +
<B>This is for users running versions below v30.</B>
 +
 +
In v30+, the module is part of the Python bindings and can be
 +
imported like this <code>from MythTV.services_api import send as api</code>.
 +
 
Where the module is installed is up to the user. The
 
Where the module is installed is up to the user. The
 
recommendation is to choose ''one'' of the following.
 
recommendation is to choose ''one'' of the following.
Line 36: Line 43:
 
use it, then include a line like this in those programs:
 
use it, then include a line like this in those programs:
 
<pre>
 
<pre>
import Utilities as api
+
import services_api as api
 
</pre>
 
</pre>
 
The ''as api'' is optional, but convenient.
 
The ''as api'' is optional, but convenient.
Line 43: Line 50:
  
 
====Install elsewhere and set PYTHONPATH====
 
====Install elsewhere and set PYTHONPATH====
Put the module somewhere other than something in the
+
Put the module somewhere other than in the
 
existing $PATH where programs using it are located.
 
existing $PATH where programs using it are located.
 
Then set ''PYTHONPATH'' to point to that directory.
 
Then set ''PYTHONPATH'' to point to that directory.
Line 49: Line 56:
 
a ''.profile'' or ''.bashrc'' file:
 
a ''.profile'' or ''.bashrc'' file:
 
<pre>
 
<pre>
export PYTHONPATH=/home/bin/python
+
export PYTHONPATH=/home/services_api
 
</pre>
 
</pre>
 
Import it as in the above.
 
Import it as in the above.
====Install in a ''dist-packages'' directory (hardest)====
+
====Install in a ''dist-packages'' directory (hardest/preferred)====
Create a new directory '''next to''' the existing
+
Create a new directory '''below''' the existing
 
''MythTV'' directory that might be found in directories
 
''MythTV'' directory that might be found in directories
 
like these:
 
like these:
Line 62: Line 69:
 
/usr/local/lib/python3.4/dist-packages
 
/usr/local/lib/python3.4/dist-packages
 
</pre>
 
</pre>
Create a new directory named (for example) ''MythTVServicesAPI''
+
Create a new directory named ''services_api''
and put the ''Utilities.py'' and an empty ''__init__.py''
+
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
 
file under it. Savy users will use a ''Makefile'' and ''setup.py'' program to do
 
this (and create ''.pyc'' files etc.)
 
this (and create ''.pyc'' files etc.)
Line 72: Line 79:
 
Importing the module would then be done like this:
 
Importing the module would then be done like this:
 
<pre>
 
<pre>
from MythTVServicesAPI import Utilities as api
+
from MythTV.services_api import send as api
 
</pre>
 
</pre>
  
 
===Displaying the module's help information===
 
===Displaying the module's help information===
As with any proper ''Python'' module, built-in help
+
As with any proper ''Python'' package, built-in help
 
is available. Try the following:
 
is available. Try the following:
 
<pre>
 
<pre>
$ python
+
$ python3 # or python2
>>> import Utilities as util
+
>>> from MythTV.services_api import send as api
</pre>
 
Or:
 
<pre>
 
$ python
 
>>> from MythTVServicesAPI import Utilities as util
 
 
</pre>
 
</pre>
 
Followed by:
 
Followed by:
 
<pre>
 
<pre>
>>> help(util)
+
>>> help(api)
 
>>> control-D # After the help prints
 
>>> control-D # After the help prints
 
</pre>
 
</pre>
 +
 
==Examples==
 
==Examples==
 
Once the module is installed, the following examples will work.
 
Once the module is installed, the following examples will work.
Line 98: Line 101:
 
the ''host'' argument to ''Send()''. That means these will be run on
 
the ''host'' argument to ''Send()''. That means these will be run on
 
a ''MythTV'' backend. But that isn't required. The
 
a ''MythTV'' backend. But that isn't required. The
module and examples can  be  put on any client that has
+
module and examples can  be  put on any host that has
 
access to the backend and run from there. Just replace
 
access to the backend and run from there. Just replace
''host='' 'localhost' with ''host='' 'backendHostNameOrIpAddress'.
+
''host='' 'localhost' with ''host='' <backendHostNameOrIpAddress>.
 
===''Myth/GetHostName'' endpoint example===
 
===''Myth/GetHostName'' endpoint example===
 
Getting the backend's hostname returns the simplest response
 
Getting the backend's hostname returns the simplest response
Line 109: Line 112:
 
API.
 
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. Adjust the ''#!'' line
 
Make it executable and run it. Adjust the ''#!'' line
 
as required. For example, ''MacPorts'' users might replace the 1st
 
as required. For example, ''MacPorts'' users might replace the 1st
 
line below with ''#!/opt/dvr/bin/python2.7''
 
line below with ''#!/opt/dvr/bin/python2.7''
  
{{Python|simpleAPItest.py|<pre>
+
{{Python|simple_api_test.py|<pre>
#!/usr/bin/env python
+
#!/usr/bin/python3
import Utilities as api
+
from MythTV.services_api import send as api
  
resultDict = api.Send(host='localhost', endpoint='Myth/GetHostName')
+
BACKEND = api.Send(host='localhost')
 +
resultDict = BACKEND.send(endpoint='Myth/GetHostName')
  
 
print 'Entire response:', resultDict
 
print 'Entire response:', resultDict
Line 124: Line 128:
 
</pre>}}
 
</pre>}}
  
As mentioned above ''as api'' is optional. Calling ''Utilities.Send(...)''
+
As mentioned above ''as api'' is optional. Calling ''send.Send(...)''
 
is also valid. There's nothing special about ''resultDict'' other than
 
is also valid. There's nothing special about ''resultDict'' other than
 
it reminds the user that the results are being returned in a ''Python''
 
it reminds the user that the results are being returned in a ''Python''
Line 135: Line 139:
 
<pre>
 
<pre>
 
Entire response: {u'String': u'backendHostName'}
 
Entire response: {u'String': u'backendHostName'}
Backend hostname (MythTV profile name) is: aBackendHostName
+
Backend hostname (MythTV profile name) is: someBackendHostName
 
</pre>
 
</pre>
  
Line 141: Line 145:
 
A slightly more complex example:
 
A slightly more complex example:
  
{{Python|getTimeZoneInfo.py|<pre>
+
{{Python|get_timezone_info.py|<pre>
#!/usr/bin/env python
+
#!/usr/bin/python3
import Utilities as api
+
from MythTV.services_api import send as api
  
r = api.Send(host='backendHostName', endpoint='Myth/GetTimeZone')
+
BACKEND = api.Send(host='localhost')
 +
r = BACKEND.send(endpoint='Myth/GetTimeZone')
  
 
print r
 
print r
Line 162: Line 167:
 
2015-11-13T19:26:18Z
 
2015-11-13T19:26:18Z
 
</pre>
 
</pre>
 +
===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"}''.
 +
{{Python|simple_post.py|<pre>
 +
#!/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
 +
</pre>}}
 +
 
===Working program using the ''Dvr/GetUpcomingList'' endpoint===
 
===Working program using the ''Dvr/GetUpcomingList'' endpoint===
Here's a simple script, with a bit of formatting, that
+
Here's a simple program, with a bit of formatting, that
 
does a practical task. Refer to the tools in the beginning of
 
does a practical task. Refer to the tools in the beginning of
 
this page to see the ''XML'' output of this endpoint. Viewing it
 
this page to see the ''XML'' output of this endpoint. Viewing it
 
with a browser may make the ''dict''s easier to understand.
 
with a browser may make the ''dict''s easier to understand.
  
Put the following in a file, like: ''shortPrintUpcoming.py'' and
+
Put the following in a file, like: ''short_print_upcoming.py'' and
type: ''./shortPrintUpcoming.py backendHostNameOrIPaddress''
+
type: ''short_print_upcoming.py backendHostNameOrIPaddress''
  
 
Note the line: ''progs = resp_dict['ProgramList']['Programs']''
 
Note the line: ''progs = resp_dict['ProgramList']['Programs']''
Line 181: Line 224:
 
the ''['Recording']'' ''dict'' where only the ''StartTs'' is
 
the ''['Recording']'' ''dict'' where only the ''StartTs'' is
 
used in this example.
 
used in this example.
{{Python|shortPrintUpcoming.py|<pre>
+
{{Python|short_print_upcoming.py|<pre>
#!/usr/bin/env python
+
#!/usr/bin/python3
# -*- coding: utf-8 -*-
+
# -*- coding: utf-8 -*-                                                        
 
+
                                                                               
# shortPrintUpcoming.py: Print Start Times, Titles and SubTitles
+
"""Print Start Times, Titles and SubTitles"""                                 
 
+
                                                                               
import os.path as path, sys, Utilities as API
+
from __future__ import print_function                                         
 
+
import sys                                                                     
try:
+
from MythTV.services_api import send as api                                   
    Host = sys.argv[1]
+
                                                                               
except:
+
                                                                               
    sys.exit( '\nUsage: {} HostnameOrIP'.format(path.basename(sys.argv[0])))
+
def main():                                                                   
 
+
    """Check host, request upcomings and print"""                             
resp_dict = API.Send(host=Host, endpoint='Dvr/GetUpcomingList')
+
                                                                               
 
+
    try:                                                                      
if list(resp_dict.keys())[0] in ['Abort', 'Warning']:
+
        host = sys.argv[1]                                                    
    sys.exit('\n{}\n'.format(list(resp_dict.values())[0]))
+
    except IndexError:                                                        
 
+
        host = 'localhost'                                                    
count = int(resp_dict['ProgramList']['Count'])
+
                                                                               
progs = resp_dict['ProgramList']['Programs']
+
    backend = api.Send(host=host)                                              
 
+
                                                                               
if count < 1: sys.exit('\nNo upcoming recordings found.\n')
+
    try:                                                                       
 
+
        resp_dict = backend.send(endpoint='Dvr/GetUpcomingList')              
for upcoming in range(count):
+
    except RuntimeError as error:                                              
    print(u'  {}  {:45.45}  {:15.15}'.format(
+
        sys.exit('\nFatal error: "{}"'.format(error))                          
        progs[upcoming]['Recording']['StartTs'],
+
                                                                               
        progs[upcoming]['Title'],
+
    count = int(resp_dict['ProgramList']['Count'])                            
        progs[upcoming]['SubTitle']).encode('utf-8'))
+
    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>}}
  
Line 225: Line 283:
 
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.
 
# Also 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:          printUpcoming.py --help
 
# Watch what it sends: printUpcoming.py --host=Name/IP --debug
 
#
 
  
##############################################################
+
Optionally filter by Titles/ChanId or limited by days. Optionally print
# MythTVServicesAPI.Utilities handles backend connections    #
+
the cast members for each recording. This is an 'overly-commented', and
# re is used to allow things like --title to be partial and #
+
simple example of using the Services API to do a task. It also forms a
#    case agnostic                                          #
+
template for working with the API.
# argparse is used in the following section                  #
 
##############################################################
 
  
from  datetime import datetime
+
Good Start:          print_upcoming.py --help
from  MythTVServicesAPI import Utilities as API
+
Watch what it sends: print_upcoming.py --host=Name/IP --debug
import argparse, re, sys, time
+
'''
  
##############################################################
+
from __future__ import print_function
# Convert the decimal flags to a printable string. From:    #
+
from datetime import datetime
# libs/libmyth/programtypes.h. The </> print for values that #
+
import argparse
# haven't been defined here yet (and maybe should be.)      #
+
import logging
##############################################################
+
import re
def flagsToStrings(flgs):
+
import sys
    strlst = []
+
import time
    if flgs & (0x00fff): strlst.append('<')
+
from MythTV.services_api import send as api
    if flgs & (1 << 12): strlst.append('Rerun')
+
from MythTV.services_api import utilities as util
    if flgs & (1 << 13): strlst.append('Dup')
 
    if flgs & (1 << 14): strlst.append('React')
 
    if flgs & (0xf8000): strlst.append('>')
 
    return ', '.join(strlst)
 
  
##############################################################
+
DAY_IN_SECONDS = 24 * 60 * 60
# Add command arguments, as required, here. --host is likely #
+
TIME_NOW = time.mktime(datetime.now().timetuple())
# a minimum and mandatory unless the host is hardcoded into  #
+
WHITE = '\033[0m'
# the script. Examples include string, integer and bool type #
+
YELLOW = '\033[93m'
# 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')
+
def flags_to_strings(flgs):
 
+
    '''
parser.add_argument('--cast', action='store_true',
+
    Convert the decimal flags to a printable string. From:
     help='include cast member names (%(default)s)')
+
    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)
  
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>',
+
def process_command_line():
     default=7, help='number of days of programs to print (%(default)s)')
+
    '''
 +
    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.add_argument('--debug', action='store_true',
+
    parser = argparse.ArgumentParser(description='Print Upcoming Programs',
    help='turn on debug messages (%(default)s)')
+
                                    epilog='Default values are in ()s')
  
# TODO: allow : in a password
+
    parser.add_argument('--cast', action='store_true',
parser.add_argument('--digest', type=str, metavar='<user:pass>',
+
                        help='include cast member names (%(default)s)')
    help='digest username:password')
 
  
mandatory.add_argument('--host', type=str, required=True,
+
    parser.add_argument('--chanid', type=int, required=False,
    metavar='<hostname>', help='backend hostname')
+
                        metavar='<chanid>',
 +
                        help='filter on MythTV chanid, e.g. 1091 (none).')
  
parser.add_argument('--port', type=int, default=6544, metavar='<port>',
+
    parser.add_argument('--days', type=int, required=False, metavar='<days>',
    help='port number of the Services API (%(default)s)')
+
                        default=7,
 +
                        help='days of programs to print (%(default)s)')
  
parser.add_argument('--showall', action='store_true',
+
    parser.add_argument('--debug', action='store_true',
    help='include conflicts etc. (%(default)s)')
+
                        help='turn on debug messages (%(default)s)')
  
parser.add_argument('--title', type=str, default='', required=False,
+
    parser.add_argument('--digest', type=str, metavar='<user:pass>',
    metavar='<title>', help='filter by title (none)')
+
                        help='digest username:password')
  
parser.add_argument('--version', action='version', version='%(prog)s 0.8')
+
    parser.add_argument('--host', type=str, required=False,
 +
                        default='localhost',
 +
                        metavar='<hostname>',
 +
                        help='backend hostname (%(default)s)')
  
args = parser.parse_args()
+
    parser.add_argument('--port', type=int, default=6544, metavar='<port>',
opts = {'debug':args.debug}
+
                        help='port number of the Services API (%(default)s)')
  
try:
+
     parser.add_argument('--showall', action='store_true',
     # TODO: allow : in a password
+
                        help='include conflicts etc. (%(default)s)')
    opts['user'], opts['pass'] = args.digest.split(':')
 
except (AttributeError, ValueError):
 
    pass
 
  
##############################################################
+
    parser.add_argument('--title', type=str, default='', required=False,
# Prepare the data to send to the backend via the API and    #
+
                        metavar='<title>', help='filter by title (none)')
# then do it.                                               #
 
##############################################################
 
  
endpoint = 'Dvr/GetUpcomingList'
+
    parser.add_argument('--version', action='version', version='%(prog)s 0.10')
  
if args.showall: rest = 'ShowAll=true'
+
    return parser.parse_args()
else:            rest = ''
 
  
resp_dict = API.Send(host=args.host, port=args.port, opts=opts,
 
                    endpoint=endpoint, rest=rest)
 
  
if list(resp_dict.keys())[0] in ['Abort', 'Warning']:
+
def print_details(program, args):
     sys.exit('\n{}\n'.format(list(resp_dict.values())[0]))
+
    '''
 +
    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']
# And also set the module's timezone offset so that the      #
+
    subtitle = program['SubTitle']
# start times in the response can be converted to local time.#
+
    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())
  
API.GetUTCOffset(host=args.host, port=args.port, opts=opts)
+
    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)) \
# Finally, loop through the results and select the items     #
+
            and (args.chanid is None or args.chanid == chanid):
# 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'
+
         print(u' {:16.16}  {:40.40}  {:25.25}  {}'
YELLOW        = '\033[93m'
+
              .format(startts, title, subtitle,
dayInSeconds  = 24 * 60 * 60
+
                      flags_to_strings(flags)).encode('utf-8'))
time_now      = time.mktime(datetime.now().timetuple())
 
count        = int(resp_dict['ProgramList']['TotalAvailable'])
 
progs        = resp_dict['ProgramList']['Programs']
 
totalUpcoming = 0
 
 
 
if args.debug:
 
    print('Debug: Upcoming recording count = {}'.format(count))
 
 
 
if count < 1:
 
    sys.exit('\nNo upcoming recordings found.\n')
 
 
 
print('\nPrinting {} days of upcoming programs sorted by StartTime'.format(
 
        args.days))
 
print('\n  {}{:19}  {:40.40}  {:15.15}  {:10.10} {}  {}'.format(
 
      YELLOW, 'StartTime', 'Title', 'SubTitle', 'Status', 'Flags', WHITE))
 
 
 
for upcoming in range(count):
 
 
 
    title    = progs[upcoming]['Title']
 
    subtitle  = progs[upcoming]['SubTitle']
 
    flags    = int(progs[upcoming]['ProgramFlags'])
 
    chanid    = int(progs[upcoming]['Channel']['ChanId'])
 
    startts  = API.UTCToLocal(progs[upcoming]['Recording']['StartTs'])
 
    status    = int(progs[upcoming]['Recording']['Status'])
 
    startTime = time.mktime(datetime.strptime(startts, "%Y-%m-%d %H:%M:%S")
 
                            .timetuple())
 
    daysInTheFuture = int((startTime - time_now ) / dayInSeconds)
 
 
 
    if daysInTheFuture >= args.days:
 
        break
 
 
 
    if (args.title  == '' or re.search(args.title, title, re.IGNORECASE)) and \
 
        (args.chanid == None or args.chanid == chanid):
 
 
 
        totalUpcoming = totalUpcoming + 1
 
        print(u'  {}  {:40.40}  {:15.15}  {:10.10} {}'.format(
 
              startts, title, subtitle,
 
              API.RecStatusToString(host=args.host, port=args.port, recStatus=status,opts=opts),
 
              flagsToStrings(flags)
 
              ).encode('utf-8'))
 
  
 
         if args.cast:
 
         if args.cast:
             cast = progs[upcoming]['Cast']['CastMembers']
+
             cast = program['Cast']['CastMembers']
             for a in range(100):
+
             for i in range(100):
 
                 try:
 
                 try:
                     print(u'\t{} ({})'.format (cast[a]['Name'],
+
                     print(u'\t{} ({})'.format(cast[i]['Name'],
                          cast[a]['TranslatedRole']).encode('utf-8'))
+
                                              cast[i]['TranslatedRole'])
 +
                          .encode('utf-8'))
 
                 except IndexError:
 
                 except IndexError:
 
                     break
 
                     break
  
print('\n  Total Upcoming Programs: {}'.format(totalUpcoming))
+
        return 1
 
 
# vim: set expandtab tabstop=4 shiftwidth=4 smartindent:
 
</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 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.  """
 
  
from datetime  import datetime, timedelta
+
    return 0
  
import re, sys
 
  
try:
+
def main():
     import requests
+
     '''
     from   requests.auth import HTTPDigestAuth
+
     Get the upcoming recordings from the backend and process them
except ImportError:
+
    based on the filters and number of days of interest.
     sys.exit('Debian: apt-get install python-requests or python3-requests')
+
     '''
  
try:
+
     args = process_command_line()
     from urllib import quote
+
     opts = dict()
except ImportError:
 
     from urllib.parse import quote
 
  
__version__ = '0.28'
+
    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)
  
ServerVersion = 'Set to back/frontend version after calls to Send()'
+
     try:
 
+
        opts['user'], opts['pass'] = args.digest.split(':')
global session
+
    except (AttributeError, ValueError):
global RecStatusCache
+
         pass
global RecTypeCache
 
global DupMethodCache
 
 
 
RecStatusCache = {}
 
RecTypeCache  = {}
 
DupMethodCache = {}
 
 
 
session = None
 
 
 
def Send(host='', port=6544, endpoint='', postdata={}, rest='', opts={}):
 
    """
 
 
 
    Form a URL and send it to the back/frontend. Error handling is done
 
    here too.
 
 
 
     Examples:
 
    =========
 
 
 
    import Utilities as api
 
    api.Send(host='someHostName', endpoint='Myth/GetHostName')
 
    Returns:
 
    {'String': 'someHostName'}
 
    api.Send(host='someFEName', port=6547, endpoint='Frontend/GetStatus')
 
    Returns:
 
    {'FrontendStatus': {'AudioTracks':...
 
 
 
    Input:
 
    ======
 
 
 
    host:    Must be set and is the hostname or IP of the back/frontend.
 
 
 
    port:    Only needed if the backend is using a different port (unlikely)
 
              or set to 6547 for frontend endpoints. Defaults to 6544.
 
 
 
    endpoint: Must be set. Example: Myth/GetHostName
 
 
 
    postdata: May be set if the endpoint allows it. Used when information is
 
              to be added/changed/deleted. postdata is passed as a Python dict
 
              e.g. {'ChanId':1071, ...}. Don't use if rest is used. The HTTP
 
              method will be a POST (as opposed to a GET.)
 
 
 
              If using postdata, TAKE EXTREME CAUTION!!! Use opts['wrmi']=False
 
              1st, set opts['debug']=True and watch what's sent. When happy
 
              with the data, make wrmi True.
 
 
 
              N.B. The MythTV Services API is still evolving and the wise user
 
              will backup their DB before including postdata.
 
 
 
    rest:    May be set if the endpoint allows it. For example, endpoint=
 
              Myth/GetRecordedList, rest='Count=10&StorageGroup=Sports'
 
              Don't use if postdata is used. The HTTP method will be a GET.
 
 
 
    opts      SHORT DESCRIPTION:
 
 
 
              It's OK to call this function without any options set and:
 
 
 
                  • If there's postdata, nothing will be sent to the server
 
                  • No "Debug:..." messages will print from this function
 
                  • The server response will be gzipped
 
                  • It will fail if the backend requires authorization (
 
                    user/pass would be required)
 
 
 
              DETAILS:
 
 
 
              opts is a dictionary of options that may be set in the calling
 
              program. Default values will be used if callers don't pass all
 
              or some of their own. The defaults are all False except for the
 
              user and pass as above.
 
 
 
    opts['debug']:  Set to True and some informational messages will be
 
                    printed.
 
 
 
    opts['noetag']:  Don't request the back/frontend to check for a matching
 
                    ETag. Mostly for testing.
 
 
 
    opts['nogzip']:  Don't request the back/frontend to gzip it's response.
 
                    Useful if watching protocol with a tool that doesn't
 
                    uncompress it.
 
 
 
    opts['user']:    Digest authentication.
 
    opts['pass']:
 
 
 
    opts['usexml']:  For testing only! If True, causes the backend to send its
 
                    response in XML rather than JSON. This will force an error
 
                    when parsing the response. Defaults to False.
 
 
 
    opts['wrmi']:    If True and there is postdata, the URL is actually sent
 
                    to the server.
 
 
 
                    If opts['wrmi'] is False and there is postdata, *NOTHING*
 
                    is sent to the server.
 
 
 
                    This is a failsafe that allows testing. Users can examine
 
                    what's about to be sent before doing it (wrmi = We Really
 
                    Mean It.)
 
 
 
    opts['wsdl']:    If True return WSDL from the back/frontend. Accepts no rest
 
                    or postdata. Just set endpoint, e.g. Content/wsdl
 
 
 
    Output:
 
    ======
 
 
 
    Either the response from the server in a Python dict format or an error
 
    message in a dict (currently with an 'Abort' or 'Warning' key.)
 
 
 
    Callers can handle the response like this:
 
 
 
         response = api.Send(...)
 
 
 
        if list(response.keys())[0] in ['Abort', 'Warning']:
 
            sys.exit('{}'.format(list(response.values())[0]))
 
 
 
        normal processing...
 
 
 
    However, some errors returned by the server are in XML, e.g. if an
 
    endpoint is invalid. That will cause the JSON decoder to fail. Use
 
    the debug opt to view the failing response.
 
 
 
    Whenever Send() is used, the global 'ServerVersion' is set to the value
 
    returned by the back/frontend in the HTTP Server: header. It is saved as
 
    just the version, e.g. 0.28. Callers can check it and *may* choose to
 
    adjust their code work with other versions.
 
 
 
    """
 
 
 
    global session
 
    global ServerVersion
 
 
 
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
 
    # 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.  #
 
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
 
 
 
    version = '29'
 
  
 
     ##############################################################
 
     ##############################################################
     # Set missing options to False and if debug is True, tell    #
+
     # Request the upcoming list from the backend and check the  #
     # the user if which options were changed.                   #
+
     # response ...                                               #
 
     ##############################################################
 
     ##############################################################
  
     optionList  = [ 'debug', 'noetag', 'nogzip', 'usexml', 'wrmi', 'wsdl' ]
+
     endpoint = 'Dvr/GetUpcomingList'
  
    missingList = ''
+
     if args.showall:
 
+
         rest = 'ShowAll=true'
    for option in optionList:
+
     else:
        try:
+
        rest = ''
            opts[option]
 
        except:
 
            missingList = missingList + option + ', '
 
            opts[option] = False
 
 
 
     if opts['debug'] and missingList:
 
        print('Debug: Missing opts set to False: {}'.format(missingList[:-2]))
 
 
 
    ##############################################################
 
    # Make sure there's an endpoint and optionally *either* rest #
 
    # or postdata. If so, then form the URL.                    #
 
    ##############################################################
 
 
 
    if endpoint == '':
 
         return { 'Abort':'No endpoint (e.g. Myth/GetHostName.)' }
 
 
 
    if postdata and rest:
 
        return { 'Abort':'Use either postdata or rest' }
 
 
 
    if rest == '': qmark = ''
 
     else:         qmark = '?'
 
 
 
    url='http://{}:{}/{}{}{}'.format(host, port, endpoint, qmark, rest)
 
 
 
    ##############################################################
 
    # Create a session. If postdata was supplied and wrmi wasn't #
 
    # set,then return immediately. Make sure postdata was passed #
 
    # as a dict. Add headers here.                              #
 
    ##############################################################
 
 
 
    if not session:
 
        session = requests.Session()
 
        session.headers.update({'User-Agent':'Python Services API Client v{}'
 
                                .format(version)})
 
        if opts['noetag']:
 
            session.headers.update({'Cache-Control':'no-store'})
 
            session.headers.update({'If-None-Match':''})
 
 
 
        if opts['nogzip']:
 
            session.headers.update({'Accept-Encoding':''})
 
        else:
 
            session.headers.update({'Accept-Encoding':'gzip,deflate'})
 
 
 
        if opts['usexml']:
 
            session.headers.update({'Accept':None})
 
        else:
 
            session.headers.update({'Accept':'application/json'})
 
 
 
        if opts['debug']: print('Debug: New session: {}'.format(session))
 
 
 
        # TODO: Problem with the BE not accepting postdata in the initial
 
        # authorized query, Using a GET first as a workaround.
 
 
 
        try:
 
            opts['user'] and  opts['pass']
 
            session.auth = HTTPDigestAuth(opts['user'], opts['pass'])
 
            Send(host=host, port=port, endpoint='Myth/version', opts=opts)
 
        except:
 
            # Proceed without authentication.
 
            pass
 
 
 
    if opts['debug']:
 
        print('Debug: URL = {}'.format(url))
 
        if postdata:
 
            print('Debug: The following postdata was included:')
 
            for key in postdata:
 
                print('  {:30} {}'.format(key, postdata[key]))
 
 
 
    if postdata and not opts['wrmi']:
 
        return { 'Warning':'wrmi=False' }
 
 
 
    if postdata:
 
        if not isinstance(postdata, dict):
 
            return { 'Abort':'usage: postdata must be passed as a dict' }
 
 
 
    if opts['wsdl'] and (rest or postdata):
 
            return { 'Abort':'usage: rest/postdata aren\'t allowed with WSDL' }
 
  
     ##############################################################
+
     backend = api.Send(host=args.host)
    # Actually try to get the data and handle errors.           #
 
    ##############################################################
 
  
 
     try:
 
     try:
         if postdata:
+
         resp_dict = backend.send(endpoint=endpoint, rest=rest, opts=opts)
            response = session.post(url, data=postdata)
+
     except RuntimeError as error:
        else:
+
         sys.exit('\nFatal error: "{}"'.format(error))
            response = session.get(url)
 
     except requests.exceptions.HTTPError:
 
        return { 'Abort':'HTTP Error. URL was: {}'.format(url) }
 
    except requests.exceptions.URLRequired:
 
        return { 'Abort':'URL Required. URL was: {}'.format(url) }
 
    except requests.exceptions.Timeout:
 
         return { 'Abort':'Connect/Read Timeout: URL was {}'.format(url) }
 
    except requests.exceptions.ConnectionError:
 
        return { 'Abort':'Connection Error: URL was {}'.format(url) }
 
    except requests.exceptions.InvalidURL:
 
        return { 'Abort':'Invalid URL: URL was {}'.format(url) }
 
    except KeyboardInterrupt:
 
        return { 'Abort':'Keyboard Interrupt' }
 
    except:
 
        return { 'Abort':'Unexpected error: URL was: {}'.format(url) }
 
 
 
    if response.status_code == 401:
 
        return { 'Abort':'Unauthorized (401) error. Need valid user/password.' }
 
 
 
    # TODO: Should handle redirects here:
 
    if response.status_code > 299:
 
        return { 'Abort':'Unexpected status returned: {}: URL was: {}'.format(
 
            response.status_code, url) }
 
 
 
    ##################################################################
 
    # Process the contents of the HTTP Server: header. Try to see    #
 
    # what version the server is running on. As of this writing the  #
 
    # expected contents for 29, 0.28 and 0.27 are similar to:        #
 
    #                                                                #
 
    #MythTV/29-pre-5-g6865940-dirty Linux/3.13.0-85-generic UPnP/1.0.#
 
    #MythTV/0.28-pre-3094-g349d3a4 Linux/3.13.0-66-generic UPnP/1.0  #
 
    #MythTV/28.0-10-g57c1afb Linux/4.4.0-21-generic UPnP/1.0.        #
 
    #Linux 3.13.0-65-generic, UPnP/1.0, MythTV 0.27.20150622-1      #
 
    ##################################################################
 
 
 
    server = response.headers['Server']
 
 
 
    if server == None:
 
        return { 'Abort':'No HTTP "Server:" header returned.' }
 
    else:
 
        if re.search(version, server):
 
            ServerVersion = '29'
 
        elif re.search('0.28', server) or re.search('28.', server):
 
            ServerVersion = '0.28'
 
        elif re.search('0.27', server):
 
            ServerVersion = '0.27'
 
        else:
 
            return { 'Abort':'Tested on 0.27/0.28/29, not: {}.'.format(server) }
 
  
 
     ##############################################################
 
     ##############################################################
     # Finally, return the response after converting the JSON to #
+
     # ... and also set the module's timezone offset so that the  #
     # a dict. Or, if the wsdl option is set return that          #
+
     # start times in the response can be converted to local time.#
 
     ##############################################################
 
     ##############################################################
  
     if opts['wsdl']:
+
     util.get_utc_offset(backend=backend, opts=opts)
        return { 'WSDL':response.text}
 
 
 
    if opts['debug']:
 
        print('Debug: 1st 60 bytes of response: {}'.format(response.text[:60]))
 
 
 
    try:
 
        return response.json()
 
    except Exception as err:
 
        return { 'Abort':err }
 
 
 
def URLEncode(value=''):
 
    """
 
    This is really unnecessary. It's more of a reminder about how to
 
    use urllib.[parse]quote(). At least as of this writing, 0.28-pre
 
    doesn't decode the escaped values and the endpoints just get the
 
    percent encoded text. E.g. don't use it. How show titles with
 
    & or = in them work isn't clear.
 
 
 
    Input:  A string. E.g a program's title or anything that has
 
            special characters like ?, & and UTF characters beyond
 
            the ASCII set.
 
 
 
    Output: The URL encoded string. E.g. ó becomes: %C3%B3 or ?
 
            becomes %3F.
 
    """
 
 
 
    if value == '':
 
        print('Warning: Utilities.URLEncode() called without any value')
 
        return value
 
 
 
    return quote(value)
 
 
 
def CreateFindTime(time=''):
 
    """
 
    Normally used to take a starttime and convert it for use in adding
 
    new recordings. GetUTCOffset() should be called before this is, but
 
    that only needs to be done once. TODO: shouldn't this be removed and
 
    just use UTCToLocal with omityear=True?
 
 
 
    Input:  Full UTC timestamp, e.g. 2014-08-12T22:00:00 (with or without
 
            the trailing 'Z'.)
 
 
 
    Output: Time portion of the above in local time.
 
    """
 
 
 
    global UTCOffset
 
 
 
    if time == '':
 
        print('Warning: CreateFindTime called without any time')
 
        return None
 
 
 
    try:
 
        int(UTCOffset)
 
        utc_offset = UTCOffset
 
    except (NameError, ValueError):
 
        print('Warning: Run GetUTCOffset() first. using UTC offset of 0.')
 
        utc_offset = 0
 
 
 
    time = time.replace('Z', '')
 
 
 
    dt = datetime.strptime(time, '%Y-%m-%dT%H:%M:%S')
 
 
 
    return (dt + timedelta(seconds = utc_offset)).strftime('%H:%M:%S')
 
 
 
def UTCToLocal(utctime='', omityear=False):
 
    """
 
    Does exactly that conversion. GetUTCOffset() should be run once before
 
    calling this function. A UTC offset of 0 will be used if UTCOffset
 
    isn't available, so the function won't abort.
 
 
 
    Inputs:  utctime  = Full UTC timestamp, e.g. 2014-08-12T22:00:00[Z].
 
            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 no trailing Z.
 
    """
 
 
 
    global UTCOffset
 
 
 
    try:
 
        int(UTCOffset)
 
        utc_offset = UTCOffset
 
    except (NameError, ValueError):
 
        print('Warning: Run GetUTCOffset() first, using UTC offset of 0.')
 
        utc_offset = 0
 
 
 
    if utctime == '':
 
        return 'Error: UTCToLocal(): utctime is 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 = utc_offset)).strftime(fromstring)
 
 
 
def GetUTCOffset(host='', port=6544, opts={}):
 
    """
 
    Get the backend's offset from UTC. Once retrieved, it's saved value is
 
    available in UTCOffset and is returned too. Additional calls to this
 
    function aren't necessary, but if made, won't query the backend again.
 
 
 
    Input:  host, optional port.
 
 
 
    Output: The offset (in seconds) or -1 and a message prints
 
    """
 
 
 
    global UTCOffset
 
 
 
    if host == '':
 
        print('GetUTCOffset(): Error: host is empty.')
 
        return -1
 
 
 
    try:
 
        int(UTCOffset)
 
        return UTCOffset
 
    except (NameError, ValueError):
 
 
 
        resp_dict = Send(host=host, port=port, endpoint='Myth/GetTimeZone',
 
                        opts=opts)
 
 
 
        if list(resp_dict.keys())[0] in ['Abort', 'Warning']:
 
            print('GetUTCOffset(): {}'.format(resp_dict))
 
            return -1
 
        else:
 
            UTCOffset = int(resp_dict['TimeZoneInfo']['UTCOffset'])
 
 
 
        return UTCOffset
 
 
 
def RecStatusToString(host='', port=6544, recStatus=0, opts={}):
 
    """
 
    Convert a signed integer to a Recording Status String
 
    and cache the result.
 
 
 
    recStatus defaults to 0, which currently (29.0) means 'Unknown'
 
    """
 
 
 
    global RecStatusCache
 
 
 
    if host == '':
 
        print('RecStatusToString(): Error: host is empty.')
 
        return -1
 
 
 
    try:
 
        str(RecStatusCache[recStatus])
 
        return RecStatusCache[recStatus]
 
    except (KeyError, NameError, ValueError):
 
        endpoint='Dvr/RecStatusToString'
 
        rest = 'RecStatusCache={}'.format(recStatus)
 
  
        resp_dict = Send(host=host, port=port, endpoint=endpoint, rest=rest, \
+
    count = int(resp_dict['ProgramList']['TotalAvailable'])
                        opts=opts)
+
    programs = resp_dict['ProgramList']['Programs']
  
        if list(resp_dict.keys())[0] in ['Abort', 'Warning']:
+
    if args.debug:
            print('RecStatusToString(): {}'.format(resp_dict))
+
        print('Debug: Upcoming recording count = {}'.format(count))
            return resp_dict.keys()[0]
 
        else:
 
            RecStatusCache[recStatus] = resp_dict['String']
 
  
         return RecStatusCache[recStatus]
+
    if count < 1:
 +
         print('\nNo upcoming recordings found.\n')
 +
        sys.exit(0)
  
def RecTypeToString(host='', port=6544, recType=0, opts={}):
+
    print('\nPrinting {} day{} of upcoming programs sorted by StartTime'
    """
+
          .format(args.days, 's'[args.days == 1:]))
    Convert a signed integer to a Recording Type String and cache
+
    print(u'\n  {}{:16}  {:40.40} {:25.25}  {}  {}'
    the result.
+
          .format(YELLOW, 'StartTime', 'Title', 'SubTitle',
 +
                  'Flags', WHITE).encode('utf-8'))
  
     recType defaults to 0, which currently (29.0) means 'Not Recording'
+
     matched_upcoming = 0
    """
 
  
     global RecTypeCache
+
     for program in programs:
  
    if host == '':
+
        retval = print_details(program, args)
        print('RecTypeToString(): Error: host is empty.')
 
        return -1
 
  
    try:
+
         if retval == -1:
         str(RecTypeCache[recType])
+
            break
        return RecTypeCache[recType]
 
    except (KeyError, NameError, ValueError):
 
        endpoint='Dvr/RecTypeToString'
 
        rest = 'RecTypeCache={}'.format(recType)
 
  
         resp_dict = Send(host=host, port=port, endpoint=endpoint, rest=rest, \
+
         matched_upcoming += retval
                        opts=opts)
 
  
        if list(resp_dict.keys())[0] in ['Abort', 'Warning']:
+
    print('\n  Total Upcoming Programs: {}'.format(matched_upcoming))
            print('RecTypeToString(): {}'.format(resp_dict))
 
            return resp_dict.keys()[0]
 
        else:
 
            RecTypeCache[recType] = resp_dict['String']
 
  
        return RecTypeCache[recType]
 
  
def DupMethodToString(host='', port=6544, dupMethod=0, opts={}):
+
if __name__ == '__main__':
     """
+
     main()
    Convert a signed integer to a Duplicate Method String and cache
 
    the result.
 
  
    dupMethod defaults to 0, which currently (29.0) means 'No Search'
+
# vim: set expandtab tabstop=4 shiftwidth=4 smartindent colorcolumn=80:
    """
 
 
 
    global DupMethodCache
 
 
 
    if host == '':
 
        print('DupMethodToString(): Error: host is empty.')
 
        return -1
 
 
 
    try:
 
        str(DupMethodCache[dupMethod])
 
        return DupMethodCache[dupMethod]
 
    except (KeyError, NameError, ValueError):
 
        endpoint='Dvr/DupMethodToString'
 
        rest = 'DupMethodCache={}'.format(dupMethod)
 
 
 
        resp_dict = Send(host=host, port=port, endpoint=endpoint, rest=rest, \
 
                        opts=opts)
 
 
 
        if list(resp_dict.keys())[0] in ['Abort', 'Warning']:
 
            print('DupMethodToString(): {}'.format(resp_dict))
 
            return resp_dict.keys()[0]
 
        else:
 
            DupMethodCache[dupMethod] = resp_dict['String']
 
 
 
        return DupMethodCache[dupMethod]
 
 
</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: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.