#!/usr/bin/env python3 # -*- coding: utf-8 -*- ############################################################################## # clear-damaged-flag # # When a MythTV recording is detected to be damaged, mythbackend sets the # recordedprogram.videoprop.DAMAGED flag. If the damage is a late start or # early finish, but in fact the entire programme has been properly recorded, # it is helpful to be able to clear the DAMAGED status of the recording so # so that it will not show up in the listings as damaged. Clearing the # DAMAGED flag should also prevent the programme from being automatically # re-recorded. # # Author: J S Worthington # ############################################################################## import argparse import logging import logging.handlers import MythTV import os import sys extra_help = ''' Notes: 1) This program has only been tested on MythTV v33. ''' ############################################################################## # Version 0.1 2023-04-01 # Initial version. # Version 0.2 2024-01-11 # Add set damaged flag option. ############################################################################## VERSION = '0.2' PROGRAM_NAME = os.path.basename(sys.argv[0]) ############################################################################## # Configuration ############################################################################## # Default logging level. Log level used for output to the log file. LOGGING_LEVEL = 'none' #LOGGING_LEVEL = 'debug' #LOGGING_LEVEL = 'info' # Default console logging level. Logging level used for output to the console. CONSOLE_LEVEL = 'info' ############################################################################## # Convert a loglevel string to a loglevel value. # # No error checking is done as the 'level' value is assumed to have been # created using argparse from a valid list of levels. ############################################################################## def _str_to_loglevel(level): return { 'none': logging.NOTSET, 'debug': logging.DEBUG, 'info': logging.INFO, 'warning': logging.WARNING, 'error': logging.ERROR, 'critical': logging.CRITICAL }[level] ############################################################################## # mythtimeformat - Convert returned time value to MythTV format. ############################################################################## def mythtimeformat(dbtime): mythtime = dbtime.isoformat() if mythtime[10:11] == 'T': mythtime = mythtime[0:10] + ' ' + mythtime[11:] return mythtime ############################################################################## # Main ############################################################################## class SmartFormatter(argparse.RawDescriptionHelpFormatter): def _split_lines(self, text, width): if text.startswith('R|'): return text[2:].splitlines(False) return argparse.RawDescriptionHelpFormatter._split_lines(self, text, width) parser = argparse.ArgumentParser( description='Delete MythTV database entries for recordings with missing files and optionally re-record (Version: ' + VERSION + ')', formatter_class=lambda prog: SmartFormatter(prog, max_help_position=15, width=80), epilog = extra_help ) parser.add_argument('recordedid', type=int, nargs='?', default=None, help='The recordedid of the recording to have the DAMAGED flag removed'); parser.add_argument('-c', '--consolelevel', default=CONSOLE_LEVEL, choices=['debug', 'info', 'warning', 'error', 'critical'], help='Sets the console logging level (default: ' + CONSOLE_LEVEL + ')') parser.add_argument('--host', default='localhost', dest='host', action='store', help='Set the host address of the MythTV backend server. Default=localhost. IP address or DNS name. IPv6 allowed.') parser.add_argument('-l', '--loglevel', default=LOGGING_LEVEL, choices=['none', 'debug', 'info', 'warning', 'error', 'critical'], help = 'Sets the logging level in the log file (default: ' + LOGGING_LEVEL + ')') parser.add_argument('-s', '--set', dest='set_damaged', action='store_true', default=False, help = 'Instead of clearing the DAMAGED flag, set the DAMAGED flag') parser.add_argument('-v', '-V', '--version', dest='display_version', default=False, action='store_true', help='Display the program name and version and exit') args = parser.parse_args() if args.display_version: print(PROGRAM_NAME + ' version ' + VERSION) exit(0) if PROGRAM_NAME == 'set-damaged-flag.py': args.set_damaged = True loglevel = _str_to_loglevel(args.loglevel) consolelevel = _str_to_loglevel(args.consolelevel) # Initialise logging. logger = logging.getLogger(PROGRAM_NAME) logger.setLevel(min(loglevel, consolelevel)) if loglevel != logging.NOTSET: # Log file for all output messages. logfile = PROGRAM_NAME + '.log' try: fh = logging.FileHandler( logfile, mode='w' ) except IOError as e: if e.errno == 13: print('Exception opening log file ' + logfile + ' (errno 13: permission denied)') else: raise exit(1) fh_formatter = logging.Formatter( '%(asctime)s %(levelname)s %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) fh.setFormatter(fh_formatter) logger.addHandler(fh) fh.setLevel(loglevel) ch = logging.StreamHandler(stream=sys.stdout) ch_formatter = logging.Formatter( '%(message)s' ) ch.setFormatter(ch_formatter) logger.addHandler(ch) ch.setLevel(consolelevel) logger.debug(f'{args=}') logger.debug(f'{loglevel=}') logger.debug(f'{consolelevel=}') logger.debug(f'{args.host=}') # Initialise the Python bindings for direct access to the database. db = MythTV.MythDB(DBHostName=args.host) dbc = db.cursor() if args.recordedid == None: logger.critical('Error - recordedid parameter is required') exit(1) logger.debug(f'Processing recordedid {args.recordedid}') sql = f'select chanid, title, starttime, progstart from recorded where recordedid={args.recordedid}' logger.debug(f'{sql=}') rows = dbc.execute(sql) if rows != 1: logger.critical(f'Error reading recording data for recordedid {args.recordedid} - check the recordedid value') exit(2) chanid, title, starttime, progstart = dbc.fetchone() starttime = mythtimeformat(starttime) progtime = mythtimeformat(progstart) sql = f'select chanid, title, starttime, videoprop, FIND_IN_SET(\'DAMAGED\', rp.videoprop) from recordedprogram rp where chanid={chanid} and starttime=\'{progstart}\'' logger.debug(f'{sql=}') rows = dbc.execute(sql) if rows != 1: logger.critical(f'Error: No recordedprogram entry matches recordedid {args.recordedid} - possible database corruption!') exit(2) chanid, title, starttime, videoprop, is_damaged = dbc.fetchone() logger.debug(f'{videoprop=}, {is_damaged=}') if args.set_damaged: if is_damaged: logger.critical(f'Error: Recording {args.recordedid} ("{title}") already has the DAMAGED flag set') exit(2) if videoprop == '': videoprop = 'DAMAGED' else: videoprop += ',DAMAGED' sql = f'update recordedprogram set videoprop=\'{videoprop}\' where chanid={chanid} and starttime=\'{starttime}\'' logger.debug(f'{sql=}') rows = dbc.execute(sql) logger.debug(f'{rows=}') if rows != 1: logger.critical(f'Error: Setting of the DAMAGED flag failed for recordedid {args.recordedid}') exit(2) logger.info(f'Set the DAMAGED flag for recordedid {args.recordedid} ("{title}") chanid={chanid} starttime=\'{starttime}\'') else: if not is_damaged: logger.critical(f'Error: Recording {args.recordedid} ("{title}") does not have the DAMAGED flag set') exit(2) sql = f'update recordedprogram set videoprop=replace(videoprop, \'DAMAGED\', \',\') where chanid={chanid} and starttime=\'{starttime}\'' logger.debug(f'{sql=}') rows = dbc.execute(sql) logger.debug(f'{rows=}') if rows != 1: logger.critical(f'Error: Deletion of the DAMAGED flag failed for recordedid {args.recordedid}') exit(2) logger.info(f'Removed DAMAGED flag from recordedid {args.recordedid} ("{title}") chanid={chanid} starttime=\'{starttime}\'')