summarylogtreecommitdiffstats
path: root/rofi-twitch
diff options
context:
space:
mode:
Diffstat (limited to 'rofi-twitch')
-rwxr-xr-xrofi-twitch202
1 files changed, 202 insertions, 0 deletions
diff --git a/rofi-twitch b/rofi-twitch
new file mode 100755
index 000000000000..7a538e9a937c
--- /dev/null
+++ b/rofi-twitch
@@ -0,0 +1,202 @@
+#!/usr/bin/env python
+
+from collections import defaultdict
+import requests
+import subprocess
+import sys
+
+s = requests.session()
+s.headers['Client-ID'] = 'jnx6yo007d0ota439ikx5g8v26qzr8'
+s.headers['User-agent'] = 'rofi-twitch v1.0 (compatible)'
+
+MANY_RESULTS = 100
+FEW_RESULTS = 10
+
+CACHE = defaultdict(list)
+
+
+def rofi(*args, body=None, prompt='', msg=''):
+ if prompt:
+ prompt += ' '
+ args = [
+ 'rofi', '-dmenu', '-i', '-no-auto-select', '-p', prompt + '❯', '-mesg',
+ msg, *args
+ ]
+ p = subprocess.run(
+ args, input=body or None, stdout=subprocess.PIPE, encoding='utf-8')
+ return p.returncode, p.stdout.strip()
+
+
+def message(msg):
+ subprocess.run(['rofi', '-e', "rofi-twitch\n\n" + msg])
+
+
+def req(url, **kwargs):
+ p = s.get('https://api.twitch.tv/' + url, params=kwargs)
+ p.raise_for_status()
+ return p.json()
+
+
+def cached(key, getter, *args):
+ key += '\0'.join(map(str, args))
+ data = CACHE[key]
+ if data:
+ return data
+ data = CACHE[key] = getter(*args)
+ return data
+
+
+def game_list():
+ def getter():
+ return req('helix/games/top', first=MANY_RESULTS)['data']
+
+ games = cached('games', getter)
+ if not games:
+ message("No games found. No connectivity? Twitch API broken?")
+ return exit, 1
+
+ gamestr = '\n'.join(g['name'] for g in games)
+ rc, gid = rofi(
+ '-no-custom',
+ '-format',
+ 'i',
+ '-kb-custom-1',
+ 'F2',
+ '-kb-custom-2',
+ 'F3',
+ body=gamestr,
+ msg="F2: search games ⋅ F3: search streams")
+ if rc == 10:
+ return game_search,
+ if rc == 11:
+ return stream_search,
+ if rc != 0:
+ return
+ try:
+ game = games[int(gid)]
+ return stream_list, game['id'], game['name']
+ except (KeyError, ValueError):
+ pass
+
+
+def game_search():
+ rc, query = rofi(prompt="Search games")
+ if rc != 0:
+ return
+
+ def getter(query):
+ return req(
+ 'kraken/search/games',
+ query=query,
+ type='suggest',
+ limit=FEW_RESULTS)['games']
+
+ games = cached('gsearch', getter, query)
+ if not games:
+ message(f"No game matching “{query}”.")
+ return game_search,
+
+ gamestr = '\n'.join(g['localized_name'] for g in games)
+ rc, gid = rofi('-no-custom', '-format', 'i', body=gamestr)
+ if rc != 0:
+ return
+ try:
+ game = games[int(gid)]
+ return stream_list, game['_id'], game['localized_name']
+ except (KeyError, ValueError):
+ pass
+
+
+def stream_search(input=''):
+ rc, query = rofi(prompt="Search streams")
+ if rc != 0:
+ return
+
+ def getter(query):
+ return req(
+ 'kraken/search/streams',
+ type='suggest',
+ query=query,
+ limit=FEW_RESULTS)['streams']
+
+ streams = cached('ssearch', getter, query)
+ if not streams:
+ message(f"No stream matching “{query}”.")
+ return stream_search,
+
+ streamstr = '\n'.join(f"{s['viewers']:>7,d} "
+ f"{s['channel']['display_name']:<22s} "
+ f"⋅ {s['game']:<22s} "
+ f"⋅ {s['channel']['status']}" for s in streams)
+ rc, sid = rofi('-no-custom', '-format', 'i', body=streamstr)
+ if rc != 0:
+ return
+ try:
+ return launch_stream, streams[int(sid)]['channel']['name']
+ except (KeyError, ValueError):
+ pass
+
+
+def stream_list(game_id, game_name):
+ def getter(game_id):
+ return req(
+ 'helix/streams', type='live', game_id=game_id,
+ first=MANY_RESULTS)['data']
+
+ streams = cached('streams', getter, game_id)
+ if not streams:
+ message(f"No stream for {game_name}.")
+ return
+
+ def user_getter(ids):
+ return req('helix/users', id=ids)['data']
+
+ ids = [s['user_id'] for s in streams]
+ users = {u['id']: u for u in cached('users', user_getter, ids)}
+ sstr = '\n'.join(f"{s['viewer_count']:>7,d} "
+ f"{users[s['user_id']]['display_name']:<22s} "
+ f"⋅ {s['title']}" for s in streams)
+ rc, sid = rofi(
+ '-no-custom',
+ '-format',
+ 'i',
+ body=sstr,
+ msg=f"Top streams for {game_name}")
+ if rc != 0:
+ return
+ try:
+ return launch_stream, users[streams[int(sid)]['user_id']]['login']
+ except (KeyError, ValueError):
+ pass
+
+
+def launch_stream(user):
+ link = f'https://www.twitch.tv/{user}'
+ streamlink = ['streamlink', link, 'best']
+ subprocess.run(['i3-msg', 'exec', ' '.join(streamlink)])
+ message(f"Starting streamlink for {link}. Enjoy!")
+ return exit,
+
+
+def exit(code=0):
+ sys.exit(code)
+
+
+def main():
+ state = [(game_list, )]
+
+ while state:
+ func, *args = state[-1]
+ ret = func(*args)
+ if ret is None:
+ # back
+ state.pop(-1)
+ else:
+ newfunc, *newargs = ret
+ # don't stack the same func twice
+ if newfunc != func:
+ state.append(ret)
+
+
+if __name__ == '__main__':
+ main()