#!/usr/bin/env python3 # -*- coding: utf-8 -*- ############################################################################## # epgdiff -- MythTV EPG differences utility # # Lists the programmes in the EPG data that are present in future EPG data but # not in the past EPG data. # # Author: J S Worthington # Created: 2021-10-24 ############################################################################## import argparse import asyncio import datetime import lxml import MythTV import os import socket import stat import struct import subprocess import sys import time import traceback from dataclasses import dataclass from enum import IntEnum from pprint import pprint from typing import Dict, List import MythTV.services_api.send as api import MythTV.services_api.utilities as util ############################################################################## # Program exit codes. ############################################################################## class ExitCode(IntEnum): OK = 0 MessageAndExit = 1 MissingPackageOrProgram = 2 WindowingProblem = 3 FrontendProblem = 4 LircProblem = 5 ############################################################################## # Startup - check for packages and programs. ############################################################################## program_name = os.path.basename(sys.argv[0]) if subprocess.call(['which', 'xdotool'], stdout=subprocess.DEVNULL) != 0: print('Executable "xdotool" is missing. Please install it.') print('In Ubuntu, use "apt install xdotool".') exit(ExitCode.MissingPackageOrProgram) ############################################################################## # # Version: 0.1 2021-10-26 # - Text output only. # Version: 0.2 2021-10-26 # - Playing with the frontend API to see if automation of selecting a # programme is possible. # Version: 0.3 2021-11-02 # - Fixed problem with processing the upcoming recordings. # - Added -G GUI option. # Version: 0.4 2021-11-12 # - Added lirc support. # - Fixed window scrolling to keep lines visible at the top and bottom. # Version: 0.5 2021-11-16 # - Added lirc manager mode to be used to switch off "mythtv" lirc clients # when the epgdiff window is on top, and to switch off "epgdiff" lirc # clients when they are not the top window. # Version: 0.6 2021-11-25 # - First working version with GUI and lirc. # Version: 0.7 2021-12-06 # - Make debug log file work when not running as root. # - Default debug output to stdout, so that it will be captured by the # irexec journal when epgdiff is run from irexec. # - Handle unable to connect to mythfrontend. # - Handle window closed externally. # - Handle frontend socket closed (mythfrontend not running now). # Version 0.8 2021-12-27 # - Fix config.xml handling. # Version 0.9 2022-12-14 # - Fix help text for channums. # Version 0.10 2023-04-23 # - Fix for newer, faster PC messing up timings for mythfrontend commands. # Version 0.11 2023-12-23 # - Minor window settings adjustments. # - Add SoHo channel to default list. # - Change .format() to f'' strings. # Version 0.12 2024-04-29 # - Fix a new TypeError traceback caused by MythTV upgrade to v34 - # Dvr/GetUpcomingList now returns Channel.ChanId as an int instead of an # str. # - Remove deleted channel "Jones! Too" (4208) # ############################################################################## VERSION = '0.12' ############################################################################## # Configuration ############################################################################## # Set this to True get debug output. #DEBUG_OUTPUT: bool = True DEBUG_OUTPUT: bool = False # Set this to send debug output to a separate debug log file. #DEBUG_FILE: bool = True DEBUG_FILE: bool = False # Location of default config.xml file. CONFIG_XML: str = '/etc/mythtv/config.xml' # The IP address of the mythbackend to talk to. Default: 127.0.0.1 HOST: str = '127.0.0.1' # The address of the frontend. FE_HOST: str = 'localhost' # The frontend port number. FE_PORT: int = 6546 # List of channel numbers (channel.channum) to do EPG diffs on. DEFAULT_CHANNUM_LIST: List[int] = [19, 4005, 4009, 4010, 4018, 4071, 4075, 4076, 4083, 4210, 4008] #DEFAULT_CHANNUM_LIST: List[int] = [] # List of chanids (channel.chanid) to do EPG diffs on. DEFAULT_CHANID_LIST: List[int] = [] # Default X display to connect to. DEFAULT_DISPLAY: str = ':0' # Window height constraints. # Warning: MIN_WINDOW_HEIGHT must be > 2*KEEP_ON_DISPLAY MIN_WINDOW_HEIGHT: int = 7 MAX_WINDOW_HEIGHT: int = 32 # Window width constraints. MIN_WINDOW_WIDTH: int = 30 MAX_WINDOW_WIDTH: int = 200 # Keep this number of lines of text on display above and below the current # selected line, if possible. KEEP_ON_DISPLAY: int = 3 # Number of lines to move when the PgUp or PgDn keys are used. PG_KEY_DISTANCE: int = 10 # Lirc socket to create and manage when running in lirc_manager mode and the # lirc socket to connect to in window mode. LIRC_SOCKET: str = '/dev/lircd' # The lirc source socket to connect to when running in lirc_manager mode. # This should be the same socket created by lircd, as configured in # /etc/lirc/lirc_options.conf in the "output=" setting, or an alias or link # to that socket. LIRC_SOURCE: str = '/run/lirc/lircd' # List of locations to try to write the debug log file to. These locations # are tried in order, and if they all fail, no debug log file is written - the # debug output will be sent to sysout. DEBUG_DIRS: dict[str] = ['/var/log/mythtv', '/var/log', '/tmp', '.'] # Delay time to use before checking mythfrontend has moved to the new location. # There is a problem with mythfrontend - when a key command is sent to it, it # takes some time to respond and do the screen update. Until the screen update # happens, the a query to mythfrontend for its current location will return # the old location. So a delay is needed before checking the location. The # faster the machine mythfrontend is running on, the longer the delay needed. # If epgdiff exists with a "wrong location" error, increase the DELAY time. # If it takes too long for epgdiff to navigate to the correct mythfrontend # screen, you can try reducing the DELAY time until "wrong location" errors # occur, then increase it a bit until the "wrong location" errors stop # happening. Units: Seconds DELAY: float = 0.1 ############################################################################## # Constants ############################################################################## # Programme line prefix PLP: str = ' ' # Length of the programme line prefix. PLPL: int = len(PLP) # Window/taskbar icon (.png encoded as base64) icon = b'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' ############################################################################## # Debug output. ############################################################################## if DEBUG_OUTPUT: global epgdiff_debug epgdiff_debug = None global previous_end previous_end = '\n' def dprint_init() -> None: global epgdiff_debug if epgdiff_debug == None: epgdiff_debug = sys.stdout if DEBUG_FILE: for debug_dir in DEBUG_DIRS: try: epgdiff_debug = open(debug_dir + '/' + program_name + '-debug.log', 'w', 1) except: continue break dprint(program_name + ' debug output started') def dprint(*args, end='\n', **kwargs) -> None: global epgdiff_debug global previous_end if epgdiff_debug != None: if previous_end == '\n': print(datetime.datetime.now().strftime('%H:%M:%S.%f')[:-3] + ' ', *args, **kwargs, file=epgdiff_debug) else: print(*args, end=end, file=epgdiff_debug, **kwargs) previous_end = end def dpprint(*args, **kwargs) -> None: pprint(*args, **kwargs, stream=epgdiff_debug) else: def dprint_init() -> None: pass def dprint(*args, end='\n', **kwargs) -> None: pass def dpprint(*args, **kwargs) -> None: pass dprint_init() ############################################################################## # Recording status. ############################################################################## class RecStatus(IntEnum): Pending = -15 Failing = -14 #OtherRecording = -13 (obsolete) #OtherTuning = -12 (obsolete) MissedFuture = -11 Tuning = -10 Failed = -9 TunerBusy = -8 LowDiskSpace = -7 Cancelled = -6 Missed = -5 Aborted = -4 Recorded = -3 Recording = -2 WillRecord = -1 Unknown = 0 DontRecord = 1 PreviousRecording = 2 CurrentRecording = 3 EarlierShowing = 4 TooManyRecordings = 5 NotListed = 6 Conflict = 7 LaterShowing = 8 Repeat = 9 Inactive = 10 NeverRecord = 11 Offline = 12 #OtherShowing = 13 (obsolete) ############################################################################## # Status of a title in the upcoming recordings list. ############################################################################## class RecStatus2(IntEnum): WillNotRecord = 0 WillRecord = 1 PreviouslyRecorded = 2 LaterShowing = 3 Conflict = 4 NotListed = 5 BadRecording = 6 def __str__(self): return ' RPLCNB'[int(self)] def map_RecStatus(status: RecStatus) -> RecStatus2: if status == RecStatus.Conflict: return RecStatus2.Conflict elif status == RecStatus.LaterShowing: return RecStatus2.LaterShowing elif status == RecStatus.NotListed: return RecStatus2.NotListed elif int(status) <= RecStatus.Aborted: return RecStatus2.BadRecording elif status == RecStatus.Recording or status == RecStatus.WillRecord: return RecStatus2.WillRecord elif status == RecStatus.CurrentRecording or status == RecStatus.PreviousRecording: return RecStatus2.PreviouslyRecorded else: return RecStatus2.WillNotRecord def update_RecStatus(status: RecStatus, old_status:RecStatus2) -> RecStatus2: status2 = map_RecStatus(status) if status2 > old_status: return status2 else: return old_status ############################################################################## # Hex id() ############################################################################## def hexid(obj): return hex(id(obj)) ############################################################################## # Read config.xml file to get database settings only (other settings ignored). ############################################################################## class ConfigXml: DBHostName = 'localhost' DBUsername = 'mythtv' DBPassword = 'mythtv' DBName = 'mythconverg' DBPort = 3306 _conf_trans = { 'Host':'DBHostName', 'UserName':'DBUserName', 'Password':'DBPassword', 'DatabaseName':'DBName', 'Port':'DBPort' } def readXML(self, filename): if not os.access(filename, os.R_OK): dprint('File ' + filename + ' not accessible!') return False try: config = lxml.etree.parse(filename) for child in config.xpath('/Configuration/Database')[0].getchildren(): if child.tag in self._conf_trans: #dprint('child.tag=' + str(child.tag)) setattr(self, self._conf_trans[child.tag], child.text) except Exception as e: dprint(str(e)) dprint('lxml.etree.parse failed') return False return True ############################################################################## # Backend class. Handles communication with a backend and database selected # using a config.xml file. If no config.xml file is provided, the local # database and backend are used. ############################################################################## class Backend(): def __init__(self, config_xml='') -> None: dprint('config_xml="' + config_xml + '"') self.config_xml = config_xml if self.config_xml != '': self.remote_config = ConfigXml() dprint(f'{self.remote_config=}') if not self.remote_config.readXML(self.config_xml): dprint('Failed to read or correctly parse ' + self.config_xml + ' file, using ' + HOST) self.config_xml = '' self.host = HOST # dprint(str(self.remote_config.DBHostName)) # dprint(str(self.remote_config.DBUsername)) # dprint(str(self.remote_config.DBPassword)) # dprint(str(self.remote_config.DBName)) # dprint(str(self.remote_config.DBPort)) self.host = self.remote_config.DBHostName dprint('self.host = ' + self.host) else: self.host = HOST self.backend = api.Send(self.host) # Initialise the UTCOffset so that the conversion to local time works. api.Send(self.host) util.get_utc_offset(self.backend) # Connect to the database. if self.config_xml != '': try: self.db = MythTV.MythDB( DBHostName = self.remote_config.DBHostName, DBUsername = self.remote_config.DBUsername, DBPassword = self.remote_config.DBPassword, DBName = self.remote_config.DBName, DBPort = self.remote_config.DBPort ) except MythTV.exceptions.MythDBError as e: if e.ecode == MythTV.MythError.DB_SCHEMAMISMATCH: dprint('Schema mismatch connecting to ' + self.host + ' database, trying again with schema ' + str(e.remote)) MythTV.MythDB._schema_local = e.remote self.db = MythTV.MythDB( DBHostName = self.remote_config.DBHostName, DBUsername = self.remote_config.DBUsername, DBPassword = self.remote_config.DBPassword, DBName = self.remote_config.DBName, DBPort = self.remote_config.DBPort ) dprint('After retry') else: raise MythTV.MythSchema._schema_local = MythTV.SCHEMA_VERSION else: self.db = MythTV.MythDB() self.dbc = self.db.cursor() def get_upcoming_recordings(self) -> List[str]: try: resp_dict = self.backend.send(endpoint='Dvr/GetUpcomingList?ShowAll=True') except: dprint(self.host + ' get_upcoming_recordings(): Exception') return [] if list(resp_dict.keys())[0] in ['Abort', 'Warning']: #sys.exit(f'\n{list(resp_dict.values())[0]}\n') dprint(self.host + ' get_upcoming_recordings(): Abort or Warning') dprint(str(resp_dict)) progs = [] else: progs = resp_dict['ProgramList']['Programs'] return progs ############################################################################## # If epgdiff is already running and displaying a window, activate it and # exit. ############################################################################## def activate_existing_epgdiff_window() -> None: result = subprocess.run(['xdotool', 'search', '--name', program_name], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) if result.returncode != 0: dprint('Existing epgdiff window not found') return window_id = int(result.stdout) dprint(f'{window_id=}') result = subprocess.run(['xdotool', 'windowactivate', str(window_id)]) if result.returncode != 0: print('Unable to switch to existing epgdiff window, exiting.') exit(ExitCode.WindowingProblem) print('Existing epgdiff window activated, exiting.') exit(ExitCode.OK) ############################################################################## # Activate the mythfrontend window so that it will receive typed characters. ############################################################################## def activate_mythfrontend_window() -> None: result = subprocess.run(['xdotool', 'search', '--name', 'MythTV Frontend'], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) if result.returncode != 0: print('Mythfrontend does not appear to be running, exiting.') exit(ExitCode.WindowingProblem) window_id = int(result.stdout.split(b'\n')[0]) dprint(f'{window_id=}') result = subprocess.run(['xdotool', 'windowactivate', str(window_id)]) if result.returncode != 0: print('Unable to switch to mythfrontend window, exiting.') exit(ExitCode.WindowingProblem) ############################################################################## # Main ############################################################################## parser = argparse.ArgumentParser( description='MythTV EPG Diff (Version: ' + VERSION + ')', formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=15) ) parser.add_argument('-A', '--all-channels', dest='all_channels', default=False, action='store_true', help='Ignore chanid and channum lists and diff all chanids that have EPG data') parser.add_argument('-V', '--version', dest='version', default=False, action='store_true', help='Display the version number and exit') parser.add_argument('-G', '--gui', dest='gui', default=False, action='store_true', help='Use the GUI interface') parser.add_argument('-M', '--lirc-manager', type=str, dest='lirc_manager', default='', action='store', help='Run as a daemon managing the lirc connections to ' + LIRC_SOCKET + '. Requires the username for the user running the X session for MythTV') parser.add_argument('-a', '--all', dest='all', default=False, action='store_true', help='Display all new programmes (including those that match recording rules)' + ' - also displays recording status for programmes that match recording rules' ) parser.add_argument('-D', '--delay', type=float, dest='delay', default=DELAY, action='store', help='Set the delay time between key commands to mythfrontend. See the DELAY value in the Configuration section of the program code for more explanation.') parser.add_argument('-d', '--dump-upcoming', dest='dump_upcoming', default=False, action='store_true', help='Dump the upcoming recordings data (debug option)') parser.add_argument('-c', '--config', type=str, action='store', default=CONFIG_XML, help='Location of MythTV config.xml file (default: ' + CONFIG_XML + ')') parser.add_argument('-f', '--fast', dest='fast', default=False, action='store_true', help='Do not fetch the upcoming recordings list, which takes a long time. ' + 'This will cause all new programmes to be listed, regardless of their matching existing recording rules.' ) parser.add_argument('-i', '--chanids', nargs='+', type=int, dest='chanids', action='store', default=[], help='Space separated list of chanid values. Can be used with -n') parser.add_argument('-l', '--lirc', dest='lirc', default=False, action='store_true', help='Use lirc (infrared remote control') parser.add_argument('-n', '--channums', nargs='+', type=int, dest='channums', action='store', default=[], help='Space separated list of channum values. Can be used with -i') parser.add_argument('-t', '--themes', dest='themes', default=False, action='store_true', help='Try out themes (debug option)') args = parser.parse_args() if args.version: print('Version '+VERSION) exit(ExitCode.MessageAndExit) if os.environ.get('DISPLAY') == None: os.environ['DISPLAY'] = DEFAULT_DISPLAY if args.lirc_manager != '': try: import psutil except ImportError: print('Please install the Python 3 psutil module. On some systems this will be available as a system package.') print('On Ubuntu, use "apt install python3-psutil". Otherwise try "pip3 install psutil".') exit(ExitCode.MissingPackageOrProgram) # Get the window ID of the currently active window. async def active_window_id(): proc = await asyncio.create_subprocess_shell('sudo -u ' + args.lirc_manager + ' xdotool getactivewindow', shell=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) result, result2 = await proc.communicate() #dprint(f'{dir(proc)=}') dprint(f'active_window_id: {proc.returncode=} {result=}') if proc.returncode != 0: print('Unable to get the currently active window number, exiting') return None dprint('active_window_id: return next') return int(result) class LircManager: class LircClient: async def reader_writer(self, direction, reader, writer, manager) -> None: # The 'direction' parameter is only used for debug output. dprint(f'reader_writer {hexid(self)} {direction}: start') dprint(f'reader_writer {hexid(self)} {direction}: {hexid(manager.epgdiff_clients_connected)=}') while True: dprint(f'reader_writer {hexid(self)} {direction}: before reader.readline()') try: line = await reader.readline() except Exception as e: print(traceback.format_exc()) break dprint(f'reader_writer {hexid(self)} {direction}: {line=} {manager.epgdiff_clients_connected=}') dprint(f'reader_writer {hexid(self)} {direction}: before at_eof()') if reader.at_eof(): dprint(f'reader_writer {hexid(self)} {direction}: at_eof()') break window_id = await active_window_id() dprint(f'reader_writer {hexid(self)} {direction}: {self.is_epgdiff=} {self.window_id=} {window_id=}') if window_id == None: print('Unable to obtain the active window ID, exiting.') break if manager.epgdiff_clients_connected == 0 or \ (self.is_epgdiff and self.window_id == window_id and manager.epgdiff_window_id == window_id) or \ (not self.is_epgdiff and window_id != manager.epgdiff_window_id): writer.write(line) dprint(f'reader_writer {hexid(self)} {direction}: wrote {line=}') else: dprint(f'reader_writer {hexid(self)} {direction}: discarded {line=}') dprint(f'reader_writer {hexid(self)} {direction}: end of loop') dprint(f'reader_writer {hexid(self)} {direction}: cleanup') await writer.drain() writer.close() self.close(manager) dprint(f'reader_writer {hexid(self)} {direction}: end') def __init__(self, client_reader, client_writer, manager): dprint(f'LircClient {hexid(self)} __init__: {hexid(manager.epgdiff_clients_connected)=}') self.closed = False self.client_reader = client_reader self.client_writer = client_writer self.window_id = None pid, uid, gid = struct.unpack('3i', client_reader._transport._sock.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize('3i'))) cmdline = psutil.Process(pid).cmdline() if cmdline[0][:6] == 'python': client_name = os.path.basename(cmdline[1]) else: client_name = os.path.basename(cmdline[0]) self.is_epgdiff = (client_name == program_name) dprint(f'LircClient {hexid(self)}: {cmdline=} {client_name=} {self.is_epgdiff=}') if self.is_epgdiff: manager.epgdiff_clients_connected += 1 dprint(f'LircClient {hexid(self)}: {manager.epgdiff_clients_connected=}') async def init(self, manager): dprint(f'LircClient {hexid(self)} init: {manager.epgdiff_clients_connected=}') self.window_id = await active_window_id() if self.is_epgdiff: manager.epgdiff_window_id = self.window_id dprint(f'LircClient {hexid(self)} init: {manager.epgdiff_window_id=}') (self.lircd_reader, self.lircd_writer) = await asyncio.open_unix_connection(LIRC_SOURCE) self.lircd_to_client_task = asyncio.create_task( self.reader_writer('lircd_to_client', self.lircd_reader, self.client_writer, manager) ) self.client_to_lircd_task = asyncio.create_task( self.reader_writer('client_to_lircd', self.client_reader, self.lircd_writer, manager) ) def close(self, manager): dprint(f'LircClient {hexid(self)} close') if not self.closed: self.closed = True if self.is_epgdiff: dprint(f'LircClient {hexid(self)} close: {manager.epgdiff_clients_connected=}') manager.epgdiff_clients_connected -= 1 dprint(f'LircClient {hexid(self)} close: {manager.epgdiff_clients_connected=}') manager.epgdiff_window_id = None self.lircd_to_client_task.cancel() self.client_to_lircd_task.cancel() async def lirc_server(self, reader, writer) -> None: dprint(f'lirc_server {hexid(self)}: creating client') client = self.LircClient(reader, writer, self) self.client_list.append(client) await client.init(self) dprint(f'lirc_server {hexid(self)}: client={hexid(client)}') def __init__(self): print(program_name + ': Running in lirc_manager mode') self.client_list = [] self.epgdiff_clients_connected = 0 self.epgdiff_window_id = None if os.path.exists(LIRC_SOCKET): os.remove(LIRC_SOCKET) loop = asyncio.get_event_loop() self.skt = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.skt.bind(LIRC_SOCKET) os.chmod(LIRC_SOCKET, stat.S_IRUSR|stat.S_IWUSR|stat.S_IRGRP|stat.S_IWGRP|stat.S_IROTH|stat.S_IWOTH) server = loop.run_until_complete(asyncio.start_unix_server(self.lirc_server, sock=self.skt)) try: loop.run_forever() except KeyboardInterrupt: pass # Cleanup for client in self.client_list: client.close(self) server.close() loop.run_until_complete(server.wait_closed()) loop.close() os.remove(LIRC_SOCKET) print('\n' + program_name + ': lirc_manager mode exited') exit(ExitCode.OK) LircManager()() try: import pyautogui except ImportError: print('Please install the Python 3 PyAutoGUI module. Try "pip3 install pyautogui".') print('The pyautogui module requires python3-xlib. This will be installed automatically') print('by pip3, but on many systems (including Ubuntu) it is better to install it as a') print('system package. On Ubuntu, use "apt install python3-xlib" and do that before') print('running pip3.') exit(ExitCode.MissingPackageOrProgram) try: import PySimpleGUI as sg except ImportError: print('Please install the Python 3 PySimpleGUI module. Try "pip3 install pysimplegui".') exit(ExitCode.MissingPackageOrProgram) if args.lirc: try: import lirc except ImportError: print('Unable to import the Python 3 lirc module. This is normally installed as part of the main lirc package - is the main lirc package installed?.') exit(ExitCode.MissingPackageOrProgram) if args.themes: # Choose a Theme for the Layout sg.theme('DarkBlue16') layout = [[sg.Text('List of InBuilt Themes')], [sg.Text('Please Choose a Theme to see Demo window')], [sg.Listbox(values = sg.theme_list(), size =(20, 12), key ='-LIST-', enable_events = True)], [sg.Button('Exit')]] window = sg.Window('Theme List', layout) # This is an Event Loop while True: event, values = window.read() if event in (None, 'Exit'): break sg.theme(values['-LIST-'][0]) sg.popup_get_text(f"This is {values['-LIST-'][0]}") # Close window.close() exit(ExitCode.OK) # If there is an exisiting epgdiff window, activate it and exit. activate_existing_epgdiff_window() be = Backend(args.config) if not args.fast: upcoming = be.get_upcoming_recordings() rec_status: Dict[str, int] = {} if upcoming==[]: print('No upcoming recordings') elif args.dump_upcoming: print('Upcoming recordings = ') dpprint(upcoming) rec_status_count = {} for recording in upcoming: status = int(recording['Recording']['Status']) title = str(recording['Channel']['ChanId']) + '|' + recording['Title'] count = rec_status_count.get(status) if count == None: rec_status_count[status] = 1 else: rec_status_count[status] = count + 1 old_status = rec_status.get(title) if old_status == None: rec_status[title] = map_RecStatus(RecStatus(status)) else: rec_status[title] = update_RecStatus(RecStatus(status), old_status) if args.dump_upcoming: for key, value in rec_status_count.items(): dprint(key, RecStatus(int(key)), value) channum_chanids = [] if args.all_channels: args.chanids = [] rows = be.dbc.execute("SELECT DISTINCT chanid FROM program ORDER BY chanid;") for chanid_row in be.dbc: args.chanids.append(chanid_row[0]) else: if args.chanids == [] and args.channums == []: args.channums = DEFAULT_CHANNUM_LIST args.chanids = DEFAULT_CHANID_LIST for channum in args.channums: rows = be.dbc.execute(f'SELECT chanid FROM channel WHERE channum={channum};') for chanid_row in be.dbc: channum_chanids.append(chanid_row[0]) @dataclass class Channel: @dataclass class Programme: title: str status: str chanid: int channum: str callsign: str name: str programmes: List[Programme] channels: List[Channel] = [] for chanid in channum_chanids + args.chanids: rows = be.dbc.execute(f'SELECT chanid,channum,callsign,name FROM channel WHERE chanid={chanid};') row = be.dbc.fetchone() if row == None: dprint('Invalid chanid: ' + str(chanid)) else: channel = Channel(int(row[0]), row[1], row[2], row[3], []) #dprint(channel.chanid, '-', channel.channum, '-', channel.callsign, '-', channel.name) sql = f""" select distinct title from program where chanid={chanid} and starttime >= utc_timestamp() and title not in (select distinct title from program where chanid={chanid} and starttime < utc_timestamp()) order by title; """ rows = be.dbc.execute(sql) for fetched in be.dbc: title = fetched[0] if args.fast: dprint(PLP + title) channel.programmes.append(Channel.Programme(title, '')) else: found_status = rec_status.get(str(chanid) + '|' + title, RecStatus2.WillNotRecord) if args.all or found_status == RecStatus2.WillNotRecord: channel.programmes.append(Channel.Programme(title, str(found_status))) channels.append(channel) output = [] for channel in channels: output.append(str(str(channel.chanid) + ' - ' + channel.channum + ' - ' + channel.callsign + ' - ' + channel.name)) for programme in channel.programmes: if args.fast: output.append(PLP + programme.title) else: if str(programme.status) == ' ': output.append(PLP + programme.title) else: output.append(PLP + programme.title + " '" + programme.status + "'") if args.gui: # Modified version of AsyncConnection from the lirc async_client.py file. # On closing, puts None in the queue to allow the calling module to know # that the connection is closed and it can shut down. from lirc.client import AbstractConnection as AbstractConnection class AsyncConnectionModified(object): ''' Asynchronous read interface on top of an AbstractConnection. Parameters: - connection: Typically a lirc.RawConnection or lirc.LircdConnection. - loop: AbstractEventLoop, typically obtained using asyncio.get_event_loop(). ''' def __init__(self, connection: AbstractConnection, loop: asyncio.AbstractEventLoop): def read_from_fd(): ''' Read data from the connection fd and put into queue. ''' line = self._conn.readline(0) if line: asyncio.ensure_future(self._queue.put(line)) self._conn = connection self._loop = loop self._queue = asyncio.Queue() self._loop.add_reader(self._conn.fileno(), read_from_fd) async def close(self): ''' Clean up loop and the base connection. ''' self._loop.remove_reader(self._conn.fileno()) await self._queue.put(None) async def readline(self) -> str: ''' Asynchronous get next line from the connection. ''' return await self._queue.get() def __aiter__(self): ''' Return async iterator. ''' return self async def __anext__(self): ''' Implement async iterator.next(). ''' return await self._queue.get() async def __aenter__(self): ''' Implement "async with". ''' return self async def __aexit__(self, exc_type, exc, traceback): ''' Implement exit from "async with". ''' self.close() class GUI: content: str line_no: int line_count: int width: int height: int first_programme: str window: sg.Window ir_q: asyncio.Queue try: fe = MythTV.Frontend(FE_HOST, FE_PORT) except MythTV.exceptions.MythFEError: print('Unable to connect to mythfrontend') exit(ExitCode.FrontendProblem) # The MythTV Python bindings have a bug - MythTV.MythError.CLOSEDSOCKET # is undefined, but is attempted to be raised when a socket closed # error happens. So instead an AttributeError occurs. The following # code has a workaround until this is fixed. The fix should instead # be raising MythTV.MythError.SOCKET, so this code will work with both # that and AttributeError. def do_jump(self, jumppoint, expected_location) -> None: dprint('jumppoint: ' + jumppoint) try: result = self.fe.jump[jumppoint] except Exception as e: if type(e) == MythTV.exceptions.MythError or type(e) == AttributeError: result = False else: raise if not result: print(' Jump failed') exit(ExitCode.FrontendProblem) actual_location = self.fe.sendQuery('location') dprint(f' location: {actual_location}') if actual_location != expected_location: print(' Wrong location') exit(ExitCode.FrontendProblem) def do_key(self, key, expected_location) -> None: dprint('key: ' + str(key)) try: result = self.fe.key[key] except Exception as e: if type(e) == MythTV.exceptions.MythError or type(e) == AttributeError: result = False else: raise if not result: print(' Key failed') exit(ExitCode.FrontendProblem) time.sleep(args.delay) actual_location = self.fe.sendQuery('location') dprint(f' location: {actual_location}') if actual_location != expected_location: print(' Wrong location') exit(ExitCode.FrontendProblem) def gotoTitleSearch(self) -> None: self.do_jump('mainmenu', 'mainmenu') self.do_key('down', 'mainmenu') self.do_key('right', 'manage_recordings.xml') self.do_key('right', 'tv_schedule.xml') self.do_key('down', 'tv_schedule.xml') self.do_key('down', 'tv_schedule.xml') self.do_key('right', 'tv_search.xml') self.do_key('right', 'ProgLister') self.do_key('right', 'ProgLister') def search_for_title(self, title) -> None: dprint(f'search_for_title: {title=}') self.gotoTitleSearch() activate_mythfrontend_window() subprocess.run(['wmctrl', '-a "MythTV Frontend"']) pyautogui.write(title) self.do_key('down', 'ProgLister') self.do_key('enter', 'ProgLister') class Direction(IntEnum): Up = -1 Down = 1 # Is line_no outside the limits of the window? def outside_limits(self, line_no: int, direction: Direction) -> bool: if direction == self.Direction.Up: if line_no < 1: return True else: return False else: if line_no >= self.line_count: return True else: return False # Is a line of text a programme line (does it have a programme line prefix at # the start of the line)? def is_programme_line(self, line_no: int) -> bool: return self.content[line_no][0:PLPL] == PLP # Move which line is selected in the window. def move_selection(self, direction: Direction, distance: int = 1) -> None: indexes = self.window['selected'].get_indexes() dprint(f'{indexes=}') index_valid = True try: self.line_no = indexes[0] except IndexError: index_valid = False if index_valid: new_line_no = self.line_no + int(direction) * distance while True: if self.outside_limits(new_line_no, direction): if direction == self.Direction.Up: new_line_no = 1 direction = self.Direction.Down else: new_line_no = self.line_count - 1 direction = self.Direction.Up if self.is_programme_line(new_line_no): break new_line_no += int(direction) if new_line_no != self.line_no: if new_line_no < self.first_displayed_line: # Scroll to keep the selected line visible. new_first_displayed_line = self.first_displayed_line - (self.line_no - new_line_no) if new_first_displayed_line < 0: new_first_displayed_line = 0 self.first_displayed_line = new_first_displayed_line elif new_line_no >= self.first_displayed_line + self.height: # Scroll to keep the selected line visible. new_first_displayed_line = self.first_displayed_line + (new_line_no - self.line_no) if new_first_displayed_line > self.line_count - self.height: new_first_displayed_line = self.line_count - self.height self.first_displayed_line = new_first_displayed_line self.line_no = new_line_no dprint(f'1: {self.line_no=}, {self.first_displayed_line=}') distance = self.line_no - self.first_displayed_line dprint(f'Top: {distance=}') if distance <= KEEP_ON_DISPLAY: self.first_displayed_line -= KEEP_ON_DISPLAY - distance if self.first_displayed_line < 0: self.first_displayed_line = 0 else: distance = self.height - (self.line_no - self.first_displayed_line) dprint(f'Bottom: {distance=} {self.height=}') if distance <= KEEP_ON_DISPLAY: self.first_displayed_line += KEEP_ON_DISPLAY - distance + 1 dprint(f'2: {self.line_no=}, {self.first_displayed_line=}') self.window['selected'].update(scroll_to_index = self.first_displayed_line, set_to_index = self.line_no) dprint(f'{self.line_no=} {self.first_displayed_line=}') def __init__(self, content): self.content = content self.line_count = len(output) self.first_displayed_line = 0 self.first_programme = '' self.width = MIN_WINDOW_WIDTH if self.line_count > MAX_WINDOW_HEIGHT: self.height = MAX_WINDOW_HEIGHT else: self.height = self.line_count if self.height < MIN_WINDOW_HEIGHT: self.height = MIN_WINDOW_HEIGHT self.line_no = 0 for line in self.content: lline = len(line) if lline > self.width: self.width = lline if self.first_programme == '': if self.is_programme_line(self.line_no): self.first_programme = line else: self.line_no += 1 if self.width > MAX_WINDOW_WIDTH: self.width = MAX_WINDOW_WIDTH #sg.theme('DarkBlue16') layout = [ [sg.Listbox( values=self.content, default_values = [self.first_programme], enable_events=True, select_mode = sg.LISTBOX_SELECT_MODE_SINGLE, bind_return_key = True, key='selected', size=(self.width, self.height), )], [sg.Button('Exit')] ] self.window = sg.Window( 'MythTV epgdiff', layout, icon=icon, font=('Helvetica', 20), return_keyboard_events=True, finalize=True ) #dpprint(dir(self.window)) dprint(f'{self.first_programme=}') #print = sg.Print self.ir_q = asyncio.Queue() if args.lirc: try: self.ir = lirc.LircdConnection(program_name, socket_path=LIRC_SOCKET) except RuntimeError: print('Cannot open lircd socket ' + LIRC_SOCKET) exit(ExitCode.LircProblem) else: self.ir = None async def sg_event_loop(self) -> None: while True: if self.ir_q.qsize() != 0: event = await self.ir_q.get() values = None dprint(f'lirc event={event}') else: event, values = self.window.read(0) if event == None: event = 'Exit' if event == sg.TIMEOUT_KEY: await asyncio.sleep(0.01) else: dprint(f'{event=} {values=}') for prefix in ['KP_', 'MouseWheel:']: if event.startswith(prefix): event = event[len(prefix):] event = event.rstrip('0123456789:') if event in (sg.WIN_CLOSED, 'Exit', 'Escape'): break elif event == 'Up': self.move_selection(self.Direction.Up) elif event == 'Down': self.move_selection(self.Direction.Down) elif event == 'Next': # PgDn keys self.move_selection(self.Direction.Down, PG_KEY_DISTANCE) elif event == 'Prior': # PgUp keys self.move_selection(self.Direction.Up, PG_KEY_DISTANCE) elif event == 'selected': self.move_selection(self.Direction.Down, 0) elif event == "Enter" or event == 'Return': dprint(event) self.search_for_title(self.content[self.line_no][PLPL:]) if self.ir != None: await self.conn.close() dprint('ir closed') self.window.close() if values == None: dprint('values: None') else: dprint('values:', values['selected']) if args.lirc: async def get_lines(self): self.conn = AsyncConnectionModified(self.ir, asyncio.get_event_loop()) while True: keypress = await self.conn.readline() await self.ir_q.put(keypress) dprint(f'{keypress=}') if keypress == None or keypress == 'Escape': break await self.conn.close() self.ir.close() self.ir = None dprint('get_lines finished') async def run(self): self.sg_event_loop_task = asyncio.create_task(self.sg_event_loop()) if self.ir != None: self.get_lines_task = asyncio.create_task(self.get_lines()) await asyncio.gather(self.get_lines_task, self.sg_event_loop_task) else: await asyncio.gather(self.sg_event_loop_task) dprint('After gather') def event_loop_exception_handler(loop, context): e = context.get('exception') self.sg_event_loop_task.cancel() if self.ir != None: self.get_lines_task.cancel() if e != None: dprint(f'event_loop_exception_handler: {e=}') raise e def main(self): loop = asyncio.get_event_loop() loop.set_exception_handler(self.event_loop_exception_handler) asyncio.run(self.run()) w = GUI(output) w.main() else: for line in output: print(line) exit(ExitCode.OK)