Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- # ---------------------------
- # Name: mythtv_recording_rules
- # ---------------------------
- '''
- Create a new recording rule for the title passed on the command line.
- Use: mythtv_recording_rules.py --help to get started.
- The --title <title> must match a program name exactly and it will also
- become the name of the new recording rule. If the rule already exists,
- the program will abort.
- This is pep8 and pylint compliant. Tested using Python 3.
- '''
- from __future__ import print_function
- import argparse
- import json
- import logging
- import sys
- from datetime import timedelta, datetime, timezone
- from MythTV.services_api import (send as api, utilities as util)
- __title__ = "mythtv_recording_rules"
- __version__ = "v0.0.6"
- WEEKDAYAFTER = (lambda date, day: date +
- timedelta(days=(day - date.weekday() + 7) % 7))
- TYPES = (
- "Single Record",
- "Record All",
- "Record One",
- "Record Daily",
- "Record Weekly",
- )
- def process_command_line():
- '''All command line processing is done here.'''
- parser = argparse.ArgumentParser(description='Add a recording rule',
- epilog='Default values are in ()s')
- mandatory = parser.add_argument_group('required arguments')
- 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 (%(default)s)')
- mandatory.add_argument('--host', type=str, required=True,
- metavar='<hostname>', help='backend hostname')
- mandatory.add_argument('--title', type=str, required=False,
- metavar='<title>',
- help='full program name, no wild cards/regex')
- parser.add_argument('--port', type=int, default=6544, metavar='<port>',
- help='port number of the Services API (%(default)s)')
- parser.add_argument('--quiet', action='store_true',
- help='suppress progress messages (%(default)s)')
- parser.add_argument('--template', type=str, required=False,
- default='Default', metavar='<temp>',
- help='template name, (%(default)s)')
- parser.add_argument('--sources', action='store_true',
- help='List configured video sources (%(default)s)')
- parser.add_argument('--channels', type=int, required=False,
- metavar="<soruceid>",
- help='List configured channels for sourceid '
- '(%(default)s)')
- parser.add_argument('--chanid', type=str, required=False,
- metavar="<chanid>",
- help='Record on this channel')
- parser.add_argument('--manual', action='store_true',
- help='Create manual record rule (%(default)s)')
- parser.add_argument('--datetime', type=str, required=False,
- metavar="<datetime>",
- help='Manual record start datetime '
- 'in ISO format. i.e. "2018-08-05T05:00:00" '
- '(%(default)s))')
- parser.add_argument('--duration', type=str, required=False,
- metavar="<duration>", default=(60 * 60),
- help='Manual record duration in seconds (%(default)s)')
- values = ', '.join(TYPES)
- parser.add_argument('--type', type=str, required=False, choices=(TYPES),
- default='Record All', metavar='<type>',
- help='Record <type> [{}] (%(default)s)'.format(values))
- parser.add_argument('--version', action='version', version='%(prog)s {}'
- .format(__version__))
- parser.add_argument('--wrmi', action='store_true',
- help='allow data to be changed (%(default)s)')
- return vars(parser.parse_args())
- def setup(backend, opts):
- '''
- Make sure the backend is up (GetHostName) and then set the backend's UTC
- offset for other methods to use.
- '''
- try:
- backend.send(endpoint='Myth/GetHostName', opts=opts)
- int(util.get_utc_offset(backend=backend, opts=opts))
- except ValueError:
- sys.exit('\nAbort, non integer response from get_utc_offset.')
- except RuntimeError as error:
- sys.exit('\nAbort on fatal API error: "{}"'.format(error))
- def get_sources(backend, args):
- '''
- See: https://www.mythtv.org/wiki/Channel_Service#GetVideoSourceList
- '''
- endpoint = 'Channel/GetVideoSourceList'
- try:
- resp_dict = backend.send(endpoint=endpoint)
- except RuntimeError as error:
- sys.exit('\nFatal error: "{}"'.format(error))
- if args['debug']:
- print(json.dumps(resp_dict['VideoSoruceList'],
- sort_keys=True, indent=4, separators=(',', ': ')))
- return resp_dict['VideoSourceList']['VideoSources']
- def schedule_already_exists(backend, args, opts):
- '''
- See if there's already a rule for the title.
- '''
- endpoint = 'Dvr/GetRecordScheduleList'
- try:
- resp_dict = backend.send(endpoint=endpoint, opts=opts)
- except RuntimeError as error:
- sys.exit('\nAbort, Get Existing Rule: Fatal error; "{}"'.format(error))
- if int(resp_dict['RecRuleList']['Count']) < 1:
- sys.exit('\nAbort, no recording rules found.\n')
- for rule in resp_dict['RecRuleList']['RecRules']:
- if rule['Title'] == args['title']:
- if args['debug']:
- print(json.dumps(rule, sort_keys=True, indent=4,
- separators=(',', ': ')))
- return True
- return False
- def get_template(backend, args, opts):
- '''
- Gets the requested (or default) template. This will be modified
- with guide data for the title of interest, then send to the
- backend in a POST. Misspelled template names return the Default
- template.
- Only the template name is used, not the trailing: (Template) text.
- '''
- endpoint = 'Dvr/GetRecordSchedule'
- rest = 'Template={}'.format(args['template'])
- try:
- resp_dict = backend.send(endpoint=endpoint, rest=rest, opts=opts)
- except RuntimeError as error:
- sys.exit('\nAbort, Get Template: Fatal error; "{}"'.format(error))
- if args['debug']:
- print(json.dumps(resp_dict['RecRule'], sort_keys=True, indent=4,
- separators=(',', ': ')))
- # Templates are always Id -1, just double checking here...
- if resp_dict['RecRule']['Id'] != '-1':
- return None
- return resp_dict['RecRule']
- def get_program_data(backend, args, opts):
- '''
- Find matching program(s) from the guide. Note that if --title=Blah,
- then any title with the string Blah in it will be returned by
- GetProgramList.
- '''
- endpoint = 'Guide/GetProgramList'
- rest = 'Details=True&TitleFilter={}'.format(args['title'])
- try:
- resp_dict = backend.send(endpoint=endpoint, rest=rest, opts=opts)
- except RuntimeError as error:
- sys.exit('\nAbort, Get Upcoming: Fatal error; "{}"'.format(error))
- count = int(resp_dict['ProgramList']['TotalAvailable'])
- if args['debug']:
- print('\nDebug: Programs matching --title {} = {}'
- .format(args['title'], count))
- if count < 1:
- sys.exit('\nAbort, No programs in the guide matching: {}.\n'
- .format(args['title']))
- for program in resp_dict['ProgramList']['Programs']:
- if args['debug']:
- print('Comparing {} to {}'.format(args['title'], program['Title']))
- if program['Title'] == args['title']:
- if args['debug']:
- print(json.dumps(program, sort_keys=True, indent=4,
- separators=(',', ': ')))
- return program
- continue
- return None
- def update_template(template, guide_data, args):
- '''
- Put selected guide information into the template to be sent as
- postdata for the new rule.
- '''
- try:
- template['StartTime'] = guide_data['StartTime']
- template['EndTime'] = guide_data['EndTime']
- template['Title'] = guide_data['Title']
- template['Type'] = args['type']
- template['Station'] = guide_data['Channel']['CallSign']
- template['ChanId'] = guide_data['Channel']['ChanId']
- template['SearchType'] = 'None'
- template['Category'] = guide_data['Category']
- template['SeriesId'] = guide_data['SeriesId']
- template['FindTime'] = util.create_find_time(guide_data['StartTime'])
- template['Description'] = 'Rule created by add_recording_rule.py'
- except KeyError:
- return False
- return True
- def add_rule(backend, template, args, opts):
- '''
- Send the changed data to the backend.
- '''
- endpoint = 'Dvr/AddRecordSchedule'
- params_not_sent = ('AverageDelay', 'CallSign', 'Id', 'LastDeleted',
- 'LastRecorded', 'NextRecording', 'ParentId')
- for param in params_not_sent:
- try:
- del template[param]
- except KeyError:
- pass
- opts['wrmi'] = args['wrmi']
- try:
- resp_dict = backend.send(endpoint=endpoint, postdata=template,
- opts=opts)
- except RuntimeWarning as error:
- sys.exit('Abort, Unable to add rule: {}. Warning was: {}.'
- .format(template['Title'], error))
- except RuntimeError as error:
- sys.exit('\nAbort, Fatal API Error response: {}\n'.format(error))
- opts['wrmi'] = False
- if isinstance(resp_dict, dict) and isinstance(resp_dict['uint'], str):
- recording_rule = int(resp_dict['uint'])
- if recording_rule < 4294967295:
- vprint('\nAdded: "{}" (RecordId {}).'
- .format(template['Title'], recording_rule), args)
- else:
- recording_rule = -1
- vprint('Backend failed to add: "{}" (RecordId {}).'
- .format(template['Title'], recording_rule), args)
- return False
- else:
- vprint('Expected a "uint: int" dictionary response, but got {}'
- .format(resp_dict), args)
- return False
- return True
- def record_title(backend, args, opts):
- '''
- Create a template based on a single title
- '''
- template = get_template(backend, args, opts)
- if not template:
- sys.exit('\nAbort, no template found for: {}.'.format(args['title']))
- guide_data = get_program_data(backend, args, opts)
- if not guide_data:
- sys.exit('\nAbort, no match in guide for: {}'.format(args['title']))
- if update_template(template, guide_data, args):
- add_rule(backend, template, args, opts)
- else:
- sys.exit('\nAbort, error while copying guide data to template.')
- def get_channels(backend, sourceid):
- '''
- See: https://www.mythtv.org/wiki/Channel_Service#GetChannelInfoList
- '''
- endpoint = 'Channel/GetChannelInfoList'
- rest = 'SoruceID={}&OnlyVisible=true&Details=true'.format(sourceid)
- try:
- resp_dict = backend.send(endpoint=endpoint, rest=rest)
- except RuntimeError as error:
- sys.exit('\nFatal error: "{}"'.format(error))
- return resp_dict['ChannelInfoList']['ChannelInfos']
- def get_channel(backend, chanid):
- '''
- See: https://www.mythtv.org/wiki/Channel_Service#GetChannelInfo
- '''
- endpoint = 'Channel/GetChannelInfo'
- rest = 'ChanID={}&OnlyVisible=true&Details=true'.format(chanid)
- try:
- resp_dict = backend.send(endpoint=endpoint, rest=rest)
- except RuntimeError as error:
- sys.exit('\nFatal error: "{}"'.format(error))
- return resp_dict['ChannelInfo']
- def record_manual_type(backend, args, opts, rec_type, chaninfo,
- template, starttime, duration):
- ''' Create a template for a manual recording '''
- if not starttime:
- sys.exit('\nAbort, manul record: no starttime provided.')
- localtz = datetime.now().astimezone().tzinfo
- # Convert to UTC
- start = starttime.replace(tzinfo=localtz).astimezone(tz=timezone.utc)
- end = starttime + timedelta(seconds=duration)
- end = end.replace(tzinfo=localtz).astimezone(tz=timezone.utc)
- template['StartTime'] = "{}".format(start.isoformat()
- .replace('+00:00', 'Z'))
- template['EndTime'] = "{}".format(end.isoformat()
- .replace('+00:00', 'Z'))
- template['Description'] = ('{} (Manual Record)'
- .format(starttime.strftime('%H')))
- template['FindTime'] = starttime.strftime('%H:%M:%S')
- template['Type'] = rec_type
- template['Title'] = args['title']
- template['Station'] = chaninfo['CallSign']
- template['CallSign'] = chaninfo['CallSign']
- print("{}\n".format(template))
- return add_rule(backend, template, args, opts)
- def record_manual_24x7(backend, args, opts, chaninfo, rec24x7):
- ''' Develop Daily rule'''
- localtz = datetime.now().astimezone().tzinfo
- # First Saturday in July, resuling in 7-day/week schedule
- saturday = WEEKDAYAFTER(datetime(2018, 7, 1, tzinfo=localtz), 5)
- duration = 60 * 60
- for hour in list(range(24)):
- start = saturday.replace(hour=hour)
- rec24x7['SubTitle'] = 'hour {}'.format(start.strftime('%H'))
- if not record_manual_type(backend, args, opts, 'Record Daily',
- chaninfo, rec24x7, start, duration):
- return False
- return True
- def record_manual(backend, args, opts):
- ''' Choose Daily or Record All rule'''
- template = get_template(backend, args, opts)
- if not template:
- sys.exit('\nAbort, no template found for: {}.'
- .format(args['template']))
- if not args['chanid']:
- sys.exit('\nAbort, no chanid provided for manual record.')
- template['ChanId'] = args['chanid']
- template['SearchType'] = 'Manual Search'
- template['Category'] = ''
- template['SeriesId'] = ''
- chaninfo = get_channel(backend, args['chanid'])
- if not chaninfo:
- print('Channel ID {} not found in available channels.'
- .format(args['chanid']))
- return
- if args['type'] == 'Record All':
- record_manual_24x7(backend, args, opts, chaninfo, template)
- else:
- record_manual_type(backend, args, opts, args['type'],
- chaninfo, template, args['datetime'],
- args['duration'])
- def vprint(message, args):
- '''
- Verbose Print: print recording rule information unless --quiet
- was used. Not fully implemented, as there are still lots of
- print()s here.
- The intention is that if run out of some other program, this
- will can remain quiet. sys.exit()s will return 1 for failures.
- This may get expanded to put messages in a log...
- '''
- if not args['quiet']:
- print(message)
- def main():
- '''
- The primary job of main is to get the arguments from the command line,
- setup logging (and possibly) handle the digest user/password then:
- • Create an instance of the Send class
- • See if a rule exists for --title
- • Get the selected template
- • Get data for command line title from the guide
- • Update the template with the guide data
- • Add the rule on the backend.
- '''
- 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(':', 1)
- except (AttributeError, ValueError):
- pass
- backend = api.Send(host=args['host'], port=args['port'])
- setup(backend, opts)
- if args['sources']:
- sources = get_sources(backend, args)
- for source in sources:
- print('{}: {}'.format(source['Id'], source['SourceName']))
- elif args['channels']:
- for channels in get_channels(backend, args['channels']):
- print('{0:>6}: {1:>5} {2:10} {3}'.format(channels['ChanId'],
- channels['ChanNum'],
- channels['CallSign'],
- channels['ChannelName']))
- else:
- if schedule_already_exists(backend, args, opts):
- sys.exit('\nAbort, rule for: {} already exists.'
- .format(args['title']))
- if int(args['manual']) > 0:
- record_manual(backend, args, opts)
- else:
- record_title(backend, args, opts)
- if __name__ == '__main__':
- main()
- # vim: set expandtab tabstop=4 shiftwidth=4 smartindent noai colorcolumn=80:
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement