#!/usr/bin/python3 # -*- coding: utf-8 -*- ''' Switch Schedules Direct channels using DataDirect to JSON SQLite N.B.: This program assumes that the SD DD xmltvid strings to be converted are integers, which is typical. Designed for switching from the DataDirect service to the tv_grab_zz_sdjson_sqlite grabber. Configures Video Sources, Channels and Settings. Also allows reverting to the DD service, although the mythconverg.settings need to be changed manually. MythFillDatabaseArgs is the only critical one and it is usually set to --dd-grab-all using mythtv-setup. Existing source(s) will be converted/reverted. Run this after doing the 4 setup steps in Wiki for tv_grab_zz_sdjson_sqlite. Use --help to see the options. If --wrmi isn't on the command line, then no changes will be sent to the backend, but what would have been changes will be displayed. The backend must be running to use this. switch_to_json.py --host yourBackendHostName --verbose for starters. No changes will be made to the mythconverg DB will be made. ''' from __future__ import absolute_import from __future__ import print_function import argparse import json import logging import sys # pylint: disable=broad-except try: from MythTV.services_api import send as api except Exception: print('\nThis script only works with MythTV v30 and above\n') OPPOSITE = '2' if sys.version_info.major == 3 else '3' sys.exit("Incomplete or missing MythTV package, try using python{}" .format(OPPOSITE)) SD_DD_GRABBER = 'schedulesdirect1' SD_JSON_GRABBER = 'tv_grab_zz_sdjson_sqlite' SD_JSON_XMLTVID = '.json.schedulesdirect.org' SD_JSON_LENGTH = len(SD_JSON_XMLTVID) def process_arguments(): ''' All command line processing is done here. ''' parser = argparse.ArgumentParser(description='Convert SD from DD to JSON', epilog='Default values are in ()s') parser.add_argument('--debug', action='store_true', help='turn on debug messages (%(default)s)') mandatory = parser.add_argument_group('required arguments') mandatory.add_argument('--host', type=str, required=True, metavar='', help='backend hostname') parser.add_argument('--digest', type=str, metavar='', help='digest username:password (%(default)s)') parser.add_argument('--port', type=int, default=6544, metavar='', help='port number of the Services API (%(default)s)') parser.add_argument('--invisible', action='store_true', help='convert invisible channels too (%(default)s)') parser.add_argument('--revert', action='store_true', help="revert to DD, doesn't do settings (%(default)s)") parser.add_argument('--verbose', action='store_true', help='dump the parameters to be sent (%(default)s)') parser.add_argument('--version', action='version', version='%(prog)s 0.9') parser.add_argument('--wrmi', action='store_true', help='actually send changes (%(default)s)') return parser.parse_args() def sanity_check(backend=None, opts=None): ''' Just make sure the backend is up. ''' try: backend.send(endpoint='Myth/version', opts=opts) except (RuntimeError, RuntimeWarning): sys.exit('No (or unexpected) result from backend, is it running?') def check_bool_response(server_response, endpoint): ''' Expect: {"bool": "true" (or "false")}. ''' try: bool_answer = server_response['bool'] except KeyError: print('{} expected a bool, got: {}'.format(endpoint, bool_answer)) return False if bool_answer != 'true': return False return True def update_video_sources(backend=None, args=None, opts=None): ''' Get all video sources and change those that are set to use the DD interface to the JSON one. Or revert to the DD one if requested. ''' get_endpt = 'Channel/GetVideoSourceList' upd_endpt = 'Channel/UpdateVideoSource' try: resp_dict = backend.send(endpoint=get_endpt, opts=opts) video_source_list = resp_dict['VideoSourceList']['VideoSources'] except (RuntimeError, KeyError) as error: sys.exit('\nFatal error: "{}"'.format(error)) if video_source_list is None: sys.exit('\nNo video sources available [very odd], aborting.') if args.verbose: print('Current video source data:') print(json.dumps(video_source_list, sort_keys=True, indent=4, separators=(',', ': '))) for postdata in video_source_list: if args.revert: if postdata['Grabber'] == SD_DD_GRABBER: print('Video source {} is already using "{}"' .format(postdata['SourceName'], SD_DD_GRABBER)) continue if postdata['Grabber'] != SD_JSON_GRABBER: print('Video source {} doesn\'t use "JSON"' .format(postdata['SourceName'])) continue postdata['Grabber'] = SD_DD_GRABBER if not args.revert: if postdata['Grabber'] == SD_JSON_GRABBER: print('Video source {} is already using JSON grabber' .format(postdata['SourceName'])) continue if postdata['Grabber'] != SD_DD_GRABBER: print('Video source {} doesn\'t use "{}"' .format(postdata['SourceName'], SD_DD_GRABBER)) continue postdata['Grabber'] = SD_JSON_GRABBER postdata['SourceId'] = postdata['Id'] del postdata['Id'] if postdata['ConfigPath'] is None or postdata['ConfigPath'] == '': del postdata['ConfigPath'] # Delete to put NULL in the DB not ""! if args.verbose: print('Changed video source data:') print(json.dumps(postdata, sort_keys=True, indent=4, separators=(',', ': '))) print('{} grabber to: {} on source: {} (Id={})' .format('Reverting' if args.revert else 'Switching', postdata['Grabber'], postdata['SourceName'], postdata['SourceId'])) try: resp_dict = backend.send(endpoint=upd_endpt, postdata=postdata, opts=opts) except RuntimeWarning: print('--wrmi not set, Video Source {} not updated' .format(postdata['SourceName'])) continue if not check_bool_response(resp_dict, upd_endpt): return False return True def update_settings(backend=None, args=None, opts=None): ''' Change 2 settings for use with the JSON grabber ''' settings = {'DataDirectMessage': 'Unavailable with SD JSON XMLTV', 'MythFillDatabaseArgs': ''} endpoint = 'Myth/PutSetting' for key in settings: if args.revert and key == 'MythFillDatabaseArgs': print('Use mythtv-setup to restore the {} setting'.format(key)) continue postdata = {'Key': key, 'Value': settings[key]} try: resp_dict = backend.send(endpoint=endpoint, postdata=postdata, opts=opts) except RuntimeWarning: print('--wrmi not set, {} setting not changed'.format(key)) return False if not check_bool_response(resp_dict, endpoint): return False return True def update_channel_xmltvid(backend=None, args=None, opts=None): ''' Change the existing xmltvid from an integer to the new string or strip off the leading I and trailing json... text to restore the xmltvid for use by SD DD. ''' get_endpt = 'Channel/GetChannelInfoList' upd_endpt = 'Channel/UpdateDBChannel' rest = 'Details=true&OnlyVisible={}&OrderByName=true'.format( 'false' if args.invisible else 'true') chan_dict = backend.send(endpoint=get_endpt, rest=rest, opts=opts) try: channel_count = int(chan_dict['ChannelInfoList']['Count']) chan_dict = chan_dict['ChannelInfoList']['ChannelInfos'] except (KeyError, ValueError): sys.exit('{} Count is missing/non-integer, aborting'.format(get_endpt)) for channel in range(channel_count): xmltvid = chan_dict[channel]['XMLTVID'] postdata = dict() if not xmltvid: continue if args.revert: try: int(xmltvid) print('Channel already using SD DD: {} [{}]' .format(chan_dict[channel]['ChannelName'], xmltvid)) continue except ValueError: if xmltvid[-SD_JSON_LENGTH:] != SD_JSON_XMLTVID \ and xmltvid[0] != 'I': print('Channel isn\'t using SD JSON: {} [{}]' .format(chan_dict[channel]['ChannelName'], xmltvid)) continue tmp_xmltvid = chan_dict[channel]['XMLTVID'][:-SD_JSON_LENGTH] postdata['XMLTVID'] = tmp_xmltvid[1:] else: if xmltvid[-SD_JSON_LENGTH:] == SD_JSON_XMLTVID \ and xmltvid[0] == 'I': print('Channel already using SD JSON: {} [{}]' .format(chan_dict[channel]['ChannelName'], xmltvid)) continue postdata['XMLTVID'] = 'I{}{}'.format(xmltvid, SD_JSON_XMLTVID) postdata['ChannelID'] = chan_dict[channel]['ChanId'] postdata['MplexID'] = chan_dict[channel]['MplexId'] postdata['SourceID'] = chan_dict[channel]['SourceId'] postdata['CallSign'] = chan_dict[channel]['CallSign'] postdata['ChannelName'] = chan_dict[channel]['ChannelName'] postdata['ServiceID'] = chan_dict[channel]['ServiceId'] postdata['ATSCMajorChannel'] = chan_dict[channel]['ATSCMajorChan'] postdata['ATSCMinorChannel'] = chan_dict[channel]['ATSCMinorChan'] postdata['UseEIT'] = chan_dict[channel]['UseEIT'] postdata['visible'] = chan_dict[channel]['Visible'] if args.verbose: print('Channel changes for: {} '.format(postdata['ChannelName'])) print(json.dumps(postdata, sort_keys=True, indent=4, separators=(',', ': '))) try: resp = backend.send(endpoint=upd_endpt, postdata=postdata, opts=opts) except RuntimeWarning: print('--wrmi not set, {} not changed' .format(chan_dict[channel]['ChannelName'])) continue if check_bool_response(resp, upd_endpt): print('Channel {} to SD {}: {}' .format('reverted' if args.revert else 'switched', 'DD' if args.revert else 'JSON', chan_dict[channel]['ChannelName'])) return True def main(): ''' Process arguments, setup logging as requested, make sure the backend is up and the API is responding, switch/revert the video sources, change settings and then switch/revert the channels. ''' args = process_arguments() opts = {'noetag': False, 'nogzip': False, 'timeout': 4, 'usexml': False, 'wrmi': args.wrmi, 'wsdl': False} 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(':', 1) except (AttributeError, ValueError): pass backend = api.Send(args.host, args.port) sanity_check(backend=backend, opts=opts) update_video_sources(backend=backend, args=args, opts=opts) update_settings(backend=backend, args=args, opts=opts) update_channel_xmltvid(backend=backend, args=args, opts=opts) if __name__ == '__main__': main() # pylint: disable=pointless-string-statement ''' :!python3 % --host ofc0 --digest=admin:mythtv --debug :!python3 % --host ofc0 --digest=admin:mythtv ''' # vim: set expandtab tabstop=4 shiftwidth=4 smartindent noai colorcolumn=80: