Repository: Instinctlol/automatic-twitch-recorder
Branch: master
Commit: dca1c582702f
Files: 10
Total size: 29.8 KB
Directory structure:
gitextract_6mjceabh/
├── .gitignore
├── ATRHandler.py
├── README.md
├── atr_cmd.py
├── daemon.py
├── main.py
├── requirements.txt
├── twitch.py
├── utils.py
└── watcher.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
Pipfile*
*/*
client_id*
client_secret*
ngrok_auth_token*
config.txt
================================================
FILE: ATRHandler.py
================================================
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler
import logging
from urllib.parse import urlparse
import hmac
import json
from jsonschema import validate, ValidationError
from daemon import Daemon
class ATRHandler(BaseHTTPRequestHandler):
schema_cmd = {
'type': 'object',
'properties': {
'cmd': {'type': 'string'},
'args': {'type': 'array', 'items': {'type': 'string'}},
},
}
message = {}
ok = False
# comment this out when developing :)
def log_message(self, format, *args):
return
def _set_response(self, msg=None):
self.send_response(HTTPStatus.OK, msg)
self.send_header('Content-type', 'text/html')
self.end_headers()
def _set_bad_request(self, msg):
self.send_response(HTTPStatus.BAD_REQUEST, msg)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(msg.encode('utf-8'))
def _send_json_response(self):
self.send_response(HTTPStatus.OK)
self.send_header('Content-type', 'application/json')
self.end_headers()
dump = json.dumps(self.message)
self.log_message('body: ' + dump)
self.wfile.write(dump.encode(encoding='utf_8'))
def _send_bad_json_response(self):
self.send_response(HTTPStatus.BAD_REQUEST)
self.send_header('Content-type', 'application/json')
self.end_headers()
dump = json.dumps(self.message)
self.log_message('body: ' + dump)
self.wfile.write(dump.encode(encoding='utf_8'))
def do_GET(self):
"""Handles GET requests, will send challenge back to twitch to register webhook.
"""
query = urlparse(self.path).query
logging.info('GET request,\nPath: %s\nHeaders:\n%s\n', str(self.path), str(self.headers))
try:
query_components = dict(qc.split('=') for qc in query.split('&'))
challenge = query_components['hub.challenge']
# s = ''.join(x for x in challenge if x.isdigit())
# print(s)
# print(challenge)
self.send_response(HTTPStatus.OK)
self.end_headers()
self.wfile.write(bytes(challenge, 'utf-8'))
except:
query_components = None
challenge = None
self._set_response()
self.wfile.write(bytes('Hello Stranger :)', 'utf-8'))
def do_POST(self):
content_length = int(self.headers['Content-Length']) # <--- Gets the size of data
post_data = self.rfile.read(content_length).decode() # <--- Gets the data itself
logging.info('POST request,\nPath: %s\nHeaders:\n%s\n\nBody:\n%s\n',
str(self.path), str(self.headers), post_data)
if self.path == '/cmd/':
payload = json.loads(post_data)
try:
validate(instance=payload, schema=self.schema_cmd)
self.handle_cmd(payload)
if self.ok:
self._send_json_response()
else:
self._send_bad_json_response()
# self._set_response()
# self.wfile.write("POST request for {}".format(self.path).encode('utf-8'))
except ValidationError as validationerror:
self.message['println'] = 'Could not validate request payload for cmd:\n' + str(validationerror)
self._send_bad_json_response()
else:
if 'Content-Type' in self.headers:
content_type = str(self.headers['Content-Type'])
else:
raise ValueError('not all headers supplied.')
if 'X-Hub-Signature' in self.headers:
hub_signature = str(self.headers['X-Hub-Signature'])
algorithm, hashval = hub_signature.split('=')
print(hashval)
print(algorithm)
if post_data and algorithm and hashval:
gg = hmac.new(Daemon.WEBHOOK_SECRET.encode(), post_data, algorithm)
if not hmac.compare_digest(hashval.encode(), gg.hexdigest().encode()):
raise ConnectionError('Hash missmatch.')
else:
raise ValueError('not all headers supplied.')
self._set_response()
self.wfile.write('POST request for {}'.format(self.path).encode('utf-8'))
def handle_cmd(self, post_data):
"""Handles POST requests on /cmd/. These contain a single command with optional arguments supplied by the user via GUI or CLI.
Parameters:
----------
post_data (dict): Contains the keys `cmd` and `args`.\n
post_data['cmd'] (str): the command to execute. \n
post_data['args'] (list): the arguments for the command.
"""
cmd_executor = {
'exit': self.cmd_exit,
'start': self.cmd_start,
'list': self.cmd_list,
'remove': self.cmd_remove,
'add': self.cmd_add,
'time': self.cmd_time,
'download_folder': self.cmd_download_folder,
}
func = cmd_executor[post_data['cmd']]
if len(post_data['args']) > 0:
func(post_data['args'])
else:
func()
def cmd_exit(self):
self.message['println'] = self.server.exit()
self.ok = True
def cmd_start(self):
self.message['println'] = self.server.start()
self.ok = True
def cmd_remove(self, args):
try:
self.ok, self.message['println'] = self.server.remove_streamer(args[0])
except IndexError:
self.ok = False
self.message['println'] = 'Missing streamer in arguments.'
def cmd_list(self):
live, offline = self.server.get_streamers()
msg = 'Live: ' + str(live).strip('[]') + '\n' + \
'Offline: ' + str(offline).strip('[]')
self.message['println'] = msg
self.ok = True
def cmd_add(self, args):
if len(args) == 0:
self.message['println'] = 'Missing streamer in arguments.'
self._send_bad_json_response()
return
if len(args) > 1:
self.ok, resp = self.server.add_streamer(args[0], args[1])
else:
self.ok, resp = self.server.add_streamer(args[0])
self.message['println'] = '\n'.join(resp)
def cmd_time(self, args):
try:
self.message['println'] = self.server.set_interval(int(args[0]))
self.ok = True
except ValueError:
self.ok = False
self.message['println'] = '\'' + args[0] + '\' is not valid.'
def cmd_download_folder(self, args):
try:
self.message['println'] = self.server.set_download_folder(str(args[0]).strip())
self.ok = True
except ValueError:
self.ok = False
self.message['println'] = '\'' + args[0] + '\' is not valid.'
================================================
FILE: README.md
================================================
# discontinued
This script was declared end-of-life and is not maintained anymore. Please have a look into well maintained, more feature-rich solutions, like [ganymede](https://github.com/Zibbp/ganymede) and [LiveStreamDVR](https://github.com/MrBrax/LiveStreamDVR)
# automatic-twitch-recorder
Checks if a user on twitch is currently streaming and then records the stream via streamlink
## Dependencies:
- streamlink (https://streamlink.github.io)
- python3 (https://www.python.org/) (I use [python3.6](https://www.python.org/downloads/release/python-368/) for windows)
## Installation:
- clone this repo or download
- make sure you have python3 installed
- open cmd/terminal
- change directory into folder containing the file 'requirements.txt'
- type `pip install -r requirements.txt`
## Usage:
### Using the CLI (Command-line interface)
- in your cmd/terminal, run `python main.py`
- type `help`
- `add streamer [quality]`: adds the streamer you want to record in given optional quality, e.g. `add forsen`. Default quality: `best`, quality options: `1080p60, 1080p, 720p60, 720p, 480p, 360p, 160p, audio_only`
- `time 10`: sets check interval in seconds
- `remove streamer`: removes streamer, also stops recording this streamer
- `start`: starts checking for / recording all added streamers
- `list`: prints all added streamers
- `exit`: stops the application and all currently running recordings
- `download_folder path`: sets the download folder for saving the recordings. (#streamer# will be replaced with the name of the streamer)
Example inputs to record forsen and nymn (this will also repeatedly check if they are online):
```
$ add forsen
$ add nymn
$ start
```
## Bugs:
- CLI shenanigans
- text will get printed into the prompt / user input. However, your input will still be valid, so do not worry. This does not restrict the functionality of this application.
- There's an open [stackoverflow question](https://stackoverflow.com/questions/57027294/cmd-module-async-job-prints-are-overwriting-prompt-input) for this. Any volunteers?
## Plans for the future:
- Refactor to easily support any supported streamlink platform, e.g. YouTube.
- When done recording, upload to YouTube
- Export to .exe so you don't have to install python
- PyInstaller and streamlink apparently do not work well together (streamlink will throw NoPluginError). Help is appreciated.
- Create a GUI with Qt (PyQt5 or PySide2) (fairly easy, but time consuming)
================================================
FILE: atr_cmd.py
================================================
import cmd
import sys
import requests
# TODO: https://stackoverflow.com/questions/37866403/python-cmd-module-resume-prompt-after-async-event
class AtrCmd(cmd.Cmd):
def _send_cmd(self, cmd_payload):
r = requests.post('http://127.0.0.1:1234/cmd/', json=cmd_payload)
resp_json = r.json()
resp_ok = r.ok
print(resp_json.pop('println'))
return resp_ok, resp_json
def _create_payload(self, command, *args):
payload = {'cmd': command,
'args': list(args)
}
return payload
def __init__(self):
super().__init__()
def do_add(self, line):
line = line.split(' ')
payload = self._create_payload('add', *line)
self._send_cmd(payload)
def help_add(self):
print('\n'.join([
'add streamer [quality]',
'Adds streamer to watchlist with (optional) selected quality.',
'Default quality: best',
]))
def do_remove(self, line):
payload = self._create_payload('remove', line)
self._send_cmd(payload)
def help_remove(self):
print('\n'.join([
'remove streamer',
'Removes streamer from watchlist, also stops recording if currently recording streamer.',
]))
def do_list(self, line):
payload = self._create_payload('list')
self._send_cmd(payload)
def help_list(self):
print('\n'.join([
'list',
'List all watched streamers, seperated in offline and live sets.',
]))
def do_start(self, line):
payload = self._create_payload('start')
self._send_cmd(payload)
def help_start(self):
print('\n'.join([
'start',
'Starts the configured daemon. You may still configure it further while it is running.',
]))
def do_time(self, line):
try:
payload = self._create_payload('time', line)
self._send_cmd(payload)
except ValueError:
print('\''+line+'\' is not valid.')
def help_time(self):
print('\n'.join([
'time seconds',
'Configures the check interval in seconds.',
'It\'s advised not to make it too low and to stay above 10 seconds.',
'Default check interval: 30 seconds.',
]))
def do_download_folder(self, line):
payload = self._create_payload('download_folder', line)
self._send_cmd(payload)
def help_download_folder(self):
print('\n'.join([
'download_folder path',
'Configures the download folder for saving the videos.',
]))
def do_EOF(self, line):
self.do_exit(line)
return True
def do_exit(self, line):
payload = self._create_payload('exit')
self._send_cmd(payload)
sys.exit()
def help_exit(self):
print('\n'.join([
'exit',
'Exits the application, stopping all running recording tasks.',
]))
def cmdloop_with_keyboard_interrupt(self):
try:
self.cmdloop()
except KeyboardInterrupt:
self.do_exit('')
if __name__ == '__main__':
AtrCmd().cmdloop_with_keyboard_interrupt()
================================================
FILE: daemon.py
================================================
import logging
import os
import threading
from concurrent.futures import ThreadPoolExecutor
from http.server import HTTPServer
import ATRHandler
import twitch
from utils import get_client_id, StreamQualities
from watcher import Watcher
class Daemon(HTTPServer):
#
# CONSTANTS
#
VALID_BROADCAST = ['live'] # 'rerun' can be added through commandline flags/options
WEBHOOK_SECRET = 'automaticTwitchRecorder'
WEBHOOK_URL_PREFIX = 'https://api.twitch.tv/helix/streams?user_id='
LEASE_SECONDS = 864000 # 10 days = 864000
check_interval = 10
def __init__(self, server_address, RequestHandlerClass):
super().__init__(server_address, RequestHandlerClass)
self.PORT = server_address[1]
self.streamers = {} # holds all streamers that need to be surveilled
self.watched_streamers = {} # holds all live streamers that are currently being recorded
self.client_id = get_client_id()
self.kill = False
self.started = False
self.download_folder = os.getcwd() + os.path.sep + "#streamer#"
# ThreadPoolExecutor(max_workers): If max_workers is None or not given, it will default to the number of
# processors on the machine, multiplied by 5
self.pool = ThreadPoolExecutor()
def add_streamer(self, streamer, quality=StreamQualities.BEST.value):
streamer = streamer.lower()
streamer_dict = {}
resp = []
ok = False
qualities = [q.value for q in StreamQualities]
if quality not in qualities:
resp.append('Invalid quality: ' + quality + '.')
resp.append('Quality options: ' + str(qualities))
else:
streamer_dict.update({'preferred_quality': quality})
# get channel id of streamer
user_info = list(twitch.get_user_info(streamer))
# check if user exists
if user_info:
streamer_dict.update({'user_info': user_info[0]})
self.streamers.update({streamer: streamer_dict})
resp.append('Successfully added ' + streamer + ' to watchlist.')
ok = True
else:
resp.append('Invalid streamer name: ' + streamer + '.')
return ok, resp
def remove_streamer(self, streamer):
streamer = streamer.lower()
if streamer in self.streamers.keys():
self.streamers.pop(streamer)
return True, 'Removed ' + streamer + ' from watchlist.'
elif streamer in self.watched_streamers.keys():
watcher = self.watched_streamers[streamer]['watcher']
watcher.quit()
return True, 'Removed ' + streamer + ' from watchlist.'
else:
return False, 'Could not find ' + streamer + '. Already removed?'
def start(self):
if not self.started:
self._check_streams()
self.started = True
return 'Daemon is started.'
else:
return 'Daemon is already running.'
def set_interval(self, secs):
if secs < 1:
secs = 1
self.check_interval = secs
return 'Interval is now set to ' + str(secs) + ' seconds.'
def set_download_folder(self, download_folder):
self.download_folder = download_folder
return 'Download folder is now set to \'' + download_folder + '\' .'
def _check_streams(self):
user_ids = []
# get channel ids of all streamers
for streamer in self.streamers.keys():
user_ids.append(self.streamers[streamer]['user_info']['id'])
if user_ids:
streams_info = twitch.get_stream_info(*user_ids)
# save streaming information for all streamers, if it exists
for stream_info in streams_info:
streamer_name = stream_info['user_name'].lower()
self.streamers[streamer_name].update({'stream_info': stream_info})
live_streamers = []
# check which streamers are live
for streamer_info in self.streamers.values():
try:
stream_info = streamer_info['stream_info']
if stream_info['type'] == 'live':
live_streamers.append(stream_info['user_name'].lower())
except KeyError:
pass
self._start_watchers(live_streamers)
if not self.kill:
t = threading.Timer(self.check_interval, self._check_streams)
t.start()
def _start_watchers(self, live_streamers_list):
for live_streamer in live_streamers_list:
if live_streamer not in self.watched_streamers:
live_streamer_dict = self.streamers.pop(live_streamer)
curr_watcher = Watcher(live_streamer_dict, self.download_folder)
self.watched_streamers.update({live_streamer: {'watcher': curr_watcher,
'streamer_dict': live_streamer_dict}})
if not self.kill:
t = self.pool.submit(curr_watcher.watch)
t.add_done_callback(self._watcher_callback)
def _watcher_callback(self, returned_watcher):
streamer_dict = returned_watcher.result()
streamer = streamer_dict['user_info']['login']
kill = streamer_dict['kill']
cleanup = streamer_dict['cleanup']
self.watched_streamers.pop(streamer)
if not cleanup:
print('Finished watching ' + streamer)
else:
output_filepath = streamer_dict['output_filepath']
if os.path.exists(output_filepath):
os.remove(output_filepath)
if not kill:
self.add_streamer(streamer, streamer_dict['preferred_quality'])
def get_streamers(self):
return list(self.watched_streamers.keys()), list(self.streamers.keys())
def exit(self):
self.kill = True
for streamer in self.watched_streamers.values():
watcher = streamer['watcher']
watcher.quit()
self.pool.shutdown()
self.server_close()
threading.Thread(target=self.shutdown, daemon=True).start()
return 'Daemon exited successfully'
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
server = Daemon(('127.0.0.1', 1234), ATRHandler.ATRHandler)
try:
server.serve_forever()
except KeyboardInterrupt:
pass
finally:
server.exit()
print('exited gracefully')
================================================
FILE: main.py
================================================
import threading
import ATRHandler
import utils
from atr_cmd import AtrCmd
from daemon import Daemon
if __name__ == '__main__':
utils.get_client_id() # creates necessary config before launch
server = Daemon(('127.0.0.1', 1234), ATRHandler.ATRHandler)
threading.Thread(target=server.serve_forever).start()
AtrCmd().cmdloop_with_keyboard_interrupt()
================================================
FILE: requirements.txt
================================================
attrs==20.3.0
certifi==2020.12.5
chardet==4.0.0
idna==2.10
importlib-metadata==3.3.0
iso-639==0.4.5
iso3166==1.0.1
isodate==0.6.0
jsonschema==3.2.0
pathvalidate==2.3.1
pycryptodome==3.9.9
pyrsistent==0.17.3
PySocks==1.7.1
requests==2.25.1
six==1.15.0
streamlink==2.0.0
typing-extensions==3.7.4.3
urllib3==1.26.2
websocket-client==0.57.0
zipp==3.4.0
================================================
FILE: twitch.py
================================================
import requests
import utils
auth = {'Client-ID': str(utils.get_client_id()),
'Authorization': 'Bearer ' + utils.get_app_access_token()}
def get_user_info(user_login, *args: str) -> list:
"""
Gets user info for user logins
See https://dev.twitch.tv/docs/api/reference#get-users
Parameters
----------
user_login: str
username string
args: str
additional string usernames (max. 99)
Returns
-------
list
contains user_info dicts
"""
get_user_id_url = 'https://api.twitch.tv/helix/users?login=' + user_login
if len(args) > 99:
args = args[:99]
for user_login_i in args:
get_user_id_url += '&login=' + user_login_i
r = requests.get(get_user_id_url, headers=auth)
temp = r.json()
if temp['data']:
return list(temp['data'])
else:
return []
def get_stream_info(user_id: str, *args):
"""
Gets stream info for user ids
See https://dev.twitch.tv/docs/api/reference#get-streams
Parameters
----------
user_id: str
user id string
args: str
additional string user ids (max. 99)
Returns
-------
list
contains stream_info dicts
"""
if len(args) > 99:
args = args[:99]
get_user_id_url = 'https://api.twitch.tv/helix/streams?first=100&user_id=' + user_id
for user_id in args:
get_user_id_url += '&user_id=' + user_id
r = requests.get(get_user_id_url, headers=auth)
temp = r.json()
if temp['data']:
return list(temp['data'])
else:
return []
================================================
FILE: utils.py
================================================
import os
from datetime import datetime, timedelta
from enum import Enum
from pathlib import Path
import json
import requests
from pathvalidate import sanitize_filename
CONFIG_FILE = os.getcwd() + os.path.sep + 'config.txt' # Location of config.txt config file.
_APP_ACCESS_TOKEN = ''
_APP_ACCESS_TOKEN_REFRESH_TIME = None
CONFIG = None
# TODO: figure it out from streamlink library
class StreamQualities(Enum):
AUDIO_ONLY = 'audio_only'
_160p = '160p'
_360 = '360p'
_480p = '480p'
_720p = '720p'
_720p60 = '720p60'
_1080p = '1080p'
_1080p60 = '1080p60'
WORST = 'worst'
BEST = 'best'
def _read_config():
global CONFIG
config_path = Path(CONFIG_FILE)
try:
config_path.resolve(strict=True)
except FileNotFoundError as ex:
config_file = open(config_path, 'w')
CONFIG = {
'client_id': '',
'client_secret': '',
'ngrok_authtoken': ''
}
config_file.write(json.dumps(CONFIG))
config_file.close()
else:
config_file = open(config_path, 'r')
CONFIG = json.loads(config_file.read())
config_file.close()
def _write_config():
global CONFIG
config_path = Path(CONFIG_FILE)
config_file = open(config_path, 'w')
config_file.write(json.dumps(CONFIG))
config_file.close()
def get_client_id():
global CONFIG
if not CONFIG:
_read_config()
if not CONFIG['client_id']:
print('Client id unset.')
print('Visit the following website to generate a client id and client secret for this script.')
print('https://dev.twitch.tv/console/apps')
print('Enter client id from website.')
CONFIG['client_id'] = input('client id: ')
print('Enter client secret from website.')
CONFIG['client_secret'] = input('client secret: ')
_write_config()
return CONFIG['client_id']
def get_client_secret():
global CONFIG
if not CONFIG:
_read_config()
if not CONFIG['client_secret']:
print('Client secret unset.')
print('Visit the following website to get a client secret for this script.')
print('https://dev.twitch.tv/console/apps')
print('Enter client secret from website.')
CONFIG['client_secret'] = input('client secret: ')
_write_config()
return CONFIG['client_secret']
def get_ngrok_auth_token():
global CONFIG
if not CONFIG:
_read_config()
if not CONFIG['ngrok_authtoken']:
print('Ngrok authtoken unset.')
print('Visit the following website to generate an authtoken for this script.')
print('https://dashboard.ngrok.com/auth/your-authtoken')
print('Enter authtoken from website.')
CONFIG['ngrok_authtoken'] = input('Authtoken: ')
_write_config()
return CONFIG['ngrok_authtoken']
def get_app_access_token():
global _APP_ACCESS_TOKEN, _APP_ACCESS_TOKEN_REFRESH_TIME
# API Notes:
# App access tokens and ID tokens cannot be refreshed.
# No scopes are needed when requesting app access tokens.
oauth_url = 'https://id.twitch.tv/oauth2/token?client_id={0}&client_secret={1}&grant_type=client_credentials'
if not _APP_ACCESS_TOKEN or not _APP_ACCESS_TOKEN_REFRESH_TIME or _APP_ACCESS_TOKEN_REFRESH_TIME < datetime.now():
r = requests.post(oauth_url.format(get_client_id(), get_client_secret()))
# {
# "access_token": "<user access token>",
# "refresh_token": "",
# "expires_in": <number of seconds until the token expires>,
# "scope": ["<your previously listed scope(s)>"],
# "token_type": "bearer"
# }
oauth_json = r.json()
_APP_ACCESS_TOKEN = oauth_json['access_token']
_APP_ACCESS_TOKEN_REFRESH_TIME = datetime.now() + timedelta(seconds=oauth_json['expires_in'] - 60)
return _APP_ACCESS_TOKEN
def get_valid_filename(s):
s = str(s)
return sanitize_filename(s)
================================================
FILE: watcher.py
================================================
import datetime
import streamlink
import os
from utils import get_valid_filename, StreamQualities
class Watcher:
streamer_dict = {}
streamer = ''
stream_title = ''
stream_quality = ''
kill = False
cleanup = False
def __init__(self, streamer_dict, download_folder):
self.streamer_dict = streamer_dict
self.streamer = self.streamer_dict['user_info']['display_name']
self.streamer_login = self.streamer_dict['user_info']['login']
self.stream_title = self.streamer_dict['stream_info']['title']
self.stream_quality = self.streamer_dict['preferred_quality']
self.download_folder = download_folder
def quit(self):
self.kill = True
def clean_break(self):
self.cleanup = True
def watch(self):
curr_time = datetime.datetime.now().strftime("%Y-%m-%d %H.%M.%S")
file_name = curr_time + " - " + self.streamer + " - " + get_valid_filename(self.stream_title) + ".ts"
directory = self._formatted_download_folder(self.streamer_login) + os.path.sep
if not os.path.exists(directory):
os.makedirs(directory)
output_filepath = directory + file_name
self.streamer_dict.update({'output_filepath': output_filepath})
streams = streamlink.streams('https://www.twitch.tv/' + self.streamer_login)
# Occurs when already recording another stream and new streamer (that is already live) is added
# not sure why this error is thrown..
# Traceback (most recent call last):
# File "C:\Program Files\Python36\lib\threading.py", line 916, in _bootstrap_inner
# self.run()
# File "E:\Downloads\automatic-twitch-recorder\venv\lib\site-packages\streamlink\stream\segmented.py", line 59, in run
# for segment in self.iter_segments():
# File "E:\Downloads\automatic-twitch-recorder\venv\lib\site-packages\streamlink\stream\hls.py", line 307, in iter_segments
# self.reload_playlist()
# File "E:\Downloads\automatic-twitch-recorder\venv\lib\site-packages\streamlink\stream\hls.py", line 235, in reload_playlist
# self.process_sequences(playlist, sequences)
# File "E:\Downloads\automatic-twitch-recorder\venv\lib\site-packages\streamlink\plugins\twitch.py", line 210, in process_sequences
# return super(TwitchHLSStreamWorker, self).process_sequences(playlist, sequences)
# TypeError: super(type, obj): obj must be an instance or subtype of type
try:
stream = streams[self.stream_quality]
except KeyError:
temp_quality = self.stream_quality
if len(streams) > 0: # False => stream is probably offline
if self.stream_quality in streams.keys():
self.stream_quality = StreamQualities.BEST.value
else:
self.stream_quality = list(streams.keys())[-1] # best not in streams? choose best effort quality
else:
self.cleanup = True
if not self.cleanup:
print('Invalid stream quality: ' + '\'' + temp_quality + '\'')
print('Falling back to default case: ' + self.stream_quality)
self.streamer_dict['preferred_quality'] = self.stream_quality
stream = streams[self.stream_quality]
else:
stream = None
if not self.kill and not self.cleanup and stream:
print(self.streamer + ' is live. Saving stream in ' +
self.stream_quality + ' quality to ' + output_filepath + '.')
try:
with open(output_filepath, "ab") as out_file: # open for [a]ppending as [b]inary
fd = stream.open()
while not self.kill and not self.cleanup:
data = fd.read(1024)
# If data is empty the stream has ended
if not data:
fd.close()
out_file.close()
break
out_file.write(data)
except streamlink.StreamError as err:
print('StreamError: {0}'.format(err)) # TODO: test when this happens
except IOError as err:
# If file validation fails this error gets triggered.
print('Failed to write data to file: {0}'.format(err))
self.streamer_dict.update({'kill': self.kill})
self.streamer_dict.update({'cleanup': self.cleanup})
return self.streamer_dict
def _formatted_download_folder(self, streamer):
return self.download_folder.replace('#streamer#', streamer)
gitextract_6mjceabh/ ├── .gitignore ├── ATRHandler.py ├── README.md ├── atr_cmd.py ├── daemon.py ├── main.py ├── requirements.txt ├── twitch.py ├── utils.py └── watcher.py
SYMBOL INDEX (64 symbols across 6 files)
FILE: ATRHandler.py
class ATRHandler (line 12) | class ATRHandler(BaseHTTPRequestHandler):
method log_message (line 24) | def log_message(self, format, *args):
method _set_response (line 27) | def _set_response(self, msg=None):
method _set_bad_request (line 32) | def _set_bad_request(self, msg):
method _send_json_response (line 38) | def _send_json_response(self):
method _send_bad_json_response (line 46) | def _send_bad_json_response(self):
method do_GET (line 54) | def do_GET(self):
method do_POST (line 75) | def do_POST(self):
method handle_cmd (line 114) | def handle_cmd(self, post_data):
method cmd_exit (line 139) | def cmd_exit(self):
method cmd_start (line 143) | def cmd_start(self):
method cmd_remove (line 147) | def cmd_remove(self, args):
method cmd_list (line 154) | def cmd_list(self):
method cmd_add (line 161) | def cmd_add(self, args):
method cmd_time (line 174) | def cmd_time(self, args):
method cmd_download_folder (line 182) | def cmd_download_folder(self, args):
FILE: atr_cmd.py
class AtrCmd (line 8) | class AtrCmd(cmd.Cmd):
method _send_cmd (line 10) | def _send_cmd(self, cmd_payload):
method _create_payload (line 17) | def _create_payload(self, command, *args):
method __init__ (line 23) | def __init__(self):
method do_add (line 26) | def do_add(self, line):
method help_add (line 31) | def help_add(self):
method do_remove (line 38) | def do_remove(self, line):
method help_remove (line 42) | def help_remove(self):
method do_list (line 48) | def do_list(self, line):
method help_list (line 52) | def help_list(self):
method do_start (line 58) | def do_start(self, line):
method help_start (line 62) | def help_start(self):
method do_time (line 68) | def do_time(self, line):
method help_time (line 75) | def help_time(self):
method do_download_folder (line 83) | def do_download_folder(self, line):
method help_download_folder (line 87) | def help_download_folder(self):
method do_EOF (line 93) | def do_EOF(self, line):
method do_exit (line 97) | def do_exit(self, line):
method help_exit (line 102) | def help_exit(self):
method cmdloop_with_keyboard_interrupt (line 108) | def cmdloop_with_keyboard_interrupt(self):
FILE: daemon.py
class Daemon (line 13) | class Daemon(HTTPServer):
method __init__ (line 23) | def __init__(self, server_address, RequestHandlerClass):
method add_streamer (line 36) | def add_streamer(self, streamer, quality=StreamQualities.BEST.value):
method remove_streamer (line 61) | def remove_streamer(self, streamer):
method start (line 73) | def start(self):
method set_interval (line 81) | def set_interval(self, secs):
method set_download_folder (line 87) | def set_download_folder(self, download_folder):
method _check_streams (line 91) | def _check_streams(self):
method _start_watchers (line 123) | def _start_watchers(self, live_streamers_list):
method _watcher_callback (line 134) | def _watcher_callback(self, returned_watcher):
method get_streamers (line 149) | def get_streamers(self):
method exit (line 152) | def exit(self):
FILE: twitch.py
function get_user_info (line 9) | def get_user_info(user_login, *args: str) -> list:
function get_stream_info (line 39) | def get_stream_info(user_id: str, *args):
FILE: utils.py
class StreamQualities (line 17) | class StreamQualities(Enum):
function _read_config (line 30) | def _read_config():
function _write_config (line 50) | def _write_config():
function get_client_id (line 58) | def get_client_id():
function get_client_secret (line 74) | def get_client_secret():
function get_ngrok_auth_token (line 88) | def get_ngrok_auth_token():
function get_app_access_token (line 102) | def get_app_access_token():
function get_valid_filename (line 125) | def get_valid_filename(s):
FILE: watcher.py
class Watcher (line 7) | class Watcher:
method __init__ (line 15) | def __init__(self, streamer_dict, download_folder):
method quit (line 23) | def quit(self):
method clean_break (line 26) | def clean_break(self):
method watch (line 29) | def watch(self):
method _formatted_download_folder (line 100) | def _formatted_download_folder(self, streamer):
Condensed preview — 10 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (32K chars).
[
{
"path": ".gitignore",
"chars": 67,
"preview": "Pipfile*\n*/*\nclient_id*\nclient_secret*\nngrok_auth_token*\nconfig.txt"
},
{
"path": "ATRHandler.py",
"chars": 7051,
"preview": "from http import HTTPStatus\nfrom http.server import BaseHTTPRequestHandler\nimport logging\nfrom urllib.parse import urlpa"
},
{
"path": "README.md",
"chars": 2489,
"preview": "# discontinued\n\nThis script was declared end-of-life and is not maintained anymore. Please have a look into well maintai"
},
{
"path": "atr_cmd.py",
"chars": 3271,
"preview": "import cmd\nimport sys\n\nimport requests\n\n\n# TODO: https://stackoverflow.com/questions/37866403/python-cmd-module-resume-p"
},
{
"path": "daemon.py",
"chars": 6565,
"preview": "import logging\nimport os\nimport threading\nfrom concurrent.futures import ThreadPoolExecutor\nfrom http.server import HTTP"
},
{
"path": "main.py",
"chars": 367,
"preview": "import threading\n\nimport ATRHandler\nimport utils\nfrom atr_cmd import AtrCmd\nfrom daemon import Daemon\n\nif __name__ == '_"
},
{
"path": "requirements.txt",
"chars": 349,
"preview": "attrs==20.3.0\ncertifi==2020.12.5\nchardet==4.0.0\nidna==2.10\nimportlib-metadata==3.3.0\niso-639==0.4.5\niso3166==1.0.1\nisoda"
},
{
"path": "twitch.py",
"chars": 1595,
"preview": "import requests\n\nimport utils\n\nauth = {'Client-ID': str(utils.get_client_id()),\n 'Authorization': 'Bearer ' + uti"
},
{
"path": "utils.py",
"chars": 3987,
"preview": "import os\nfrom datetime import datetime, timedelta\nfrom enum import Enum\nfrom pathlib import Path\nimport json\n\nimport re"
},
{
"path": "watcher.py",
"chars": 4753,
"preview": "import datetime\nimport streamlink\nimport os\nfrom utils import get_valid_filename, StreamQualities\n\n\nclass Watcher:\n s"
}
]
About this extraction
This page contains the full source code of the Instinctlol/automatic-twitch-recorder GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 10 files (29.8 KB), approximately 7.1k tokens, and a symbol index with 64 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.