├── .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 |
4 |
5 |
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 | 
3 | 
4 | 
5 | 
6 | 
7 | # TwitchRecover
8 |
9 | 
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 |
--------------------------------------------------------------------------------