· 4 years ago · Jul 10, 2021, 09:20 PM
1# -*- coding: utf-8 -*-
2
3'''
4Switch Schedules Direct channels using DataDirect to JSON SQLite
5
6N.B.: This program assumes that the SD DD xmltvid strings to
7be converted are integers, which is typical.
8
9Designed for switching from the DataDirect service to the
10tv_grab_zz_sdjson_sqlite grabber. Configures Video Sources,
11Channels and Settings. Also allows reverting to the DD
12service, although the mythconverg.settings need to be changed
13manually. MythFillDatabaseArgs is the only critical one and
14it is usually set to --dd-grab-all using mythtv-setup.
15
16Don't create a new source, as the WiKi talks about. Existing
17source(s) will be converted/reverted.
18
19Run this after doing the 4 setup steps in tv_grab_zz_sdjson_sqlite.
20
21Use --help to see the options. If --wrmi isn't on the command line,
22then no changes will be sent to the backend, but what would
23have been changes will be displayed.
24
25The backend must be running to use this.
26
27Try: python{2|3} switch_to_json.py --host yourBackendHostName --verbose for
28starters. No changes will be made to the mythconverg DB will be made.
29'''
30
31from __future__ import absolute_import
32from __future__ import print_function
33import argparse
34import json
35import logging
36import sys
37
38# pylint: disable=broad-except
39try:
40 from MythTV.services_api import send as api
41except Exception:
42 print('\nThis script only works with MythTV v30 and above\n')
43 OPPOSITE = '2' if sys.version_info.major == 3 else '3'
44 sys.exit("Incomplete or missing MythTV package, try using python{}"
45 .format(OPPOSITE))
46
47SD_DD_GRABBER = 'schedulesdirect1'
48SD_JSON_GRABBER = 'tv_grab_zz_sdjson_sqlite'
49SD_JSON_XMLTVID = '.json.schedulesdirect.org'
50SD_JSON_LENGTH = len(SD_JSON_XMLTVID)
51
52
53def process_arguments():
54 ''' All command line processing is done here. '''
55
56 parser = argparse.ArgumentParser(description='Convert SD from DD to JSON',
57 epilog='Default values are in ()s')
58
59 parser.add_argument('--debug', action='store_true',
60 help='turn on debug messages (%(default)s)')
61
62 mandatory = parser.add_argument_group('required arguments')
63
64 mandatory.add_argument('--host', type=str, required=True,
65 metavar='<hostname>', help='backend hostname')
66
67 parser.add_argument('--digest', type=str, metavar='<user:pass>',
68 help='digest username:password (%(default)s)')
69
70 parser.add_argument('--port', type=int, default=6544, metavar='<port>',
71 help='port number of the Services API (%(default)s)')
72
73 parser.add_argument('--invisible', action='store_true',
74 help='convert invisible channels too (%(default)s)')
75
76 parser.add_argument('--revert', action='store_true',
77 help="revert to DD, doesn't do settings (%(default)s)")
78
79 parser.add_argument('--verbose', action='store_true',
80 help='dump the parameters to be sent (%(default)s)')
81
82 parser.add_argument('--version', action='version', version='%(prog)s 0.9')
83
84 parser.add_argument('--wrmi', action='store_true',
85 help='actually send changes (%(default)s)')
86
87 return parser.parse_args()
88
89
90def sanity_check(backend=None, opts=None):
91 ''' Just make sure the backend is up. '''
92
93 try:
94 backend.send(endpoint='Myth/version', opts=opts)
95 except (RuntimeError, RuntimeWarning):
96 sys.exit('No (or unexpected) result from backend, is it running?')
97
98
99def check_bool_response(server_response, endpoint):
100 ''' Expect: {"bool": "true" (or "false")}. '''
101
102 try:
103 bool_answer = server_response['bool']
104 except KeyError:
105 print('{} expected a bool, got: {}'.format(endpoint, bool_answer))
106 return False
107
108 if bool_answer != 'true':
109 return False
110
111 return True
112
113
114def update_video_sources(backend=None, args=None, opts=None):
115 '''
116 Get all video sources and change those that are set
117 to use the DD interface to the JSON one. Or revert
118 to the DD one if requested.
119 '''
120
121 get_endpt = 'Channel/GetVideoSourceList'
122 upd_endpt = 'Channel/UpdateVideoSource'
123
124 try:
125 resp_dict = backend.send(endpoint=get_endpt, opts=opts)
126 video_source_list = resp_dict['VideoSourceList']['VideoSources']
127 except (RuntimeError, KeyError) as error:
128 sys.exit('\nFatal error: "{}"'.format(error))
129
130 if video_source_list is None:
131 sys.exit('\nNo video sources available [very odd], aborting.')
132
133 if args.verbose:
134 print('Current video source data:')
135 print(json.dumps(video_source_list, sort_keys=True,
136 indent=4, separators=(',', ': ')))
137
138 for postdata in video_source_list:
139
140 if args.revert:
141 if postdata['Grabber'] == SD_DD_GRABBER:
142 print('Video source {} is already using "{}"'
143 .format(postdata['SourceName'], SD_DD_GRABBER))
144 continue
145
146 if postdata['Grabber'] != SD_JSON_GRABBER:
147 print('Video source {} doesn\'t use "JSON"'
148 .format(postdata['SourceName']))
149 continue
150
151 postdata['Grabber'] = SD_DD_GRABBER
152
153 if not args.revert:
154 if postdata['Grabber'] == SD_JSON_GRABBER:
155 print('Video source {} is already using JSON grabber'
156 .format(postdata['SourceName']))
157 continue
158
159 if postdata['Grabber'] != SD_DD_GRABBER:
160 print('Video source {} doesn\'t use "{}"'
161 .format(postdata['SourceName'], SD_DD_GRABBER))
162 continue
163
164 postdata['Grabber'] = SD_JSON_GRABBER
165
166 postdata['SourceId'] = postdata['Id']
167 del postdata['Id']
168
169 if postdata['ConfigPath'] is None or postdata['ConfigPath'] == '':
170 del postdata['ConfigPath'] # Delete to put NULL in the DB not ""!
171
172 if args.verbose:
173 print('Changed video source data:')
174 print(json.dumps(postdata, sort_keys=True,
175 indent=4, separators=(',', ': ')))
176
177 print('{} grabber to: {} on source: {} (Id={})'
178 .format('Reverting' if args.revert else 'Switching',
179 postdata['Grabber'], postdata['SourceName'],
180 postdata['SourceId']))
181
182 try:
183 resp_dict = backend.send(endpoint=upd_endpt, postdata=postdata,
184 opts=opts)
185 except RuntimeWarning:
186 print('--wrmi not set, Video Source {} not updated'
187 .format(postdata['SourceName']))
188 continue
189
190 if not check_bool_response(resp_dict, upd_endpt):
191 return False
192
193 return True
194
195
196def update_settings(backend=None, args=None, opts=None):
197 ''' Change 2 settings for use with the JSON grabber '''
198
199 settings = {'DataDirectMessage': 'Unavailable with SD JSON XMLTV',
200 'MythFillDatabaseArgs': ''}
201
202 endpoint = 'Myth/PutSetting'
203
204 for key in settings:
205
206 if args.revert and key == 'MythFillDatabaseArgs':
207 print('Use mythtv-setup to restore the {} setting'.format(key))
208 continue
209
210 postdata = {'Key': key, 'Value': settings[key]}
211
212 try:
213 resp_dict = backend.send(endpoint=endpoint, postdata=postdata,
214 opts=opts)
215 except RuntimeWarning:
216 print('--wrmi not set, {} setting not changed'.format(key))
217 return False
218
219 if not check_bool_response(resp_dict, endpoint):
220 return False
221
222 return True
223
224
225def update_channel_xmltvid(backend=None, args=None, opts=None):
226 '''
227 Change the existing xmltvid from an integer to the new string or
228 strip off the leading I and trailing json... text to restore the
229 xmltvid for use by SD DD.
230 '''
231
232 get_endpt = 'Channel/GetChannelInfoList'
233 upd_endpt = 'Channel/UpdateDBChannel'
234 rest = 'Details=true&OnlyVisible={}&OrderByName=true'.format(
235 'false' if args.invisible else 'true')
236
237 chan_dict = backend.send(endpoint=get_endpt, rest=rest, opts=opts)
238
239 try:
240 channel_count = int(chan_dict['ChannelInfoList']['Count'])
241 chan_dict = chan_dict['ChannelInfoList']['ChannelInfos']
242 except (KeyError, ValueError):
243 sys.exit('{} Count is missing/non-integer, aborting'.format(get_endpt))
244
245 for channel in range(channel_count):
246
247 xmltvid = chan_dict[channel]['XMLTVID']
248
249 postdata = dict()
250
251 if not xmltvid:
252 continue
253
254 if args.revert:
255 try:
256 int(xmltvid)
257 print('Channel already using SD DD: {} [{}]'
258 .format(chan_dict[channel]['ChannelName'], xmltvid))
259 continue
260 except ValueError:
261 if xmltvid[-SD_JSON_LENGTH:] != SD_JSON_XMLTVID \
262 and xmltvid[0] != 'I':
263 print('Channel isn\'t using SD JSON: {} [{}]'
264 .format(chan_dict[channel]['ChannelName'], xmltvid))
265 continue
266 tmp_xmltvid = chan_dict[channel]['XMLTVID'][:-SD_JSON_LENGTH]
267 postdata['XMLTVID'] = tmp_xmltvid[1:]
268
269 else:
270 if xmltvid[-SD_JSON_LENGTH:] == SD_JSON_XMLTVID \
271 and xmltvid[0] == 'I':
272 print('Channel already using SD JSON: {} [{}]'
273 .format(chan_dict[channel]['ChannelName'], xmltvid))
274 continue
275 postdata['XMLTVID'] = 'I{}{}'.format(xmltvid, SD_JSON_XMLTVID)
276
277 postdata['ChannelID'] = chan_dict[channel]['ChanId']
278 postdata['MplexID'] = chan_dict[channel]['MplexId']
279 postdata['SourceID'] = chan_dict[channel]['SourceId']
280 postdata['CallSign'] = chan_dict[channel]['CallSign']
281 postdata['ChannelName'] = chan_dict[channel]['ChannelName']
282 postdata['ServiceID'] = chan_dict[channel]['ServiceId']
283 postdata['ATSCMajorChannel'] = chan_dict[channel]['ATSCMajorChan']
284 postdata['ATSCMinorChannel'] = chan_dict[channel]['ATSCMinorChan']
285 postdata['UseEIT'] = chan_dict[channel]['UseEIT']
286 postdata['visible'] = chan_dict[channel]['Visible']
287
288 if args.verbose:
289 print('Channel changes for: {} '.format(postdata['ChannelName']))
290 print(json.dumps(postdata, sort_keys=True,
291 indent=4, separators=(',', ': ')))
292
293 try:
294 resp = backend.send(endpoint=upd_endpt, postdata=postdata,
295 opts=opts)
296 except RuntimeWarning:
297 print('--wrmi not set, {} not changed'
298 .format(chan_dict[channel]['ChannelName']))
299 continue
300
301 if check_bool_response(resp, upd_endpt):
302 print('Channel {} to SD {}: {}'
303 .format('reverted' if args.revert else 'switched',
304 'DD' if args.revert else 'JSON',
305 chan_dict[channel]['ChannelName']))
306
307 return True
308
309
310def main():
311 '''
312 Process arguments, setup logging as requested, make sure the backend
313 is up and the API is responding, switch/revert the video sources,
314 change settings and then switch/revert the channels.
315 '''
316
317 args = process_arguments()
318
319 opts = {'noetag': False, 'nogzip': False, 'timeout': 4, 'usexml': False,
320 'wrmi': args.wrmi, 'wsdl': False}
321
322 logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
323 logging.getLogger('requests.packages.urllib3').setLevel(logging.WARNING)
324 logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING)
325
326 try:
327 opts['user'], opts['pass'] = args.digest.split(':', 1)
328 except (AttributeError, ValueError):
329 pass
330
331 backend = api.Send(args.host, args.port)
332
333 sanity_check(backend=backend, opts=opts)
334
335 update_video_sources(backend=backend, args=args, opts=opts)
336
337 update_settings(backend=backend, args=args, opts=opts)
338
339 update_channel_xmltvid(backend=backend, args=args, opts=opts)
340
341
342if __name__ == '__main__':
343 main()
344
345# pylint: disable=pointless-string-statement
346'''
347:!python3 % --host ofc0 --digest=admin:mythtv --debug
348:!python3 % --host ofc0 --digest=admin:mythtv
349'''
350
351# vim: set expandtab tabstop=4 shiftwidth=4 smartindent noai colorcolumn=80: