1from apify import Actor
2from playwright.async_api import async_playwright, expect
3from bs4 import BeautifulSoup
4import math
5import numpy
6
7
8
9browser = None
10context = None
11page = None
12readable_content = None
13
14
15was_above_target = None
16iterations = 0
17page_jump = 1000
18default_page_jump = 1000
19page_jump_when_close = 50
20current_page_int = 1
21current_target_lp = 0
22close_to_target_threshold = 20
23searching_for_MR = False
24ranking_list = None
25all_users_on_page = None
26last_user_lp_int = 0
27overshot_1500_once = False
28began_fine_search = False
29final_page_MR = 0
30
31
32email = ""
33password = ""
34start_pages = []
35search_only_this_rank = 0
36initial_page_jump = 0
37skip_LP = False
38skip_MR = False
39
40
41base_start_url = "https://www.streetfighter.com/6/buckler/ranking/league?character_filter=2&character_id=luke&platform=1&user_status=1&home_filter=1&home_category_id=0&home_id=1&league_rank=0&page="
42login_page = "https://www.streetfighter.com/6/buckler/auth/loginep?redirect_url=/?status=login"
43master_url = "https://www.streetfighter.com/6/buckler/ranking/master?character_filter=2&character_id=luke&platform=1&home_filter=1&home_category_id=0&home_id=1&page=1&season_type=1"
44
45target_lp_per_rank = [
46 25000,
47 23800, 22600, 21400, 20200, 19000,
48 17800, 16600, 15400, 14200, 13000,
49 12200, 11400, 10600, 9800, 9000,
50 8200, 7400, 6600, 5800, 5000,
51 4600, 4200, 3800, 3400, 3000,
52 2600, 2200, 1800, 1400, 1000,
53 800, 600, 400, 200
54 ]
55
56ranks = [
57 "Master",
58 "Diamond 5", "Diamond 4", "Diamond 3", "Diamond 2", "Diamond 1",
59 "Platinum 5", "Platinum 4", "Platinum 3", "Platinum 2", "Platinum 1",
60 "Gold 5", "Gold 4", "Gold 3", "Gold 2", "Gold 1",
61 "Silver 5", "Silver 4", "Silver 3", "Silver 2", "Silver 1",
62 "Bronze 5", "Bronze 4", "Bronze 3", "Bronze 2", "Bronze 1",
63 "Iron 5", "Iron 4", "Iron 3", "Iron 2", "Iron 1",
64 "Rookie 5", "Rookie 4", "Rookie 3", "Rookie 2"
65]
66
67estimated_start_pages = [
68 6924,
69 7345, 8187, 9310, 10756, 13511,
70 15264, 17165, 19529, 22393, 28175,
71 30096, 31578, 33109, 34669, 37285,
72 38229, 39900, 41955, 41749, 44936,
73 47992, 49314, 50827, 52197, 54275,
74 55075, 56391, 57853, 59267, 59273,
75 62472, 63021, 63673, 64285
76]
77
78target_MR_per_bucket = [
79 1000, 1100, 1200, 1300, 1400,
80 1500, 1501, 1600, 1700, 1800,
81 1900, 2000
82]
83
84estimated_start_pages_MR = [
85 4700, 4700, 4700, 4236, 3678,
86 2520, 1932, 845, 405, 186,
87 80, 34
88]
89
90MR_bucket_names = [
91 "Unrated (No MR)", "Master 1",
92 "Master 2", "Master 3", "Master 4", "Master 5", "Master 6",
93 "Unrated (1500 MR)", "Master 7", "Master 8", "Master 9",
94 "Master 10", "Master 11", "Master 12"
95]
96
97
98total_players_int = 0
99page_of_each_rank_start = []
100placement_of_users_per_rank = []
101players_in_each_rank = []
102
103placement_of_users_per_MR_bucket = []
104page_of_each_rank_start_MR = []
105
106
107async def main():
108 async with Actor:
109
110 actor_input = await Actor.get_input() or {}
111 GetInfoFromActorInput(actor_input)
112
113 if not skip_LP:
114
115 async with async_playwright() as playwright:
116
117
118 await DoSearch(playwright, Actor)
119
120
121 if search_only_this_rank is not None:
122 await SendOutputToApify(Actor)
123
124 if not skip_MR and search_only_this_rank is None:
125
126 async with async_playwright() as playwright:
127 await DoSearchForMR(playwright)
128
129
130 await SendMRResultsToApify(Actor)
131
132
133async def DoSearch(playwright, Actor):
134 """Performs the entire operation and stores the results in global variables"""
135 global placement_of_users_per_rank
136 global players_in_each_rank
137 global total_players_int
138 global searching_for_MR
139
140 searching_for_MR = False
141
142
143 if (search_only_this_rank is not None):
144 start_index = search_only_this_rank
145 end_index = search_only_this_rank + 1
146 print("Searching for " + ranks[start_index])
147
148
149 else:
150 start_index = 0
151 end_index = len(ranks)
152 print("Searching for all ranks")
153
154
155 for i in range(start_index, end_index):
156
157 await CreateBrowser(playwright)
158
159
160 if total_players_int == 0:
161 total_players_int = await GetTotalPlayers()
162
163 target_lp = target_lp_per_rank[i]
164 start_page = start_pages[i]
165 rank_name = ranks[i]
166 print("Searching for " + rank_name + "...")
167
168
169 placement_of_first_user = await FindPlacingOfFirstUserInRank(start_page, target_lp)
170 print("\n" + rank_name + " begins at #" + str(placement_of_first_user) + "\nThere are " + str(total_players_int) + " players in total\n")
171
172 percentile = (total_players_int - placement_of_first_user) / total_players_int
173 percentile = percentile * 100
174 percentile = round(percentile, 2)
175 print(rank_name + " is the " + str(percentile) + "th percentile\n")
176
177
178 placement_of_users_per_rank.append(placement_of_first_user)
179
180
181 if search_only_this_rank is None:
182 await SendSingleOutputToApify(Actor, i)
183
184async def CreateBrowser(playwright):
185 """This will launch the browser and log you into CFN. Call this first."""
186
187 global browser
188 global context
189 global page
190 global iterations
191 global page_jump
192
193 browser = await playwright.firefox.launch(headless = Actor.config.headless)
194 context = await browser.new_context()
195 page = await context.new_page()
196
197
198 await page.goto(login_page)
199 await page.content()
200 await page.wait_for_timeout(1000)
201
202
203
204 await InputAgeCheck()
205 await page.content()
206 await page.wait_for_timeout(1000)
207
208
209
210
211
212
213 await LogIn()
214 await page.content()
215 await page.wait_for_timeout(1000)
216
217
218
219
220
221
222 start_url = base_start_url + str(1)
223 await page.goto(start_url, timeout=60000)
224
225async def FindPlacingOfFirstUserInRank(start_page, target_lp):
226 """This is our main method. If all goes according to plan, you can await this, and it'll return the placing of the first user in the target rank."""
227 global current_target_lp
228 global page_jump
229 global page_of_each_rank_start
230 global began_fine_search
231
232 current_target_lp = target_lp
233
234 if (start_page is None):
235 Actor.log.error('Start page is null!')
236 return
237
238
239 start_url = base_start_url + str(start_page)
240 await page.goto(start_url, timeout=60000);
241
242 print("Start URL: " + page.url)
243
244
245
246
247 pagination = page.locator("div[class='ranking_pc__LlGv4']").locator("div[class='ranking_ranking_pager__top__etBHR']").locator("ul[class='pagination']").first
248 await expect(pagination).to_be_visible(timeout=30000)
249
250 page_jump = initial_page_jump or 1000
251 iterations = 0
252 highest_user_in_last_rank = None
253 began_fine_search = False
254
255
256 while True:
257
258 highest_user_in_last_rank = await SearchForBeginningOfRank(pagination)
259
260
261 if highest_user_in_last_rank is not None:
262 break
263
264
265 iterations += 1
266 if iterations > 30:
267 break
268
269 placement_str = await highest_user_in_last_rank.locator("dt").text_content()
270 placement_str = placement_str.strip()
271 placement_str = placement_str[1:]
272 placement_int = int(placement_str)
273
274 lp_str = await highest_user_in_last_rank.locator("dd").text_content()
275 lp_str = lp_str[:-3]
276
277 username = await highest_user_in_last_rank.locator("span[class='ranking_name__El29_']").text_content()
278
279
280 print("\nHighest ranked user in previous rank: " + str(username))
281 print("LP: " + lp_str + "\nPosition: " + placement_str + "\nPage: " + str(current_page_int) + "\nURL: " + page.url)
282
283 page_of_each_rank_start.append(current_page_int)
284
285
286
287 return placement_int + 1
288
289async def RefreshInfoAboutCurrentPage(pagination):
290 global current_page_int
291
292
293 current_page = pagination.locator("xpath=/li[@class='active']").first
294 await expect(current_page).to_be_visible()
295
296 current_page_text = await current_page.text_content()
297 current_page_str = str(current_page_text)
298 current_page_int = int(current_page_str)
299 print("\nCurrent page: " + current_page_str)
300
301async def RefreshInfoAboutUsersOnPage():
302 global ranking_list
303 global all_users_on_page
304 global last_user_lp_int
305
306
307 ranking_list = page.locator("xpath=//ul[@class='ranking_ranking_list__szajj']").first
308 await expect(ranking_list).to_be_visible()
309
310
311 all_users_on_page = await ranking_list.locator("xpath=/li").all()
312 last_user = all_users_on_page[len(all_users_on_page) - 1]
313 await expect(last_user).to_be_visible()
314
315 last_user_lp_str = await last_user.locator("dd").text_content()
316 last_user_lp_str = last_user_lp_str[:-3]
317 last_user_lp_int = int(last_user_lp_str)
318 print("LP of the last user on the page: " + str(last_user_lp_int) + "\nWe're looking for " + str(current_target_lp))
319
320async def SearchForBeginningOfRank(pagination):
321 """Each time this method is called, we'll load a page and attempt to find the highest-ranked user in the previous rank. This would tell us where the current rank begins.
322 If the loaded page doesn't contain the highest ranked user, nothing will be returned, and you'll have to call this again"""
323 global was_above_target
324 global page_jump
325 global began_fine_search
326
327 await RefreshInfoAboutCurrentPage(pagination)
328 await RefreshInfoAboutUsersOnPage()
329
330
331 highest_lp_in_last_rank = current_target_lp - 1
332
333
334 if last_user_lp_int > highest_lp_in_last_rank and not began_fine_search:
335
336 if was_above_target == True:
337 page_jump = math.floor(page_jump / 2)
338
339
340 if last_user_lp_int == current_target_lp:
341 if abs(page_jump) > page_jump_when_close:
342 page_jump = page_jump_when_close
343
344 page_jump = abs(page_jump)
345 print("Page jump: " + str(page_jump))
346
347 was_above_target = False
348
349
350 elif last_user_lp_int < highest_lp_in_last_rank - close_to_target_threshold and not began_fine_search:
351
352 if was_above_target == False:
353 page_jump = math.floor(page_jump / 2)
354
355 page_jump = -abs(page_jump)
356 print("Page jump: " + str(page_jump))
357
358 was_above_target = True
359
360
361 else:
362 print("We're very close to our target! It's time to start incrementing one page at a time")
363
364 began_fine_search = True
365
366
367 lp_list = await GetAllLpOnPage(ranking_list)
368
369
370 if not current_target_lp in lp_list:
371 print("We overshot a little, so we'll have to move backward one page at a time to find the first user with an LP of " + str(current_target_lp))
372 page_jump = -1
373
374
375 else:
376 target_index = -1
377
378 for i in range(len(lp_list)):
379 if lp_list[i] < current_target_lp:
380 target_index = i
381 break
382
383
384 return all_users_on_page[target_index]
385
386
387
388
389 if page_jump == 0:
390 began_fine_search = True
391
392
393 next_page = current_page_int + page_jump
394 max_page = final_page_MR if searching_for_MR else 99999
395 next_page = numpy.clip(next_page, 1, max_page)
396 print("Next page: " + str(next_page))
397
398 target_url = GetURLForPage(next_page)
399
400
401 await page.goto(target_url, timeout=180000)
402 await page.wait_for_url(target_url)
403 await page.content()
404
405async def DoSearchForMR(playwright):
406 global placement_of_users_per_MR_bucket
407 global default_page_jump
408 global close_to_target_threshold
409 global searching_for_MR
410 global page_jump_when_close
411
412
413
414 default_page_jump = 250
415
416
417 await DetermineLastPage(playwright)
418 print("The final page is " + str(final_page_MR))
419
420
421
422 close_to_target_threshold = 1
423 page_jump_when_close = 5
424 searching_for_MR = True
425
426
427 for i in range(0, len(target_MR_per_bucket)):
428
429 await CreateBrowser(playwright)
430
431 target_MR = target_MR_per_bucket[i]
432 start_page = estimated_start_pages_MR[i]
433 rank_name = MR_bucket_names[i + 2]
434
435 print("Searching for " + rank_name + "...")
436
437
438 placement_of_first_user = await FindPlacingOfFirstUserInMRBucket(start_page, target_MR)
439 print("\n" + rank_name + " begins at #" + str(placement_of_first_user))
440
441
442 placement_of_users_per_MR_bucket.append(placement_of_first_user)
443
444async def FindPlacingOfFirstUserInMRBucket(start_page, target_MR):
445 """This is our main method. If all goes according to plan, you can await this, and it'll return the placing of the first user in the target rank."""
446 global page_jump
447 global page_of_each_rank_start_MR
448 global current_target_lp
449 global began_fine_search
450
451 current_target_lp = target_MR
452
453
454 start_url = master_url.replace("page=1", "page=" + str(start_page))
455 await page.goto(start_url, timeout=60000)
456
457 print("Start URL: " + page.url)
458
459
460 pagination = page.locator("div[class='ranking_pc__LlGv4']").locator("div[class='ranking_ranking_pager__top__etBHR']").locator("ul[class='pagination']").first
461 await expect(pagination).to_be_visible(timeout=30000)
462
463 page_jump = initial_page_jump or default_page_jump
464 iterations = 0
465 highest_user_in_last_rank = None
466 began_fine_search = False
467
468
469 while True:
470
471
472 if current_target_lp == 1501:
473 highest_user_in_last_rank = await SearchForBeginningOfRankFromAbove(pagination)
474 else:
475 highest_user_in_last_rank = await SearchForBeginningOfRank(pagination)
476
477
478 if highest_user_in_last_rank is not None:
479 break
480
481
482 iterations += 1
483 if iterations > 30:
484 break
485
486 placement_str = await highest_user_in_last_rank.locator("dt").text_content()
487 placement_str = placement_str.strip()
488 placement_str = placement_str[1:]
489 placement_int = int(placement_str)
490
491 lp_str = await highest_user_in_last_rank.locator("dd").text_content()
492 lp_str = lp_str[:-3]
493
494 username = await highest_user_in_last_rank.locator("span[class='ranking_name__El29_']").text_content()
495
496
497 print("\nHighest ranked user in previous rank: " + str(username))
498 print("LP: " + lp_str + "\nPosition: " + placement_str + "\nPage: " + str(current_page_int) + "\nURL: " + page.url)
499
500 page_of_each_rank_start_MR.append(current_page_int)
501
502
503
504 return placement_int + 1
505
506async def SearchForBeginningOfRankFromAbove(pagination):
507 """There are some finnicky differences between this and SearchForBeginningOfRank that we might as well make a separate method"""
508 global was_above_target
509 global page_jump
510 global overshot_1500_once
511 global began_fine_search
512
513 await RefreshInfoAboutCurrentPage(pagination)
514 await RefreshInfoAboutUsersOnPage()
515
516
517 lowest_mr_in_next_rank = current_target_lp
518 highest_mr_in_prev_rank = current_target_lp - 1
519
520 print("Annoying exception: we're starting from above and trying to find any page with " + str(lowest_mr_in_next_rank) + "\nThen we'll iterate downward to find where it began")
521
522
523 if last_user_lp_int > lowest_mr_in_next_rank and not began_fine_search:
524
525 if was_above_target == True:
526 page_jump = math.floor(page_jump / 2)
527
528
529 if last_user_lp_int == current_target_lp:
530 if abs(page_jump) > page_jump_when_close:
531 page_jump = page_jump_when_close
532
533 page_jump = abs(page_jump)
534 print("Page jump: " + str(page_jump))
535
536 was_above_target = False
537
538
539 elif last_user_lp_int < lowest_mr_in_next_rank and not began_fine_search:
540
541 if was_above_target == False:
542 page_jump = math.floor(page_jump / 2)
543
544 page_jump = -abs(page_jump)
545 print("Page jump: " + str(page_jump))
546
547 was_above_target = True
548
549
550 else:
551 print("We're very close to our target! It's time to start incrementing one page at a time")
552
553 began_fine_search = True
554
555
556 lp_list = await GetAllLpOnPage(ranking_list)
557
558
559 if not overshot_1500_once:
560 if not highest_mr_in_prev_rank in lp_list:
561 print("Trying to find the last user with an LP of " + str(highest_mr_in_prev_rank))
562 page_jump = 5
563
564
565 else:
566 overshot_1500_once = True
567 page_jump = -1
568
569 else:
570 page_jump = -1
571
572
573 if lowest_mr_in_next_rank in lp_list:
574 target_index = -1
575
576 for i in range(len(lp_list)):
577 if lp_list[i] < current_target_lp:
578 target_index = i
579 break
580
581
582 return all_users_on_page[target_index]
583
584
585
586
587 if page_jump == 0:
588 began_fine_search = True
589
590
591 next_page = current_page_int + page_jump
592 next_page = numpy.clip(next_page, 1, final_page_MR)
593 print("Next page: " + str(next_page))
594
595 target_url = GetURLForPage(next_page)
596
597
598 await page.goto(target_url, timeout=180000)
599 await page.wait_for_url(target_url)
600 await page.content()
601
602
603
604def GetInfoFromActorInput(actor_input):
605 global email
606 global password
607 global start_pages
608 global search_only_this_rank
609 global initial_page_jump
610 global skip_LP
611 global skip_MR
612
613 email = actor_input.get('email')
614 password = actor_input.get('password')
615
616 start_pages = estimated_start_pages
617 start_page_override = actor_input.get('start_page')
618
619 initial_page_jump = actor_input.get('initial_page_jump')
620
621 search_only_this_rank = actor_input.get('rank_to_search')
622
623 if search_only_this_rank is not None and start_page_override is not None:
624 start_pages[search_only_this_rank] = start_page_override
625
626 start_page_array = actor_input.get('start_page_array')
627
628 if search_only_this_rank is None and start_page_array is not None:
629 if len(start_page_array) == len(start_pages):
630 for i in range(0, len(start_pages)):
631 start_pages[i] = start_page_array[i]
632 else:
633 Apify.log.error("Error! Start page override length: " + str(len(start_page_array)) + "\nShould be equal to " + str(len(start_pages)))
634
635 skip_LP = actor_input.get('skip_LP')
636 skip_MR = actor_input.get('skip_MR')
637
638async def SendMRResultsToApify(Actor):
639 output = []
640
641 for i in range(len(placement_of_users_per_MR_bucket)):
642 rank_name = "Master " + (str(i + 1))
643 current_rank_starts_at = placement_of_users_per_MR_bucket[i]
644
645 output.append(
646 {
647 "Rank Name": rank_name,
648 "Starts at Placing": current_rank_starts_at,
649 "Page of Rank Start": page_of_each_rank_start_MR[i]
650 }
651 )
652
653 Actor.log.info("Output: " + str(output))
654
655 dataset = await Actor.open_dataset()
656 await dataset.push_data(output)
657
658async def SendSingleOutputToApify(Actor, index):
659 output =[(
660 {
661 "Rank Name": ranks[index],
662 "Starts at Placing": placement_of_users_per_rank[index],
663 "Page of Rank Start": page_of_each_rank_start[index]
664 }
665 )]
666
667 Actor.log.info("Output: " + str(output))
668
669 dataset = await Actor.open_dataset()
670 await dataset.push_data(output)
671
672async def SendOutputToApify(Actor):
673 output = []
674
675
676 if search_only_this_rank is not None:
677 output.append(
678 {
679 "Rank Name": ranks[search_only_this_rank],
680 "Starts at Placing": placement_of_users_per_rank[0],
681 "Page of Rank Start": page_of_each_rank_start[0]
682 }
683 )
684
685 Actor.log.info("Output: " + str(output))
686
687 dataset = await Actor.open_dataset()
688 await dataset.push_data(output)
689
690
691 else:
692 for i in range(len(placement_of_users_per_rank)):
693 rank_name = ranks[i]
694 current_rank_starts_at = placement_of_users_per_rank[i]
695
696 output.append(
697 {
698 "Rank Name": rank_name,
699 "Starts at Placing": current_rank_starts_at,
700 "Page of Rank Start": page_of_each_rank_start[i]
701 }
702 )
703
704
705 prev_rank_starts_at = placement_of_users_per_rank[i - 1] if i > 0 else 0
706 players_in_rank = current_rank_starts_at - prev_rank_starts_at
707
708 percentage = GetPercentageString(players_in_rank, total_players_int)
709 print(rank_name + " contains " + str(players_in_rank) + " players\nIt represents " + percentage + " of the playerbase")
710
711 players_in_each_rank.append(players_in_rank)
712
713 Actor.log.info("Output: " + str(output))
714
715 dataset = await Actor.open_dataset()
716 await dataset.push_data(output)
717
718 Actor.log.info("Players in each rank:\n" + str(players_in_each_rank).replace(",", "\n"))
719
720
721
722async def InputAgeCheck():
723 """Fills out the age check dropdown"""
724
725 dropdown = page.locator("select[id='country']")
726 await dropdown.select_option("Canada")
727
728
729 await page.locator("select[id='birthYear']").select_option('1992')
730 await page.locator("select[id='birthMonth']").select_option('1')
731 await page.locator("select[id='birthDay']").select_option('15')
732
733
734 await page.locator("button[name='submit']").click()
735
736
737 await page.wait_for_timeout(3000)
738
739 print("Passed age check!\n")
740
741async def LogIn():
742
743 email_field = page.locator("input[type='email']")
744 await email_field.fill(email)
745
746 pw_field = page.locator("input[type='password']")
747 await pw_field.fill(password)
748
749
750 await page.locator("button[name='submit']").click()
751
752
753 await page.wait_for_timeout(10000)
754
755 print("Logged in!\n")
756
757
758
759async def GetAllLpOnPage(ranking_list):
760 children = await ranking_list.locator("xpath=/li").all()
761 output = []
762
763
764 for i in range(len(children)):
765 userLi = children[i]
766
767
768 lpRaw = await userLi.locator("dd").text_content()
769 lpStr = str(lpRaw)
770 lpStr = lpStr[:-3]
771 lpInt = int(lpStr)
772
773
774 output.append(lpInt)
775
776 print("All LP on page: " + str(output))
777 return output
778
779async def GetPageHtml():
780 """We don't really need this, but it's useful for debugging"""
781 html = await page.content()
782 soup = BeautifulSoup(html, features = "html.parser")
783
784 global readable_content
785 readable_content = soup.prettify()
786
787async def GetTotalPlayers():
788 total_players_str = await page.locator("span[class='ranking_ranking_now__last__oqSXS']").last.text_content()
789 total_players_str = total_players_str[1:]
790 total_players_str = total_players_str.strip()
791 return int(total_players_str)
792
793def GetPercentageString(players_in_rank, total_players):
794 percentage_int = (players_in_rank / total_players) * 100
795 return str(percentage_int) + "%"
796
797def GetURLForPage(desired_page):
798 string_to_replace = "page=" + str(current_page_int)
799 string_to_add = "page=" + str(desired_page)
800
801 current_url = page.url
802 output = current_url.replace(string_to_replace, string_to_add)
803 return output
804
805async def DetermineLastPage(playwright):
806 """Navigates to the last page and records it in final_page_MR"""
807 global final_page_MR
808
809
810 await CreateBrowser(playwright)
811
812
813 await page.goto(master_url, timeout=60000)
814
815 pagination = page.locator("div[class='ranking_pc__LlGv4']").locator("div[class='ranking_ranking_pager__top__etBHR']").locator("ul[class='pagination']").first
816
817
818 await pagination.get_by_text("Last").click()
819 await page.wait_for_timeout(10000)
820
821
822 await RefreshInfoAboutCurrentPage(pagination)
823
824
825 final_page_MR = current_page_int
826
827 if final_page_MR == 1:
828 Actor.log.error('Failed to determine final page')
829 await Actor.exit()
830