#!/usr/bin/env python import os from datetime import datetime, timedelta import requests import bs4 from bs4 import BeautifulSoup, Tag from init_api import get_service import dotenv import asyncio import re class Match: def __init__(self, team1: str, team2: str, score1: int, score2: int, raw_time: str, stage: str, roundName: str, vods: list[str]): self.team1 = team1 self.team2 = team2 self.score1 = score1 self.score2 = score2 self.stage = stage self.roundName = roundName self.vods = vods self.time = datetime(*list(map(lambda v: int(v), raw_time.split(",")))) self.eventId = None def get_format(self) -> int: if self.stage == "Play-In": if self.roundName == "Qualifiers" or self.roundName == "EMEA vs NA": return 5 return 3 if self.stage == "Swiss": if self.roundName in ["Round 1", "Round 2"]: return 1 if self.roundName == "Round 3" and self.time.day == 22: return 1 return 3 if self.stage == "Knockout": return 5 def event_data(self): def shorten(link: str): # remove all query parameters except t= return re.sub(r'([?&](?!t=))([^&=]+=[^&=]+)', r'\1', link) vods = "\r\n".join([f'Game {i + 1}: {shorten(link)}' for i, link in enumerate(self.vods)]) return { "start": { "dateTime": self.time.isoformat() + "Z" }, "end": { "dateTime": (self.time + timedelta(hours=self.get_format())).isoformat() + "Z" }, "summary": f"{self.team1} - {self.team2}", "description": f"{self.stage} {self.roundName} BO{self.get_format()}\n{self.score1} - {self.score2}\n{vods}" } async def update_event(self): api = get_service() api.events().update( calendarId=os.environ["CALENDAR_ID"], eventId=self.eventId, body=self.event_data() ).execute() print(self) print(f"Event updated: {self.eventId}") print("") async def create_event(self): api = get_service() api.events().insert( calendarId=os.environ["CALENDAR_ID"], body=self.event_data() ).execute() print(self) print(f"Event created") print("") def is_in_calendar(self, calendar_events): for event in calendar_events: if datetime.fromisoformat(event["start"]["dateTime"][:-1]) == self.time: self.eventId = event["id"] return True return False def __repr__(self): return f"{self.team1:>3} - {self.time} - {self.team2:<3} | VODs: {len(self.vods)}" def fetch_matches(): sources = [ "https://lol.fandom.com/wiki/2023_Worlds_Qualifying_Series", "https://lol.fandom.com/wiki/2023_Season_World_Championship/Play-In", "https://lol.fandom.com/wiki/2023_Season_World_Championship/Main_Event" ] stages = ["Play-In", "Swiss", "Knockout"] matches: list[Match] = [] for src in sources: fetched_data = requests.get(src).text soup = BeautifulSoup(fetched_data, "html.parser") match_lists: list[Tag] = soup.find_all(attrs={"class": "matchlist"}) vodsTable: Tag = soup.find(attrs={"id", "md-table"}).find("tbody") for match_list in match_lists: roundName = match_list.find("tr").find("th").find(recursive=False, string=True).text raw_matches: list[Tag] = match_list.find_all(attrs={"class", "ml-row"}) # somehow identify stage... stage = stages[0] if (src == sources[0] or src == sources[1]) else \ (stages[1] if "Round" in roundName else stages[2]) for raw_match in raw_matches: raw_time = raw_match.find(attrs={"class", "TimeInLocal"}).text scores = raw_match.find_all(attrs={"class", "matchlist-score"}) scores = [0, 0] if not scores else tuple(map(lambda v: int(v.text), scores)) score1 = scores[0] score2 = scores[1] team1 = raw_match.find(attrs={"class", "matchlist-team1"}).find(attrs={"class", "teamname"}).text team2 = raw_match.find(attrs={"class", "matchlist-team2"}).find(attrs={"class", "teamname"}).text vods = [] if "TBD" not in [team1, team2]: def is_current_match_row(tr): both = tr.find_all(attrs={"class", "teamname"}) if len(both) == 2: return both[0].text == team1 and both[1].text == team2 return False roundStartRow: Tag = vodsTable.find(string=roundName).parent.parent currentRow: Tag = roundStartRow.find_next_sibling(is_current_match_row) for i in range(5): fullLink = currentRow.find("a", string="PB") if fullLink is not None: vods.append(fullLink["href"]) currentRow = currentRow.find_next_sibling("tr") if currentRow is None or len(currentRow.contents) > 5: break else: break matches.append(Match(team1, team2, score1, score2, raw_time, stage, roundName, vods)) return matches def get_calendar_events(): api = get_service() start = "2023-04-01T00:00:00Z" end = "2023-11-20T00:00:00Z" calendarId = os.environ.get("CALENDAR_ID") events = api.events().list( calendarId=calendarId, timeMin=start, timeMax=end, maxResults=2500, singleEvents=True, orderBy='startTime' ).execute()["items"] return events def delete_calendar_events(): api = get_service() calendarId = os.environ.get("CALENDAR_ID") events = get_calendar_events() for event in events: api.events().delete(calendarId=calendarId, eventId=event["id"]).execute() print("Deleted all calendar events") async def update(): dotenv.load_dotenv() events = get_calendar_events() tasks = [] for match in fetch_matches(): if match.is_in_calendar(events): tasks.append(match.update_event()) else: tasks.append(match.create_event()) await asyncio.gather(*tasks) if __name__ == "__main__": asyncio.run(update())