From 8bfa376bb5570738214f2b009e15df1938370998 Mon Sep 17 00:00:00 2001 From: Ben Winston Date: Wed, 30 Mar 2022 23:47:21 -0400 Subject: rename project roku-tui --- roku_tui.py | 218 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100755 roku_tui.py (limited to 'roku_tui.py') diff --git a/roku_tui.py b/roku_tui.py new file mode 100755 index 0000000..5312631 --- /dev/null +++ b/roku_tui.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 + +import urllib.request +import time +import curses +from configparser import ConfigParser +from pathlib import Path +import xml.etree.ElementTree as ET + +class RokuConfig: + + def __init__(self, config_file): + config = ConfigParser() + config.read(config_file) + self.ip = config['general']['roku_ip'] + +class RemoteKey: + + def __init__(self, hotkey, label, endpoint, y, x): + self.hotkey = hotkey + self.label = label + self.endpoint = endpoint + self.y = y + self.x = x + +class RokuRemote: + + def __init__(self, ip): + self.base_url = 'http://{}:8060/'.format(ip) + self.setup_keys() + + def setup_keys(self): + self.keys = {} + + # first row + self.keys['b'] = RemoteKey('b', 'Back', 'keypress/back', 1, 1) + self.keys['h'] = RemoteKey('h', 'Home', 'keypress/home', 1, 10) + + # second row + self.keys['r'] = RemoteKey('r', 'Repl', 'keypress/instantreplay', 13, 1) + self.keys['*'] = RemoteKey('*', 'Star', 'keypress/info', 13, 10) + + # arrow keys + self.keys['KEY_UP'] = RemoteKey('KEY_UP', '^', 'keypress/up', 4, 7) + self.keys['KEY_LEFT'] = RemoteKey('KEY_LEFT', '<', 'keypress/left', 7, 1) + self.keys['\n'] = RemoteKey('\n', 'OK!', 'keypress/select', 7, 6) + self.keys['KEY_RIGHT'] = RemoteKey('KEY_RIGHT', '>', 'keypress/right', 7, 13) + self.keys['KEY_DOWN'] = RemoteKey('KEY_DOWN', 'v', 'keypress/down', 10, 7) + + # third row + self.keys['<'] = RemoteKey('<', '<<', 'keypress/rev', 16, 1) + self.keys['p'] = RemoteKey('p', 'P', 'keypress/play', 16, 7) + self.keys['>'] = RemoteKey('>', '>>', 'keypress/fwd', 16, 12) + +def draw_key(stdscr, key, pressed=False): + color = 1 if not pressed else 2 + draw_rect_key(stdscr, key.y, key.x, key.label, color) + +def press_key(stdscr, key, base_url): + draw_key(stdscr, key, True) + stdscr.refresh() + request_url = base_url + key.endpoint + urllib.request.urlopen(request_url, b'') + time.sleep(0.1) + +def draw_rect_key(stdscr, y, x, text, colors): + iw = len(text) + 2 + ow = iw + 2 + + owf = "{:^" + str(ow) + "}" + iwf = "{:^" + str(iw) + "}" + + # draw the box + stdscr.addstr(y, x, owf.format('-' * iw)) + stdscr.addstr(y + 1, x, "|") + stdscr.addstr(y + 1, x + ow - 1, "|") + stdscr.addstr(y + 2, x, owf.format('-' * iw)) + + # draw the inside + if colors == 1: + stdscr.addstr(y + 1, x + 1, iwf.format(text)) + elif colors == 2: + stdscr.addstr(y + 1, x + 1, iwf.format(text), curses.A_STANDOUT) + + +def status(stdscr): + stdscr.addstr(20, 1, "~*~ roku-cli ~*~") + +def search_loop(stdscr, base_url): + # setup for search + stdscr.addstr(22, 1, '/') + min_x = 2 + cur_x = min_x + stdscr.move(22, cur_x) + curses.curs_set(True) + while 1: + stdscr.refresh() + letter = stdscr.getkey() + if letter in ('\n', 'KEY_ESC'): + stdscr.move(22, 0) + stdscr.clrtoeol() + curses.curs_set(False) + return + elif letter == '\x7f': + if cur_x > min_x: + cur_x -= 1 + stdscr.addstr(22, cur_x, " ") + stdscr.move(22, cur_x) + request_url = base_url + 'keypress/Backspace' + else: + stdscr.addstr(22, cur_x, letter) + cur_x += 1 + if letter == ' ': + letter = '%20' + request_url = base_url + 'keypress/Lit_' + letter + urllib.request.urlopen(request_url, b'') + +def help_toggle(stdscr): + + help_lines = ["'h' -> Home", + "'b' -> Back", + "'p' -> Play/Pause", + "'r' -> Replay", + "'*' -> Options (star key)", + "arrow keys -> navigation", + "ENTER -> OK", + "'<' -> Rewind", + "'>' -> Fast-Forward", + "'/' -> Toggle typing mode (ENTER or ESC to leave)", + "'?' -> Show this help screen", + "'q' -> Quit", + "", + "Press any key to return to remote"] + + stdscr.clear() + status(stdscr) + for i in range(1,len(help_lines)+1): + stdscr.addstr(i, 1, help_lines[i - 1]) + stdscr.getch() # wait for keypress + stdscr.clear() + status(stdscr) + +def remote_loop(stdscr, remote): + while 1: + # draw all the keys + for k in remote.keys: + draw_key(stdscr, remote.keys[k]) + + # listen for an input + c = stdscr.getkey() + + # if the input is in the mapped keys, press the key + if c in remote.keys: + press_key(stdscr, remote.keys[c], remote.base_url) + elif c == '/': + search_loop(stdscr, remote.base_url) + elif c == '?': + help_toggle(stdscr) + elif c == 'q': + break + +def main(stdscr, config): + + init_curses() + stdscr.clear() + + # create the remote + remote = RokuRemote(config.ip) + + status(stdscr) + + remote_loop(stdscr, remote) + +def init_curses(): + # set up colors + curses.use_default_colors() + + # clear the screen and hide the cursor + curses.curs_set(False) + +def get_device_name(base_url): + request_url = base_url + "query/device-info" + out = urllib.request.urlopen(request_url) + response = out.read().decode() + root = ET.fromstring(response) + return root.findall("model-name")[0].text + +def init_run(config_file): + print("Welcome to roku-cli!") + print("Please enter the IP address of the Roku device you wish to control.") + print("This can be found in the Settings > Network > About menu, ") + print("or via your router.") + print("You will only need to do this once.") + print("If you wish to change the IP, modify the file at ~/.config/roku/roku.config") + print() + ip = input("IP Address: ") + remote = RokuRemote(ip) + + # if this returns, the IP is valid + device_name = get_device_name(remote.base_url) + + # write the config, create the directory if necessary + if not config_file.parent.exists(): + config_file.parent.mkdir() + config = ConfigParser() + config["general"] = {"roku_ip": ip} + with open(str(config_file), 'w') as cf: + config.write(cf) + +def startup(): + config_file = Path('~/.config/roku/roku.config').expanduser() + if not config_file.exists(): + init_run(config_file) + config = RokuConfig(config_file) + curses.wrapper(main, config) + +if __name__ == '__main__': + startup() -- cgit v1.2.3