├── LICENSE ├── README.md ├── requirements.txt └── twitch-recorder.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mika C. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twitch Stream Recorder 2 | This Python code is a simple Twitch stream recorder using the streamlink library, which allows you to record the best available quality livestream of a Twitch streamer and save the recording as a mp4 file to your computer. The reason I made this is, because other existing libraries were either broken or made things too complicated bothering people to get their twitch API tokens etc. 3 | 4 | ## Features 5 | * Record twitch streams right as they are happening 6 | * Save streams as mp4 7 | * Easy set up & usage 8 | 9 | ## Setup 10 | 1. Install the necessary python libraries [with pip](https://youtu.be/9z7gGUbAj5U?t=13) through cmd / terminal: 11 | ``pip install streamlink`` 12 | 13 | 2. Install ``ffmpeg`` and make sure it's declared as a system environment variable 14 | 15 | Guide for Windows: https://www.wikihow.com/Install-FFmpeg-on-Windows 16 | 17 | All other Operating systems: https://www.hostinger.com/tutorials/how-to-install-ffmpeg 18 | 19 | 3. Clone or download the twitch stream recorder repository from GitHub & extract the files. 20 | 21 | 4. Locate the extracted files and click on ``twitch-recorder.py`` 22 | 23 | ## Usage 24 | The first time you start up the program, it will ask you to declare a; 25 | 1. output folder location, where all future live streams should be saved inside. 26 | 2. twitch live streamers username you want to record 27 | 28 | 29 | 30 | Once the code is running, it will retrieve the best available stream URL for the entered Twitch username, and then use the ffmpeg command to record the stream to a file in the specified output folder. The file name will include the Twitch username and the date and time, which the recorder was started at. 31 | The code will run continuously until you stop it manually with a keyboard interrupt (Ctrl+C on Windows & Linux, or Command+C on macOS). 32 | 33 | After you set the recordings output folder location it will create a file named ``config.ini`` which will store the location permanently, so you only need to enter any streamers username you want to record in the future instead. 34 | 35 | ## Disclaimer 36 | Please note that recording and distributing Twitch streams without the permission of the content creator may violate Twitch's terms of service and could lead to legal consequences. Use this code responsibly and with respect for the creators whose content you are recording. 37 | 38 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | streamlink>=7.3.0 2 | -------------------------------------------------------------------------------- /twitch-recorder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import subprocess, os, configparser, threading, time, sys, json 3 | from datetime import datetime 4 | from tkinter import filedialog, Tk 5 | from time import sleep as s 6 | 7 | class TwitchRecorder: 8 | def __init__(self, username, config_file='config.ini'): 9 | self.username = username 10 | self.config_file = config_file 11 | self.config = configparser.ConfigParser() 12 | self.output_folder = None 13 | self.recording_process = None 14 | self.start_time = None 15 | self.output_filename = None 16 | self.is_recording = False 17 | self.stream_title = "" 18 | self.stream_category = "" 19 | 20 | self._setup_config() 21 | self._setup_output_folder() 22 | 23 | def _setup_config(self): 24 | print("🔧 Setting up configuration...") 25 | if not os.path.exists(self.config_file): 26 | self.config['DEFAULT'] = {'output_folder': ''} 27 | with open(self.config_file, 'w') as f: 28 | self.config.write(f) 29 | self.config.read(self.config_file) 30 | 31 | def _setup_output_folder(self): 32 | self.output_folder = self.config['DEFAULT']['output_folder'] 33 | 34 | if not self.output_folder: 35 | print('📁 Select output folder...') 36 | s(2) 37 | root = Tk() 38 | root.withdraw() 39 | self.output_folder = filedialog.askdirectory(title="Select Output Folder") 40 | root.destroy() 41 | 42 | if not self.output_folder: 43 | print("❌ No folder selected. Exiting.") 44 | sys.exit(1) 45 | 46 | self.config['DEFAULT']['output_folder'] = self.output_folder 47 | with open(self.config_file, 'w') as f: 48 | self.config.write(f) 49 | 50 | if not os.path.exists(self.output_folder): 51 | print(f"❌ Output folder doesn't exist: {self.output_folder}") 52 | sys.exit(1) 53 | 54 | def _check_stream_live(self): 55 | print(f"🔍 Checking if {self.username} is live...") 56 | try: 57 | result = subprocess.run([ 58 | 'streamlink', '--json', f'https://www.twitch.tv/{self.username}' 59 | ], capture_output=True, text=True, timeout=20) 60 | 61 | if result.returncode == 0: 62 | print("✅ Stream is available") 63 | try: 64 | stream_data = json.loads(result.stdout) 65 | self.stream_title = stream_data.get('metadata', {}).get('title', '').replace('/', '-').replace('\\', '-').replace(':', '-')[:50] 66 | self.stream_category = stream_data.get('metadata', {}).get('category', '').replace('/', '-').replace('\\', '-').replace(':', '-')[:30] 67 | print(f"📊 Title: {self.stream_title}") 68 | print(f"🎮 Category: {self.stream_category}") 69 | except: 70 | pass 71 | return True 72 | return False 73 | except: 74 | return False 75 | 76 | def _create_filename(self): 77 | timestamp = datetime.now().strftime("%d_%m_%y - %H_%M") 78 | 79 | # Build filename with stream info 80 | parts = [self.username, timestamp] 81 | if self.stream_category: 82 | parts.insert(1, self.stream_category) 83 | if self.stream_title: 84 | parts.insert(-1, self.stream_title) 85 | 86 | filename = f'{" - ".join(parts)}.mp4' 87 | self.output_filename = os.path.join(self.output_folder, filename) 88 | print(f"📝 Output: {filename}") 89 | 90 | def _set_title(self, title): 91 | try: 92 | safe_title = title.replace(":", "").replace("|", "-") 93 | if os.name == 'nt': 94 | os.system(f'title "{safe_title}"') 95 | except: 96 | pass 97 | 98 | def _status_monitor(self): 99 | while self.is_recording: 100 | try: 101 | if self.output_filename and os.path.exists(self.output_filename): 102 | size_mb = os.path.getsize(self.output_filename) / (1024 * 1024) 103 | if self.start_time: 104 | elapsed = datetime.now() - self.start_time 105 | duration = str(elapsed).split('.')[0] 106 | status = f"🔴 RECORDING {self.username} - {duration} - {size_mb:.1f}MB" 107 | self._set_title(status) 108 | print(f"\r📊 {status}", end="", flush=True) 109 | time.sleep(3) 110 | except: 111 | break 112 | 113 | def start_recording(self): 114 | print(f"🎬 Starting recording of {self.username}") 115 | 116 | if not self._check_stream_live(): 117 | print("❌ Stream not available") 118 | return False 119 | 120 | self._create_filename() 121 | self.start_time = datetime.now() 122 | self.is_recording = True 123 | 124 | print(f"⏰ Started at: {self.start_time.strftime('%H:%M:%S')}") 125 | 126 | # Start status thread 127 | threading.Thread(target=self._status_monitor, daemon=True).start() 128 | 129 | # Start recording 130 | cmd = ['streamlink', f'https://www.twitch.tv/{self.username}', 'best', '--output', self.output_filename] 131 | 132 | try: 133 | print("📡 Recording...") 134 | self.recording_process = subprocess.Popen( 135 | cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1 136 | ) 137 | 138 | # Monitor output for errors/warnings 139 | for line in self.recording_process.stdout: 140 | if not self.is_recording: 141 | break 142 | line = line.strip() 143 | if 'error' in line.lower() or 'critical' in line.lower(): 144 | print(f"\n❌ {line}") 145 | elif 'warning' in line.lower(): 146 | print(f"\n⚠️ {line}") 147 | 148 | return_code = self.recording_process.wait() 149 | self.is_recording = False 150 | 151 | return self._handle_completion(return_code) 152 | 153 | except KeyboardInterrupt: 154 | print(f"\n🛑 Stopped by user") 155 | self._stop_recording() 156 | return False 157 | except Exception as e: 158 | print(f"\n❌ Error: {e}") 159 | self._stop_recording() 160 | return False 161 | 162 | def _stop_recording(self): 163 | self.is_recording = False 164 | if self.recording_process: 165 | try: 166 | self.recording_process.terminate() 167 | self.recording_process.wait(timeout=5) 168 | except: 169 | self.recording_process.kill() 170 | 171 | def _handle_completion(self, return_code): 172 | if os.path.exists(self.output_filename): 173 | size_mb = os.path.getsize(self.output_filename) / (1024 * 1024) 174 | duration = datetime.now() - self.start_time if self.start_time else None 175 | 176 | print(f"\n🏁 FINISHED") 177 | print(f"📊 Size: {size_mb:.1f}MB") 178 | if duration: 179 | print(f"📊 Duration: {str(duration).split('.')[0]}") 180 | 181 | if return_code == 0: 182 | self._set_title(f"Complete - {self.username} - {size_mb:.1f}MB") 183 | return True 184 | 185 | print(f"⚠️ Finished with issues") 186 | return False 187 | 188 | if __name__ == '__main__': 189 | streamer_name = input('Streamer Username to record: ') 190 | recorder = TwitchRecorder(streamer_name) 191 | 192 | try: 193 | if recorder.start_recording(): 194 | print("🎉 Success!") 195 | else: 196 | print("😞 Failed") 197 | except Exception as e: 198 | print(f"💥 Fatal error: {e}") 199 | --------------------------------------------------------------------------------