├── .idea ├── .gitignore ├── TwitchRecover.iml ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── README.md ├── recover.py └── requirements.txt /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/TwitchRecover.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![GitHub contributors](https://img.shields.io/github/contributors/tanersb/TwitchRecover?style=for-the-badge) 3 | ![GitHub forks](https://img.shields.io/github/forks/tanersb/TwitchRecover?style=for-the-badge) 4 | ![GitHub Repo stars](https://img.shields.io/github/stars/tanersb/TwitchRecover?style=for-the-badge) 5 | ![GitHub issues](https://img.shields.io/github/issues/tanersb/TwitchRecover?style=for-the-badge) 6 | ![GitHub](https://img.shields.io/github/license/tanersb/TwitchRecover?style=for-the-badge) 7 | # TwitchRecover 8 | 9 | ![example1](https://user-images.githubusercontent.com/58490105/172502426-c53d08e3-2724-487a-9f64-5437521fea1f.png) 10 | 11 | 12 | Guide: 13 | 14 | As a first step, run `pip install -r requirements.txt` to install required packages 15 | 16 | Using a Twitch Tracker or Streams Charts link: 17 | 18 | You can use the Twitch Tracker or Streams Charts link of a stream to directly get the VOD links. 19 | 20 | 21 | i.e. https://twitchtracker.com/blastpremier/streams/46313458365 22 | 23 | 24 | i.e. https://streamscharts.com/channels/blastpremier/streams/46313458365 25 | 26 | ## How do i open this link 27 | 28 | Use the VLC media player. 29 | CTRL + N (open network stream) and pastle this link. 30 | 31 | ## Suggestion 32 | 33 | As a suggestion, I would like to express my gratitude to the author of [owk880301](https://github.com/owk880301) for his excellent work on developing the by-pass method. Project prepared with Cloudflare bypass method: https://github.com/owk880301/TwitchRecover_cloudflare_bypass 34 | 35 | 36 | -------------------------------------------------------------------------------- /recover.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import hashlib 3 | import time 4 | import urllib.request 5 | from threading import Thread 6 | from bs4 import BeautifulSoup 7 | import requests 8 | import webbrowser 9 | import random 10 | import sys 11 | 12 | 13 | 14 | domains = [ 15 | "https://vod-secure.twitch.tv", 16 | "https://vod-metro.twitch.tv", 17 | "https://vod-pop-secure.twitch.tv", 18 | "https://d2e2de1etea730.cloudfront.net", 19 | "https://dqrpb9wgowsf5.cloudfront.net", 20 | "https://ds0h3roq6wcgc.cloudfront.net", 21 | "https://d2nvs31859zcd8.cloudfront.net", 22 | "https://d2aba1wr3818hz.cloudfront.net", 23 | "https://d3c27h4odz752x.cloudfront.net", 24 | "https://dgeft87wbj63p.cloudfront.net", 25 | "https://d1m7jfoe9zdc1j.cloudfront.net", 26 | "https://d3vd9lfkzbru3h.cloudfront.net", 27 | "https://d2vjef5jvl6bfs.cloudfront.net", 28 | "https://d1ymi26ma8va5x.cloudfront.net", 29 | "https://d1mhjrowxxagfy.cloudfront.net", 30 | "https://ddacn6pr5v0tl.cloudfront.net", 31 | "https://d3aqoihi2n8ty8.cloudfront.net", 32 | "https://d1xhnb4ptk05mw.cloudfront.net", 33 | "https://d6tizftlrpuof.cloudfront.net", 34 | "https://d36nr0u3xmc4mm.cloudfront.net", 35 | "https://d1oca24q5dwo6d.cloudfront.net", 36 | "https://d2um2qdswy1tb0.cloudfront.net", 37 | 'https://d1w2poirtb3as9.cloudfront.net', 38 | 'https://d6d4ismr40iw.cloudfront.net', 39 | 'https://d1g1f25tn8m2e6.cloudfront.net', 40 | 'https://dykkng5hnh52u.cloudfront.net', 41 | 'https://d2dylwb3shzel1.cloudfront.net', 42 | 'https://d2xmjdvx03ij56.cloudfront.net'] 43 | 44 | find1c = 0 45 | 46 | 47 | 48 | def linkChecker(link): # twitchtracker ve streamscharts destekli 49 | global streamername 50 | global vodID 51 | link = link.split('/') 52 | if link[2] == 'twitchtracker.com': 53 | streamername = link[3] 54 | vodID = link[5] 55 | return 1 56 | elif link[2] == 'streamscharts.com': 57 | streamername = link[4] 58 | vodID = link[6] 59 | return 2 60 | elif link[0] == 'twitchtracker.com': 61 | streamername = link[1] 62 | vodID = link[3] 63 | return 3 64 | elif link[0] == 'streamscharts.com': 65 | streamername = link[2] 66 | vodID = link[4] 67 | return 4 68 | else: 69 | print('Check the link again. (An unsupported link has been entered or the link has an error.)') 70 | return 0 71 | 72 | 73 | def linkTimeCheck(link): 74 | # global timestamp 75 | if linkChecker(link) == 2 or linkChecker(link) == 4: # streamscharts 76 | print('Date and Time are checking..') 77 | r = requests.get(link) 78 | 79 | soup = BeautifulSoup(r.content, 'html.parser') 80 | 81 | gelenveri = soup.find_all('time', 'ml-2 font-bold') 82 | 83 | 84 | try: 85 | time = gelenveri[0].text 86 | 87 | except: 88 | print('You probably got into cloudflare for bots.(could not find time data) There is nothing I can do for this error for now. \n' 89 | 'Please fork if you can bypass this cloudflare. \n' 90 | 'You will not get an error when you try again after a while. \n' 91 | 'So try again after a while. ') 92 | 93 | return 94 | 95 | 96 | if '\n' in time: 97 | time = time.replace('\n', '') 98 | 99 | if ',' in time: 100 | time = time.replace(',', '') 101 | 102 | print(f'Clock data: {time}') 103 | print(f'Streamer name: {streamername} \nvodID: {vodID}') 104 | 105 | time = time.split(' ') 106 | 107 | hoursandminut = time[3] 108 | 109 | hoursandminut = hoursandminut.split(':') 110 | 111 | day = int(time[0]) 112 | 113 | month = time[1] 114 | 115 | year = int(time[2]) 116 | 117 | hour = int(hoursandminut[0]) 118 | 119 | minute = int(hoursandminut[1]) 120 | 121 | def months(month): 122 | if month == 'Jan': 123 | return 1 124 | if month == 'Feb': 125 | return 2 126 | if month == 'Mar': 127 | return 3 128 | if month == 'Apr': 129 | return 4 130 | if month == 'May': 131 | return 5 132 | if month == 'Jun': 133 | return 6 134 | if month == 'Jul': 135 | return 7 136 | if month == 'Aug': 137 | return 8 138 | if month == 'Sep': 139 | return 9 140 | if month == 'Oct': 141 | return 10 142 | if month == 'Nov': 143 | return 11 144 | if month == 'Dec': 145 | return 12 146 | else: 147 | return 0 148 | 149 | month = months(month) 150 | 151 | second = 60 152 | 153 | timestamp = str(year) + '-' + str(month) + '-' + str(day) + '-' + str(hour) + '-' + str(minute) + '-' + str( 154 | second) 155 | 156 | print(f'timestamp', timestamp) 157 | return timestamp 158 | 159 | elif linkChecker(link) == 1 or linkChecker(link) == 3: #twitchtracker 160 | print('Date and Time are checking...') 161 | 162 | useragent = ["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36", 163 | "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36", 164 | "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36", 165 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36", 166 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36", 167 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:103.0) Gecko/20100101 Firefox/103.0", 168 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 12.5; rv:103.0) Gecko/20100101 Firefox/103.0", 169 | "Mozilla/5.0 (X11; Linux i686; rv:103.0) Gecko/20100101 Firefox/103.0", 170 | "Mozilla/5.0 (Linux x86_64; rv:103.0) Gecko/20100101 Firefox/103.0", 171 | "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:103.0) Gecko/20100101 Firefox/103.0", 172 | "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:103.0) Gecko/20100101 Firefox/103.0", 173 | "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:103.0) Gecko/20100101 Firefox/103.0", 174 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0", 175 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 12.5; rv:102.0) Gecko/20100101 Firefox/102.0", 176 | "Mozilla/5.0 (X11; Linux i686; rv:102.0) Gecko/20100101 Firefox/102.0", 177 | "Mozilla/5.0 (Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0", 178 | "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:102.0) Gecko/20100101 Firefox/102.0", 179 | "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0", 180 | "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0", 181 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Safari/605.1.15", 182 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36 Edg/103.0.1264.77", 183 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36 Edg/103.0.1264.77", 184 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36'] 185 | 186 | 187 | header = { 188 | 'user-agent': f'{random.choice(useragent)}' 189 | } 190 | 191 | 192 | r = requests.get(link, headers=header) 193 | 194 | soup = BeautifulSoup(r.content, 'html.parser') 195 | 196 | gelenveri = soup.find_all('div', 'stream-timestamp-dt') 197 | 198 | 199 | try: 200 | time = gelenveri[0].text 201 | except: 202 | print('You probably got into cloudflare for bots.(could not find time data) There is nothing I can do for this error for now. \n' 203 | 'Please fork if you can bypass this cloudflare. \n' 204 | 'You will not get an error when you try again after a while. \n' 205 | 'So try again after a while. ') 206 | return 207 | 208 | 209 | print(f'Clock data: {gelenveri[0].text}') 210 | print(f'Streamer name: {streamername} \nvodID: {vodID}') 211 | 212 | firstandsecond_time = gelenveri[0].text.split(' ') 213 | 214 | first_time = firstandsecond_time[0].split('-') 215 | second_time = firstandsecond_time[1].split(':') 216 | 217 | day = int(first_time[2]) 218 | 219 | month = int(first_time[1]) 220 | 221 | year = int(first_time[0]) 222 | 223 | hour = int(second_time[0]) 224 | 225 | minute = int(second_time[1]) 226 | 227 | second = int(second_time[2]) 228 | 229 | timestamp = str(year) + '-' + str(month) + '-' + str(day) + '-' + str(hour) + '-' + str(minute) + '-' + str( 230 | second) 231 | 232 | print(f'timestamp', timestamp) 233 | 234 | return timestamp 235 | 236 | elif linkChecker(link) == 0: 237 | print('You entered an unsupported link.') 238 | return 0 239 | else: 240 | print('An unknown error has occurred.') 241 | return None 242 | 243 | 244 | def totimestamp(dt, epoch=datetime.datetime(1970, 1, 1)): 245 | td = dt - epoch 246 | return (td.microseconds + (td.seconds + td.days * 86400) * 10 ** 6) / 10 ** 6 247 | 248 | 249 | def find(timestamp, domain): 250 | timestamp = timestamp.split('-') 251 | year = int(timestamp[0]) 252 | month = int(timestamp[1]) 253 | day = int(timestamp[2]) 254 | hour = int(timestamp[3]) 255 | minute = int(timestamp[4]) 256 | second = int(timestamp[5]) 257 | 258 | def check(url): 259 | global find1c 260 | try: 261 | urllib.request.urlopen(url) 262 | except urllib.error.HTTPError: 263 | pass 264 | else: 265 | print(url) 266 | #webbrowser.open(url) 267 | find1c = 1 268 | 269 | threads = [] 270 | 271 | if second == 60: 272 | for i in range(60): 273 | seconds = i 274 | 275 | td = datetime.datetime(year, month, day, hour, minute, seconds) 276 | 277 | converted_timestamp = totimestamp(td) 278 | 279 | formattedstring = streamername + "_" + vodID + "_" + str(int(converted_timestamp)) 280 | 281 | hash = str(hashlib.sha1(formattedstring.encode('utf-8')).hexdigest()) 282 | 283 | requiredhash = hash[:20] 284 | 285 | finalformattedstring = requiredhash + '_' + formattedstring 286 | 287 | url = f"{domain}/{finalformattedstring}/chunked/index-dvr.m3u8" 288 | 289 | threads.append(Thread(target=check, args=(url,))) 290 | 291 | for i in threads: 292 | i.start() 293 | for i in threads: 294 | i.join() 295 | else: 296 | td = datetime.datetime(year, month, day, hour, minute, second) 297 | 298 | converted_timestamp = totimestamp(td) 299 | 300 | formattedstring = streamername + "_" + vodID + "_" + str(int(converted_timestamp)) 301 | 302 | hash = str(hashlib.sha1(formattedstring.encode('utf-8')).hexdigest()) 303 | 304 | requiredhash = hash[:20] 305 | 306 | finalformattedstring = requiredhash + '_' + formattedstring 307 | 308 | url = f"{domain}/{finalformattedstring}/chunked/index-dvr.m3u8" 309 | 310 | threads.append(Thread(target=check, args=(url,))) 311 | 312 | for i in threads: 313 | i.start() 314 | for i in threads: 315 | i.join() 316 | 317 | 318 | if len(sys.argv) < 2: 319 | # just python and recover.py as 1st argument 320 | print('Find the broadcast link you want from Twitchtracker or Streamscharts site.') 321 | link = str(input('Enter the link:')) 322 | else: 323 | link = sys.argv[1] 324 | 325 | timestamp = linkTimeCheck(link) 326 | 327 | if timestamp == None: 328 | quit() 329 | 330 | for domain in domains: 331 | if find1c == 0: 332 | find(timestamp, domain) 333 | else: 334 | pass 335 | 336 | if find1c == 0: 337 | print('No File Found on Twitch Servers.') 338 | 339 | if find1c == 1: 340 | time.sleep(10) 341 | 342 | 343 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4 2 | requests 3 | --------------------------------------------------------------------------------