aboutsummaryrefslogtreecommitdiff
path: root/roku_tui.py
diff options
context:
space:
mode:
Diffstat (limited to 'roku_tui.py')
-rwxr-xr-xroku_tui.py218
1 files changed, 218 insertions, 0 deletions
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()