#!/usr/bin/env python # -*- coding: utf-8 -*- ''' Switch from DD to JSON Schedules Direct grabber (or revert.) Designed for switching from the Data Direct grabber to the tv_grab_zz_sdjson_sqlite grabber. Handles Video Sources, Channels and Settings. Also allows reverting back to the DD grabber, although the backend settings need to be changed manually. Run this after doing the setup steps in 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 you'll see what would have happened. N.B.: Assumes that all SD DD xmltvid are integers!!! ''' from __future__ import print_function import argparse import json import logging import sys from MythTV.services_api import send as api SD_DD_GRABBER = 'schedulesdirect1' SD_JSON_GRABBER = 'tv_grab_zz_sdjson_sqlite' SD_JSON_XMLTVID = '.json.schedulesdirect.org' 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)') 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='restore SD DD, but not 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.1') parser.add_argument('--wrmi', action='store_true', help='actually send changes (%(default)s)') mandatory = parser.add_argument_group('requrired arguments') mandatory.add_argument('--host', type=str, required=True, metavar='', help='backend hostname') 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('Myth/version got unexpected result from backend, aborting') 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 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['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.revert: postdata['Grabber'] = SD_DD_GRABBER else: postdata['Grabber'] = SD_JSON_GRABBER 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 changed') return False if not check_bool_response(resp_dict, upd_endpt): return False return True def update_settings(backend, args, opts): ''' Change 2 settings for use with the JSON grabber ''' settings = {'DataDirectMessage': 'Unavailable with SD JSON XMLTV', 'MythFillDatabaseArgs': ''} if args.revert: print('DB settings won\' t be reverted: {}'.format(settings)) return True endpoint = 'Myth/PutSetting' for key in settings: # TODO checkout iteritems on python v2 and 3 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, args, opts): ''' 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)) sd_json_length = len(SD_JSON_XMLTVID) 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 else: 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, {} source not changed'.format(upd_endpt)) return False del postdata # I don't think this is necessary... 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, '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, args, opts) update_channel_xmltvid(backend, args, opts) if __name__ == '__main__': main() # :!% --host ofc0 --digest=admin:mythtv --debug # :!% --host ofc0 --digest=admin:mythtv # vim: set expandtab tabstop=4 shiftwidth=4 smartindent noai colorcolumn=80: