From c8f9c8016902c6a7729f95536f6ad27209b95b29 Mon Sep 17 00:00:00 2001 From: Vy Date: Tue, 9 Dec 2025 15:58:45 +1100 Subject: [PATCH 01/37] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1e83ddb5c..74d4af9f5 100755 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,7 @@ main.spec logs/* Pipfile* env +.env desktop.ini assets/controller/* assets/*_tmp From 5bd08a67f8c4c0e8feec7484854362ce51acea94 Mon Sep 17 00:00:00 2001 From: Vy Date: Tue, 9 Dec 2025 16:04:04 +1100 Subject: [PATCH 02/37] Update versions.json and requirements.txt --- assets/versions.json | 3 ++- dependencies/requirements.txt | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/versions.json b/assets/versions.json index 220dbc569..b8df19301 100755 --- a/assets/versions.json +++ b/assets/versions.json @@ -5,6 +5,7 @@ "game_tracker" ], "supported_providers": [ - "StartGG" + "StartGG", + "ParryGG" ] } \ No newline at end of file diff --git a/dependencies/requirements.txt b/dependencies/requirements.txt index 569c4a2ac..ed2039881 100755 --- a/dependencies/requirements.txt +++ b/dependencies/requirements.txt @@ -22,3 +22,6 @@ msgpack==1.1.0 qasync==0.27.1 tqdm==2.2.3 tzdata +grpcio +parrygg>=0.1.3 +python-dotenv From 69347650c7dec283ee83ee8fef7415bc61ad1b92 Mon Sep 17 00:00:00 2001 From: Vy Date: Tue, 9 Dec 2025 16:38:48 +1100 Subject: [PATCH 03/37] Update TSHWebServerActions.py Now also matches on parry.gg urls --- src/TSHWebServerActions.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/TSHWebServerActions.py b/src/TSHWebServerActions.py index 1ae14bf3f..dbf8a3bdf 100644 --- a/src/TSHWebServerActions.py +++ b/src/TSHWebServerActions.py @@ -516,7 +516,8 @@ def load_tournament(self, url=None): return "OK" else: validators = [ - QRegularExpression("start.gg/tournament/[^/]+/event[s]?/[^/]+") + QRegularExpression("start.gg/tournament/[^/]+/event[s]?/[^/]+"), + QRegularExpression("parry.gg/[^/]+/[^/]+/[^/]+") ] for validator in validators: @@ -533,6 +534,17 @@ def load_tournament(self, url=None): # Some URLs in startgg have eventS but the API doesn't work with that format url = url.replace("/events/", "/event/") + elif "parry.gg" in url: + # Remove the "_manage" part of admin urls first + url = url.replace("/_manage", "") + + matches = re.match( + "(.*parry.gg/[^/]*/[^/]*)", url) + + if matches: + url = matches.group() + + SettingsManager.Set("TOURNAMENT_URL", url) TSHTournamentDataProvider.instance.signals.tournament_url_update.emit(url) From 6d1d3db3158ff0fcf63a624be25b2c8304aa551d Mon Sep 17 00:00:00 2001 From: Vy Date: Tue, 9 Dec 2025 17:01:05 +1100 Subject: [PATCH 04/37] Update TSHTournamentDataProvider.py Now also matches on parry.gg urls. Includes commented code that will be uncommented once the parry.gg integration is further along. --- src/TSHTournamentDataProvider.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/TSHTournamentDataProvider.py b/src/TSHTournamentDataProvider.py index 1c2323732..50b720a1b 100644 --- a/src/TSHTournamentDataProvider.py +++ b/src/TSHTournamentDataProvider.py @@ -8,6 +8,8 @@ from .TSHGameAssetManager import TSHGameAssetManager from .TournamentDataProvider.TournamentDataProvider import TournamentDataProvider from .TournamentDataProvider.StartGGDataProvider import StartGGDataProvider +# TODO +# from .TournamentDataProvider.ParryGGDataProvider import ParryGGDataProvider from .Helpers.TSHVersionHelper import get_supported_providers from loguru import logger @@ -63,6 +65,10 @@ def SetGameFromProvider(self): if "start.gg" in self.provider.url: TSHGameAssetManager.instance.SetGameFromStartGGId( self.provider.videogame) + # TODO + # elif "parry.gg" in self.provider.url: + # TSHGameAssetManager.instance.SetGameFromParryGGId( + # self.provider.videogame) else: logger.error("Unsupported provider...") @@ -76,6 +82,10 @@ def SetTournament(self, url, initialLoading=False): TSHTournamentDataProvider.instance.provider = StartGGDataProvider( url, self.threadPool, self) url = TSHTournamentDataProvider.instance.provider.GetRealEventURL(url) + # TODO + # elif url is not None and "parry.gg" in url: + # TSHTournamentDataProvider.instance.provider = ParryGGDataProvider( + # url, self.threadPool, self) else: logger.error("Unsupported provider...") TSHTournamentDataProvider.instance.provider = None @@ -105,6 +115,10 @@ def SetTournamentSignal(self, url, initialLoading=False): if url is not None and "start.gg" in url: TSHTournamentDataProvider.instance.provider = StartGGDataProvider( url, self.threadPool, self) + # TODO + # elif url is not None and "parry.gg" in url: + # TSHTournamentDataProvider.instance.provider = ParryGGDataProvider( + # url, self.threadPool, self) else: logger.error("Unsupported provider...") TSHTournamentDataProvider.instance.provider = None @@ -139,7 +153,8 @@ def SetStartggEventSlug(self, mainWindow): okButton = QPushButton("OK") validators = [ QRegularExpression("start.gg/tournament/[^/]+/event[s]?/[^/]+"), - QRegularExpression("start.gg/admin/tournament/[^/]+/brackets/[^/]+") + QRegularExpression("start.gg/admin/tournament/[^/]+/brackets/[^/]+"), + QRegularExpression("parry.gg/[^/]+/[^/]+/[^/]+") ] def validateText(): @@ -173,6 +188,16 @@ def validateText(): # Some URLs in startgg have eventS but the API doesn't work with that format url = url.replace("/events/", "/event/") + elif "parry.gg" in url: + # Remove the "_manage" part of admin urls first + url = url.replace("/_manage", "") + + matches = re.match( + "(.*parry.gg/[^/]*/[^/]*)", url) + + if matches: + url = matches.group() + SettingsManager.Set("TOURNAMENT_URL", url) TSHTournamentDataProvider.instance.SetTournament( SettingsManager.Get("TOURNAMENT_URL")) @@ -193,6 +218,10 @@ def SetUserAccount(self, window, startgg=False): if (self.provider and self.provider.url and "start.gg" in self.provider.url) or startgg: window_text = QApplication.translate( "app", "Paste the URL to the player's StartGG profile") + # TODO + # elif (self.provider and self.provider.url and "parry.gg" in self.provider.url): + # window_text = QApplication.translate( + # "app", "Paste the URL to the player's ParryGG profile") else: logger.error(QApplication.translate( "app", "Invalid tournament data provider")) From 9849ff28dd6795ef5f8a06f7f2100b4862cae6d0 Mon Sep 17 00:00:00 2001 From: Vy Date: Tue, 9 Dec 2025 17:15:59 +1100 Subject: [PATCH 05/37] Comments about Parry.gg url matching --- src/TSHTournamentDataProvider.py | 4 ++++ src/TSHWebServerActions.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/TSHTournamentDataProvider.py b/src/TSHTournamentDataProvider.py index 50b720a1b..00e1ba6e8 100644 --- a/src/TSHTournamentDataProvider.py +++ b/src/TSHTournamentDataProvider.py @@ -154,6 +154,10 @@ def SetStartggEventSlug(self, mainWindow): validators = [ QRegularExpression("start.gg/tournament/[^/]+/event[s]?/[^/]+"), QRegularExpression("start.gg/admin/tournament/[^/]+/brackets/[^/]+"), + + # This could maybe become just "parry.gg/[^/]+/[^/]+" + # But that form of url directs to the /main/bracket page anyway, + # so it's rare to see it shortened unless typing the url manually. QRegularExpression("parry.gg/[^/]+/[^/]+/[^/]+") ] diff --git a/src/TSHWebServerActions.py b/src/TSHWebServerActions.py index dbf8a3bdf..c3fc8722b 100644 --- a/src/TSHWebServerActions.py +++ b/src/TSHWebServerActions.py @@ -517,6 +517,10 @@ def load_tournament(self, url=None): else: validators = [ QRegularExpression("start.gg/tournament/[^/]+/event[s]?/[^/]+"), + + # This could maybe become just "parry.gg/[^/]+/[^/]+" + # But that form of url directs to the /main/bracket page anyway, + # so it's rare to see it shortened unless typing the url manually. QRegularExpression("parry.gg/[^/]+/[^/]+/[^/]+") ] From 4ccf1b927cf20b19c480733490211c32b94b9790 Mon Sep 17 00:00:00 2001 From: Vy Date: Tue, 9 Dec 2025 23:13:53 +1100 Subject: [PATCH 06/37] Create ParryGGDataProvider.py Mostly boilerplate with lots of imports, but will be worked on. Just need a starting point. --- .../ParryGGDataProvider.py | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 src/TournamentDataProvider/ParryGGDataProvider.py diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py new file mode 100644 index 000000000..0e6f7e1e4 --- /dev/null +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -0,0 +1,155 @@ +import grpc +import os +import traceback + +from dotenv import load_dotenv +load_dotenv() +PARRYGG_API_KEY = os.getenv('PARRYGG_API_KEY') + +from google.protobuf.json_format import MessageToJson +from loguru import logger + +# ParryGG Imports +# TODO: Probably won't use all of these. +from parrygg.services.tournament_service_pb2_grpc import TournamentServiceStub +from parrygg.services.event_service_pb2_grpc import EventServiceStub +from parrygg.services.phase_service_pb2_grpc import PhaseServiceStub +from parrygg.services.bracket_service_pb2_grpc import BracketServiceStub +from parrygg.services.match_service_pb2_grpc import MatchServiceStub +from parrygg.services.match_game_service_pb2_grpc import MatchGameServiceStub +from parrygg.services.entrant_service_pb2_grpc import EntrantServiceStub +from parrygg.services.user_service_pb2_grpc import UserServiceStub +from parrygg.services.game_service_pb2_grpc import GameServiceStub + +# TODO: This is poor practice, will change later. +from parrygg.services.tournament_service_pb2 import * +from parrygg.services.event_service_pb2 import * +from parrygg.services.phase_service_pb2 import * +from parrygg.services.bracket_service_pb2 import * +from parrygg.services.match_service_pb2 import * +from parrygg.services.match_game_service_pb2 import * +from parrygg.services.entrant_service_pb2 import * +from parrygg.services.user_service_pb2 import * +from parrygg.services.game_service_pb2 import * + +from .TournamentDataProvider import TournamentDataProvider + +# Other Imports used by StartGGDataProvider +# TODO: May or may not be required. +from ..Helpers.TSHCountryHelper import TSHCountryHelper +from ..Helpers.TSHDictHelper import deep_get +from ..Helpers.TSHDirHelper import TSHResolve +from ..Helpers.TSHQtHelper import invokeSlot +from ..TSHGameAssetManager import TSHGameAssetManager +from ..TSHPlayerDB import TSHPlayerDB +import orjson +from ..Helpers.TSHLocaleHelper import TSHLocaleHelper +from ..TSHBracket import is_power_of_two +from ..Workers import Worker + +class ParryGGDataProvider(TournamentDataProvider): + metadata = None + channel = None + # Services will be created as they're required. + tournament_service = None + event_service = None + + def __init__(self, url, threadpool, tshTdp) -> None: + super().__init__(url, threadpool, tshTdp) + self.name = "ParryGG" + + # Initialize gRPC channel + self._initialize_grpc_connection() + + def _initialize_grpc_connection(self): + """Initialize gRPC connection to ParryGG API""" + try: + # Create gRPC channel + self.channel = grpc.secure_channel("api.parry.gg:443", grpc.ssl_channel_credentials()) + + # Set up metadata for authentication + if PARRYGG_API_KEY: + self.metadata = [("x-api-key", PARRYGG_API_KEY)] + else: + logger.warning("PARRYGG_API_KEY not found in environment variables") + self.metadata = [] + + except Exception as e: + logger.error(f"Failed to initialize ParryGG gRPC connection: {e}") + + def GetIconURL(self): + # TODO: Accessed early + pass + + def GetEntrants(self): + # TODO: Accessed early + pass + + def GetTournamentData(self, progress_callback=None, cancel_event=None): + # TODO: Accessed early + pass + + def GetMatch(self, setId, progress_callback=None, cancel_event=None): + pass + + def GetMatches(self, getFinished=False, progress_callback=None, cancel_event=None): + pass + + def GetStations(self, progress_callback=None, cancel_event=None): + pass + + def GetStreamQueue(self, streamName=None, progress_callback=None, cancel_event=None): + pass + + def GetStreamMatchId(self, streamName): + pass + + def GetStationMatchId(self, stationId): + pass + + def GetStationMatchsId(self, stationId): + pass + + def GetUserMatchId(self, user): + pass + + def GetRecentSets(self, id1, id2, videogame, callback): + pass + + def GetLastSets(self, playerId, playerNumber): + pass + + def GetCompletedSets(self): + pass + + def GetPlayerHistoryStandings(self, playerId, playerNumber, gameType): + pass + + def GetTournamentPhases(self, progress_callback=None, cancel_event=None): + # TODO: Accessed early + pass + + def GetTournamentPhaseGroup(self, id, progress_callback=None, cancel_event=None): + pass + + def GetStandings(self, playerNumber): + pass + + def GetFutureMatch(self, progrss_callback=None): + pass + + def GetFutureMatchesList(self, sets: object, progress_callback=None, cancel_event=None): + pass + + def cleanup(self): + """Properly cleanup gRPC channel and resources""" + if hasattr(self, 'channel') and self.channel: + try: + self.channel.close() + logger.info("gRPC channel closed successfully") + except Exception as e: + logger.error(f"Error closing gRPC channel: {e}") + + def __del__(self): + """Ensure cleanup happens when destroyed""" + self.cleanup() From 3d66da6d9cde83cd8e67e3a2c2ce2ee9ec72132e Mon Sep 17 00:00:00 2001 From: Vy Date: Thu, 11 Dec 2025 14:08:41 +1100 Subject: [PATCH 07/37] Initialisation work Services can be created as they're required with _create_service _initialize_grpc_connection was unnecessary, and the channel is now setup in __init__. --- .../ParryGGDataProvider.py | 66 +++++++++++++------ 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index 0e6f7e1e4..3f51e4c59 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -48,34 +48,58 @@ from ..Workers import Worker class ParryGGDataProvider(TournamentDataProvider): - metadata = None - channel = None - # Services will be created as they're required. tournament_service = None event_service = None + phase_service = None + bracket_service = None + match_service = None + matchgame_service = None + entrant_service = None + user_service = None + game_service = None def __init__(self, url, threadpool, tshTdp) -> None: super().__init__(url, threadpool, tshTdp) self.name = "ParryGG" + + # Set up metadata with API key + if PARRYGG_API_KEY: + self.metadata = [("x-api-key", PARRYGG_API_KEY)] + else: + logger.warning("PARRYGG_API_KEY not found in environment variables") + self.metadata = [] - # Initialize gRPC channel - self._initialize_grpc_connection() - - def _initialize_grpc_connection(self): - """Initialize gRPC connection to ParryGG API""" - try: - # Create gRPC channel - self.channel = grpc.secure_channel("api.parry.gg:443", grpc.ssl_channel_credentials()) - - # Set up metadata for authentication - if PARRYGG_API_KEY: - self.metadata = [("x-api-key", PARRYGG_API_KEY)] - else: - logger.warning("PARRYGG_API_KEY not found in environment variables") - self.metadata = [] - - except Exception as e: - logger.error(f"Failed to initialize ParryGG gRPC connection: {e}") + # Initialize gRPC + self.channel = grpc.secure_channel("api.parry.gg:443", grpc.ssl_channel_credentials()) + self._create_service("Tournament") + + # Get tournament and event slugs + self.tournament_slug = self.url.split("parry.gg/")[1].split("/")[0] + self.event_slug = self.url.split("parry.gg/")[1].split("/")[1] + + + def _create_service(self, service_name): + match service_name: + case "Tournament": + self.tournament_service = TournamentServiceStub(self.channel) + case "Event": + self.event_service = EventServiceStub(self.channel) + case "Phase": + self.phase_service = PhaseServiceStub(self.channel) + case "Bracket": + self.bracket_service = BracketServiceStub(self.channel) + case "Match": + self.match_service = MatchServiceStub(self.channel) + case "MatchGame": + self.matchgame_service = MatchGameServiceStub(self.channel) + case "Entrant": + self.entrant_service = EntrantServiceStub(self.channel) + case "User": + self.user_service = UserServiceStub(self.channel) + case "Game": + self.game_service = GameServiceStub(self.channel) + case _: + logger.error(f"Service {service_name} not recognized") def GetIconURL(self): # TODO: Accessed early From 71fef6bd5ee428b1ecea5046296b9c4d71f97c71 Mon Sep 17 00:00:00 2001 From: Vy Date: Sat, 13 Dec 2025 09:00:52 +1100 Subject: [PATCH 08/37] ParryGG: GetIconURL --- .../ParryGGDataProvider.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index 3f51e4c59..4202a514b 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -102,9 +102,22 @@ def _create_service(self, service_name): logger.error(f"Service {service_name} not recognized") def GetIconURL(self): - # TODO: Accessed early - pass - + if self.tournament_service is None: + self._create_service("Tournament") + + get_tournament_request = GetTournamentRequest() + get_tournament_request.tournament_slug = self.tournament_slug + response = self.tournament_service.GetTournament(get_tournament_request, metadata=self.metadata) + + for image in response.tournament.images: + # Look for banner image, can be replaced in future if icons are added. + if image.type == "IMAGE_TYPE_BANNER": + return image.url + + # Fallback to the ParryGG favicon if no suitable image is found. + logger.warning("No banner image found.") + return "https://parry.gg/assets/favicon-BgItT2B4.png" + def GetEntrants(self): # TODO: Accessed early pass From a70f2ae104d076657df26fc160761ac55d919d4f Mon Sep 17 00:00:00 2001 From: Vy Date: Sat, 13 Dec 2025 09:28:08 +1100 Subject: [PATCH 09/37] _create_service is now _setup_service Will verify the service is none before creating it, rather than relying on the service being verified as none before it is called. --- .../ParryGGDataProvider.py | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index 4202a514b..796ffca82 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -71,39 +71,46 @@ def __init__(self, url, threadpool, tshTdp) -> None: # Initialize gRPC self.channel = grpc.secure_channel("api.parry.gg:443", grpc.ssl_channel_credentials()) - self._create_service("Tournament") + self._setup_service("Tournament") # Get tournament and event slugs self.tournament_slug = self.url.split("parry.gg/")[1].split("/")[0] self.event_slug = self.url.split("parry.gg/")[1].split("/")[1] - - def _create_service(self, service_name): + def _setup_service(self, service_name): match service_name: case "Tournament": - self.tournament_service = TournamentServiceStub(self.channel) + if self.tournament_service is None: + self.tournament_service = TournamentServiceStub(self.channel) case "Event": - self.event_service = EventServiceStub(self.channel) + if self.event_service is None: + self.event_service = EventServiceStub(self.channel) case "Phase": - self.phase_service = PhaseServiceStub(self.channel) + if self.phase_service is None: + self.phase_service = PhaseServiceStub(self.channel) case "Bracket": - self.bracket_service = BracketServiceStub(self.channel) + if self.bracket_service is None: + self.bracket_service = BracketServiceStub(self.channel) case "Match": - self.match_service = MatchServiceStub(self.channel) + if self.match_service is None: + self.match_service = MatchServiceStub(self.channel) case "MatchGame": - self.matchgame_service = MatchGameServiceStub(self.channel) + if self.matchgame_service is None: + self.matchgame_service = MatchGameServiceStub(self.channel) case "Entrant": - self.entrant_service = EntrantServiceStub(self.channel) + if self.entrant_service is None: + self.entrant_service = EntrantServiceStub(self.channel) case "User": - self.user_service = UserServiceStub(self.channel) + if self.user_service is None: + self.user_service = UserServiceStub(self.channel) case "Game": - self.game_service = GameServiceStub(self.channel) + if self.game_service is None: + self.game_service = GameServiceStub(self.channel) case _: logger.error(f"Service {service_name} not recognized") def GetIconURL(self): - if self.tournament_service is None: - self._create_service("Tournament") + self._setup_service("Tournament") get_tournament_request = GetTournamentRequest() get_tournament_request.tournament_slug = self.tournament_slug From d4339d4f7caff688e49076e5371bed774e0aa705 Mon Sep 17 00:00:00 2001 From: Vy Date: Sat, 13 Dec 2025 09:28:30 +1100 Subject: [PATCH 10/37] ParryGG: GetEntrants --- .../ParryGGDataProvider.py | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index 796ffca82..c51fc4d87 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -126,8 +126,44 @@ def GetIconURL(self): return "https://parry.gg/assets/favicon-BgItT2B4.png" def GetEntrants(self): - # TODO: Accessed early - pass + self._setup_service("Event") + + get_event_entrants_request = GetEventEntrantsRequest() + get_event_entrants_request.event_identifier.event_slug_path.tournament_slug = self.tournament_slug + get_event_entrants_request.event_identifier.event_slug_path.event_slug = self.event_slug + response = self.event_service.GetEventEntrants(get_event_entrants_request, metadata=self.metadata) + + players = [] + + try: + for entrant in response.event_entrants: + # Assuming single user for now, no teams. + user = entrant.entrant.users[0] + + # Format the entrant correctly. + formatted_entrant = { + "prefix": "", + "gamerTag": user.gamer_tag, + "name": f"{user.first_name} {user.last_name}".strip(), + "id": [entrant.entrant.id], + "pronoun": user.pronouns, + "avatar": "", + "country_code": user.location_country, + "state_code": user.location_state, + "seed": entrant.seed + } + + # Extract avatar URL from images. + for image in user.images: + if image.type == 'IMAGE_TYPE_AVATAR': + formatted_entrant["avatar"] = image.url + break + + players.append(formatted_entrant) + except Exception as e: + logger.error(f"Error processing entrants: {e}") + + TSHPlayerDB.AddPlayers(players) def GetTournamentData(self, progress_callback=None, cancel_event=None): # TODO: Accessed early From f8957a82668e1e370f08071be7477b53ebe6b95b Mon Sep 17 00:00:00 2001 From: Vy Date: Sat, 13 Dec 2025 10:12:55 +1100 Subject: [PATCH 11/37] ParryGG: GetTournamentData --- src/TSHTournamentDataProvider.py | 4 ++ .../ParryGGDataProvider.py | 45 ++++++++++++++++--- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/TSHTournamentDataProvider.py b/src/TSHTournamentDataProvider.py index 00e1ba6e8..68af8d500 100644 --- a/src/TSHTournamentDataProvider.py +++ b/src/TSHTournamentDataProvider.py @@ -156,8 +156,12 @@ def SetStartggEventSlug(self, mainWindow): QRegularExpression("start.gg/admin/tournament/[^/]+/brackets/[^/]+"), # This could maybe become just "parry.gg/[^/]+/[^/]+" + # # But that form of url directs to the /main/bracket page anyway, # so it's rare to see it shortened unless typing the url manually. + # + # Only downside is that the tournament management page would match + # (parry.gg/tournamentname/_manage) which doesn't include the event slug. QRegularExpression("parry.gg/[^/]+/[^/]+/[^/]+") ] diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index c51fc4d87..7f9d4a090 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -8,6 +8,8 @@ from google.protobuf.json_format import MessageToJson from loguru import logger +from datetime import datetime +from dateutil import parser # ParryGG Imports # TODO: Probably won't use all of these. @@ -114,9 +116,9 @@ def GetIconURL(self): get_tournament_request = GetTournamentRequest() get_tournament_request.tournament_slug = self.tournament_slug - response = self.tournament_service.GetTournament(get_tournament_request, metadata=self.metadata) + get_tournament_response = self.tournament_service.GetTournament(get_tournament_request, metadata=self.metadata) - for image in response.tournament.images: + for image in get_tournament_response.tournament.images: # Look for banner image, can be replaced in future if icons are added. if image.type == "IMAGE_TYPE_BANNER": return image.url @@ -131,12 +133,12 @@ def GetEntrants(self): get_event_entrants_request = GetEventEntrantsRequest() get_event_entrants_request.event_identifier.event_slug_path.tournament_slug = self.tournament_slug get_event_entrants_request.event_identifier.event_slug_path.event_slug = self.event_slug - response = self.event_service.GetEventEntrants(get_event_entrants_request, metadata=self.metadata) + get_event_entrants_response = self.event_service.GetEventEntrants(get_event_entrants_request, metadata=self.metadata) players = [] try: - for entrant in response.event_entrants: + for entrant in get_event_entrants_response.event_entrants: # Assuming single user for now, no teams. user = entrant.entrant.users[0] @@ -166,8 +168,39 @@ def GetEntrants(self): TSHPlayerDB.AddPlayers(players) def GetTournamentData(self, progress_callback=None, cancel_event=None): - # TODO: Accessed early - pass + self._setup_service("Tournament") + + get_tournament_request = GetTournamentRequest() + get_tournament_request.tournament_slug = self.tournament_slug + get_tournament_response = self.tournament_service.GetTournament(get_tournament_request, metadata=self.metadata) + + tournament_data = get_tournament_response.tournament + + for event in tournament_data.events: + if event.slug == self.event_slug: + event_data = event + break + + tournament_info = {} + + try: + tournament_info["tournamentName"] = tournament_data.name + tournament_info["eventName"] = event_data.name + tournament_info["numEntrants"] = event_data.entrant_count + tournament_info["address"] = tournament_data.venue_address + tournament_info["startAt"] = int(parser.parse(tournament_data.start_date).timestamp()) + tournament_info["endAt"] = int(parser.parse(tournament_data.end_date).timestamp()) + tournament_info["eventStartAt"] = int(parser.parse(event_data.start_date).timestamp()) + tournament_info["eventEndAt"] = None + + for slug in tournament_data.slugs: + if slug.type == 'SLUG_TYPE_CUSTOM': + tournament_info["shortLink"] = slug.slug + break + except Exception as e: + logger.error(f"Error extracting tournament data: {e}") + + return tournament_info def GetMatch(self, setId, progress_callback=None, cancel_event=None): pass From d908bbce2005fd504c3bf87d49eead0062105a48 Mon Sep 17 00:00:00 2001 From: Vy Date: Sat, 13 Dec 2025 10:41:42 +1100 Subject: [PATCH 12/37] ParryGG: GetTournamentPhases --- .../ParryGGDataProvider.py | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index 7f9d4a090..7707e1d18 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -239,8 +239,37 @@ def GetPlayerHistoryStandings(self, playerId, playerNumber, gameType): pass def GetTournamentPhases(self, progress_callback=None, cancel_event=None): - # TODO: Accessed early - pass + self._setup_service("Event") + + get_event_request = GetEventRequest() + get_event_request.event_identifier.event_slug_path.tournament_slug = self.tournament_slug + get_event_request.event_identifier.event_slug_path.event_slug = self.event_slug + get_event_response = self.event_service.GetEvent(get_event_request, metadata=self.metadata) + + phases = [] + + try: + for phase in get_event_response.event.phases: + phase_info = { + "id": phase.id, + "name": phase.name, + "groups": [] + } + + for bracket in phase.brackets: + bracket_info = { + "id": bracket.id, + "name": bracket.name, + "type": phase.bracket_type.replace("BRACKET_TYPE_", "") + } + phase_info["groups"].append(bracket_info) + + phases.append(phase_info) + + except Exception as e: + logger.error(f"Error getting tournament phases: {e}") + + return phases def GetTournamentPhaseGroup(self, id, progress_callback=None, cancel_event=None): pass From 92e6d92835fb80e302b2aa52bb08de9e590c06a3 Mon Sep 17 00:00:00 2001 From: Vy Date: Mon, 15 Dec 2025 11:30:03 +1100 Subject: [PATCH 13/37] Bugfixes and Cleaning --- .../ParryGGDataProvider.py | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index 7707e1d18..ad89fbe37 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -18,7 +18,6 @@ from parrygg.services.phase_service_pb2_grpc import PhaseServiceStub from parrygg.services.bracket_service_pb2_grpc import BracketServiceStub from parrygg.services.match_service_pb2_grpc import MatchServiceStub -from parrygg.services.match_game_service_pb2_grpc import MatchGameServiceStub from parrygg.services.entrant_service_pb2_grpc import EntrantServiceStub from parrygg.services.user_service_pb2_grpc import UserServiceStub from parrygg.services.game_service_pb2_grpc import GameServiceStub @@ -29,7 +28,6 @@ from parrygg.services.phase_service_pb2 import * from parrygg.services.bracket_service_pb2 import * from parrygg.services.match_service_pb2 import * -from parrygg.services.match_game_service_pb2 import * from parrygg.services.entrant_service_pb2 import * from parrygg.services.user_service_pb2 import * from parrygg.services.game_service_pb2 import * @@ -73,11 +71,25 @@ def __init__(self, url, threadpool, tshTdp) -> None: # Initialize gRPC self.channel = grpc.secure_channel("api.parry.gg:443", grpc.ssl_channel_credentials()) - self._setup_service("Tournament") - # Get tournament and event slugs + # Get tournament and event slugs and IDs self.tournament_slug = self.url.split("parry.gg/")[1].split("/")[0] self.event_slug = self.url.split("parry.gg/")[1].split("/")[1] + self._get_ids() + + def _get_ids(self): + self._setup_service("Tournament") + + get_tournament_request = GetTournamentRequest() + get_tournament_request.tournament_slug = self.tournament_slug + get_tournament_response = self.tournament_service.GetTournament(get_tournament_request, metadata=self.metadata) + + self.tournament_id = get_tournament_response.tournament.id + + for event in get_tournament_response.tournament.events: + if event.slug == self.event_slug: + self.event_id = event.id + break def _setup_service(self, service_name): match service_name: @@ -152,7 +164,7 @@ def GetEntrants(self): "avatar": "", "country_code": user.location_country, "state_code": user.location_state, - "seed": entrant.seed + "seed": 0 #entrant.seed } # Extract avatar URL from images. @@ -188,9 +200,9 @@ def GetTournamentData(self, progress_callback=None, cancel_event=None): tournament_info["eventName"] = event_data.name tournament_info["numEntrants"] = event_data.entrant_count tournament_info["address"] = tournament_data.venue_address - tournament_info["startAt"] = int(parser.parse(tournament_data.start_date).timestamp()) - tournament_info["endAt"] = int(parser.parse(tournament_data.end_date).timestamp()) - tournament_info["eventStartAt"] = int(parser.parse(event_data.start_date).timestamp()) + tournament_info["startAt"] = tournament_data.start_date + tournament_info["endAt"] = tournament_data.end_date + tournament_info["eventStartAt"] = event_data.start_date tournament_info["eventEndAt"] = None for slug in tournament_data.slugs: @@ -199,7 +211,7 @@ def GetTournamentData(self, progress_callback=None, cancel_event=None): break except Exception as e: logger.error(f"Error extracting tournament data: {e}") - + return tournament_info def GetMatch(self, setId, progress_callback=None, cancel_event=None): @@ -242,8 +254,7 @@ def GetTournamentPhases(self, progress_callback=None, cancel_event=None): self._setup_service("Event") get_event_request = GetEventRequest() - get_event_request.event_identifier.event_slug_path.tournament_slug = self.tournament_slug - get_event_request.event_identifier.event_slug_path.event_slug = self.event_slug + get_event_request.id = self.event_id get_event_response = self.event_service.GetEvent(get_event_request, metadata=self.metadata) phases = [] @@ -259,9 +270,21 @@ def GetTournamentPhases(self, progress_callback=None, cancel_event=None): for bracket in phase.brackets: bracket_info = { "id": bracket.id, - "name": bracket.name, - "type": phase.bracket_type.replace("BRACKET_TYPE_", "") + "name": bracket.name } + + match phase.bracket_type: + case 0: + bracket_info["type"] = "UNSPECIFIED" + case 1: + bracket_info["type"] = "SINGLE_ELIMINATION" + case 2: + bracket_info["type"] = "DOUBLE_ELIMINATION" + case 3: + bracket_info["type"] = "ROUND_ROBIN" + case _: + bracket_info["type"] = "UNKNOWN" + phase_info["groups"].append(bracket_info) phases.append(phase_info) From a7f723e2d71bc8a054b6929a0c1a1bdb31a08bf7 Mon Sep 17 00:00:00 2001 From: Vy Date: Tue, 16 Dec 2025 16:04:15 +1100 Subject: [PATCH 14/37] Further Misc Bugfixes and Cleaning --- .../ParryGGDataProvider.py | 71 +++++++++---------- 1 file changed, 33 insertions(+), 38 deletions(-) diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index ad89fbe37..f049fcd71 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -1,6 +1,6 @@ -import grpc import os import traceback +import grpc from dotenv import load_dotenv load_dotenv() @@ -32,6 +32,10 @@ from parrygg.services.user_service_pb2 import * from parrygg.services.game_service_pb2 import * +from parrygg.models.slug_pb2 import SlugType +from parrygg.models.bracket_pb2 import BracketType +from parrygg.models.image_pb2 import ImageType + from .TournamentDataProvider import TournamentDataProvider # Other Imports used by StartGGDataProvider @@ -57,6 +61,7 @@ class ParryGGDataProvider(TournamentDataProvider): entrant_service = None user_service = None game_service = None + _timeout = 10 def __init__(self, url, threadpool, tshTdp) -> None: super().__init__(url, threadpool, tshTdp) @@ -68,21 +73,19 @@ def __init__(self, url, threadpool, tshTdp) -> None: else: logger.warning("PARRYGG_API_KEY not found in environment variables") self.metadata = [] - - # Initialize gRPC - self.channel = grpc.secure_channel("api.parry.gg:443", grpc.ssl_channel_credentials()) # Get tournament and event slugs and IDs + self._get_slugs_and_ids() + + def _get_slugs_and_ids(self): self.tournament_slug = self.url.split("parry.gg/")[1].split("/")[0] self.event_slug = self.url.split("parry.gg/")[1].split("/")[1] - self._get_ids() - - def _get_ids(self): + self._setup_service("Tournament") get_tournament_request = GetTournamentRequest() get_tournament_request.tournament_slug = self.tournament_slug - get_tournament_response = self.tournament_service.GetTournament(get_tournament_request, metadata=self.metadata) + get_tournament_response = self.tournament_service.GetTournament(get_tournament_request, metadata=self.metadata, timeout=self._timeout) self.tournament_id = get_tournament_response.tournament.id @@ -92,6 +95,9 @@ def _get_ids(self): break def _setup_service(self, service_name): + if not hasattr(self, 'channel') or self.channel is None: + self.channel = grpc.secure_channel("api.parry.gg:443", grpc.ssl_channel_credentials()) + match service_name: case "Tournament": if self.tournament_service is None: @@ -128,11 +134,11 @@ def GetIconURL(self): get_tournament_request = GetTournamentRequest() get_tournament_request.tournament_slug = self.tournament_slug - get_tournament_response = self.tournament_service.GetTournament(get_tournament_request, metadata=self.metadata) + get_tournament_response = self.tournament_service.GetTournament(get_tournament_request, metadata=self.metadata, timeout=self._timeout) for image in get_tournament_response.tournament.images: # Look for banner image, can be replaced in future if icons are added. - if image.type == "IMAGE_TYPE_BANNER": + if image.type == ImageType.IMAGE_TYPE_BANNER: return image.url # Fallback to the ParryGG favicon if no suitable image is found. @@ -141,15 +147,15 @@ def GetIconURL(self): def GetEntrants(self): self._setup_service("Event") - - get_event_entrants_request = GetEventEntrantsRequest() - get_event_entrants_request.event_identifier.event_slug_path.tournament_slug = self.tournament_slug - get_event_entrants_request.event_identifier.event_slug_path.event_slug = self.event_slug - get_event_entrants_response = self.event_service.GetEventEntrants(get_event_entrants_request, metadata=self.metadata) players = [] try: + get_event_entrants_request = GetEventEntrantsRequest() + get_event_entrants_request.event_identifier.event_slug_path.tournament_slug = self.tournament_slug + get_event_entrants_request.event_identifier.event_slug_path.event_slug = self.event_slug + get_event_entrants_response = self.event_service.GetEventEntrants(get_event_entrants_request, metadata=self.metadata, timeout=self._timeout) + for entrant in get_event_entrants_response.event_entrants: # Assuming single user for now, no teams. user = entrant.entrant.users[0] @@ -184,7 +190,7 @@ def GetTournamentData(self, progress_callback=None, cancel_event=None): get_tournament_request = GetTournamentRequest() get_tournament_request.tournament_slug = self.tournament_slug - get_tournament_response = self.tournament_service.GetTournament(get_tournament_request, metadata=self.metadata) + get_tournament_response = self.tournament_service.GetTournament(get_tournament_request, metadata=self.metadata, timeout=self._timeout) tournament_data = get_tournament_response.tournament @@ -200,13 +206,13 @@ def GetTournamentData(self, progress_callback=None, cancel_event=None): tournament_info["eventName"] = event_data.name tournament_info["numEntrants"] = event_data.entrant_count tournament_info["address"] = tournament_data.venue_address - tournament_info["startAt"] = tournament_data.start_date - tournament_info["endAt"] = tournament_data.end_date - tournament_info["eventStartAt"] = event_data.start_date - tournament_info["eventEndAt"] = None + tournament_info["startAt"] = tournament_data.start_date.seconds + tournament_info["endAt"] = tournament_data.end_date.seconds + tournament_info["eventStartAt"] = event_data.start_date.seconds + tournament_info["eventEndAt"] = "" for slug in tournament_data.slugs: - if slug.type == 'SLUG_TYPE_CUSTOM': + if slug.type == SlugType.SLUG_TYPE_CUSTOM: tournament_info["shortLink"] = slug.slug break except Exception as e: @@ -252,14 +258,14 @@ def GetPlayerHistoryStandings(self, playerId, playerNumber, gameType): def GetTournamentPhases(self, progress_callback=None, cancel_event=None): self._setup_service("Event") - - get_event_request = GetEventRequest() - get_event_request.id = self.event_id - get_event_response = self.event_service.GetEvent(get_event_request, metadata=self.metadata) phases = [] try: + get_event_request = GetEventRequest() + get_event_request.id = self.event_id + get_event_response = self.event_service.GetEvent(get_event_request, metadata=self.metadata, timeout=self._timeout) + for phase in get_event_response.event.phases: phase_info = { "id": phase.id, @@ -270,21 +276,10 @@ def GetTournamentPhases(self, progress_callback=None, cancel_event=None): for bracket in phase.brackets: bracket_info = { "id": bracket.id, - "name": bracket.name + "name": bracket.name, + "type": BracketType.Name(phase.bracket_type) } - match phase.bracket_type: - case 0: - bracket_info["type"] = "UNSPECIFIED" - case 1: - bracket_info["type"] = "SINGLE_ELIMINATION" - case 2: - bracket_info["type"] = "DOUBLE_ELIMINATION" - case 3: - bracket_info["type"] = "ROUND_ROBIN" - case _: - bracket_info["type"] = "UNKNOWN" - phase_info["groups"].append(bracket_info) phases.append(phase_info) From befa4ddfdd5cfdd4694b624d2d68ef3a7f1e975a Mon Sep 17 00:00:00 2001 From: Vy Date: Tue, 16 Dec 2025 16:10:11 +1100 Subject: [PATCH 15/37] Basic Support in TSHTournamentDataProvider.py --- src/TSHTournamentDataProvider.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/TSHTournamentDataProvider.py b/src/TSHTournamentDataProvider.py index 68af8d500..a89b6ee15 100644 --- a/src/TSHTournamentDataProvider.py +++ b/src/TSHTournamentDataProvider.py @@ -8,8 +8,7 @@ from .TSHGameAssetManager import TSHGameAssetManager from .TournamentDataProvider.TournamentDataProvider import TournamentDataProvider from .TournamentDataProvider.StartGGDataProvider import StartGGDataProvider -# TODO -# from .TournamentDataProvider.ParryGGDataProvider import ParryGGDataProvider +from .TournamentDataProvider.ParryGGDataProvider import ParryGGDataProvider from .Helpers.TSHVersionHelper import get_supported_providers from loguru import logger @@ -82,10 +81,9 @@ def SetTournament(self, url, initialLoading=False): TSHTournamentDataProvider.instance.provider = StartGGDataProvider( url, self.threadPool, self) url = TSHTournamentDataProvider.instance.provider.GetRealEventURL(url) - # TODO - # elif url is not None and "parry.gg" in url: - # TSHTournamentDataProvider.instance.provider = ParryGGDataProvider( - # url, self.threadPool, self) + elif url is not None and "parry.gg" in url: + TSHTournamentDataProvider.instance.provider = ParryGGDataProvider( + url, self.threadPool, self) else: logger.error("Unsupported provider...") TSHTournamentDataProvider.instance.provider = None @@ -115,10 +113,9 @@ def SetTournamentSignal(self, url, initialLoading=False): if url is not None and "start.gg" in url: TSHTournamentDataProvider.instance.provider = StartGGDataProvider( url, self.threadPool, self) - # TODO - # elif url is not None and "parry.gg" in url: - # TSHTournamentDataProvider.instance.provider = ParryGGDataProvider( - # url, self.threadPool, self) + elif url is not None and "parry.gg" in url: + TSHTournamentDataProvider.instance.provider = ParryGGDataProvider( + url, self.threadPool, self) else: logger.error("Unsupported provider...") TSHTournamentDataProvider.instance.provider = None From b285759957872364981e749656a17540c8708408 Mon Sep 17 00:00:00 2001 From: Vy Date: Wed, 17 Dec 2025 09:11:12 +1100 Subject: [PATCH 16/37] Get API Key from Settings, rather than .env --- src/Settings/TSHSettingsWindow.py | 18 ++++++++ src/TSHTournamentDataProvider.py | 11 ++++- .../ParryGGDataProvider.py | 44 ++++++------------- 3 files changed, 41 insertions(+), 32 deletions(-) diff --git a/src/Settings/TSHSettingsWindow.py b/src/Settings/TSHSettingsWindow.py index 84ff865cb..97462807c 100644 --- a/src/Settings/TSHSettingsWindow.py +++ b/src/Settings/TSHSettingsWindow.py @@ -280,6 +280,24 @@ def UiMounted(self): self.add_setting_widget(QApplication.translate( "settings", "Bluesky"), SettingsWidget("bsky_account", bskySettings)) + + # Add API Key settings + APIKeySettings = [] + APIKeySettings.append(( + QApplication.translate( + "settings.api_keys", "parry.gg"), + "parrygg", + "password", + "", + None, + QApplication.translate( + "settings.api_keys", "You can get an API Key from parry.gg/api-keys") + "\n" + + QApplication.translate( + "settings.api_keys", "Please note that the API Key will be stored in plain text on your computer") + )) + + self.add_setting_widget(QApplication.translate( + "settings", "API Keys"), SettingsWidget("api_keys", APIKeySettings)) self.resize(1000, 500) QApplication.processEvents() diff --git a/src/TSHTournamentDataProvider.py b/src/TSHTournamentDataProvider.py index a89b6ee15..df4f43ca5 100644 --- a/src/TSHTournamentDataProvider.py +++ b/src/TSHTournamentDataProvider.py @@ -82,8 +82,15 @@ def SetTournament(self, url, initialLoading=False): url, self.threadPool, self) url = TSHTournamentDataProvider.instance.provider.GetRealEventURL(url) elif url is not None and "parry.gg" in url: - TSHTournamentDataProvider.instance.provider = ParryGGDataProvider( - url, self.threadPool, self) + if not SettingsManager.Get("api_keys.parrygg"): + logger.error("ParryGG API key not set") + TSHTournamentDataProvider.instance.provider = None + try: + TSHTournamentDataProvider.instance.provider = ParryGGDataProvider( + url, self.threadPool, self, SettingsManager.Get("api_keys.parrygg")) + except Exception as e: + logger.error(f"Failed to initialize ParryGG provider: {e}") + TSHTournamentDataProvider.instance.provider = None else: logger.error("Unsupported provider...") TSHTournamentDataProvider.instance.provider = None diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index f049fcd71..67db28b6d 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -1,15 +1,5 @@ -import os -import traceback import grpc - -from dotenv import load_dotenv -load_dotenv() -PARRYGG_API_KEY = os.getenv('PARRYGG_API_KEY') - -from google.protobuf.json_format import MessageToJson from loguru import logger -from datetime import datetime -from dateutil import parser # ParryGG Imports # TODO: Probably won't use all of these. @@ -36,20 +26,19 @@ from parrygg.models.bracket_pb2 import BracketType from parrygg.models.image_pb2 import ImageType -from .TournamentDataProvider import TournamentDataProvider - # Other Imports used by StartGGDataProvider # TODO: May or may not be required. -from ..Helpers.TSHCountryHelper import TSHCountryHelper -from ..Helpers.TSHDictHelper import deep_get -from ..Helpers.TSHDirHelper import TSHResolve -from ..Helpers.TSHQtHelper import invokeSlot -from ..TSHGameAssetManager import TSHGameAssetManager +from .TournamentDataProvider import TournamentDataProvider from ..TSHPlayerDB import TSHPlayerDB -import orjson -from ..Helpers.TSHLocaleHelper import TSHLocaleHelper -from ..TSHBracket import is_power_of_two -from ..Workers import Worker +# from ..Helpers.TSHCountryHelper import TSHCountryHelper +# from ..Helpers.TSHDictHelper import deep_get +# from ..Helpers.TSHDirHelper import TSHResolve +# from ..Helpers.TSHQtHelper import invokeSlot +# from ..TSHGameAssetManager import TSHGameAssetManager +# import orjson +# from ..Helpers.TSHLocaleHelper import TSHLocaleHelper +# from ..TSHBracket import is_power_of_two +# from ..Workers import Worker class ParryGGDataProvider(TournamentDataProvider): tournament_service = None @@ -61,20 +50,15 @@ class ParryGGDataProvider(TournamentDataProvider): entrant_service = None user_service = None game_service = None + _timeout = 10 + metadata = None - def __init__(self, url, threadpool, tshTdp) -> None: + def __init__(self, url, threadpool, tshTdp, api_key=None) -> None: super().__init__(url, threadpool, tshTdp) self.name = "ParryGG" - # Set up metadata with API key - if PARRYGG_API_KEY: - self.metadata = [("x-api-key", PARRYGG_API_KEY)] - else: - logger.warning("PARRYGG_API_KEY not found in environment variables") - self.metadata = [] - - # Get tournament and event slugs and IDs + self.metadata = [("x-api-key", api_key)] self._get_slugs_and_ids() def _get_slugs_and_ids(self): From ff96158cd01f6c95a7d5adeb9a78e04e6e6e2fc1 Mon Sep 17 00:00:00 2001 From: Vy Date: Wed, 17 Dec 2025 09:27:50 +1100 Subject: [PATCH 17/37] Bugfix for image.type comparison --- src/TournamentDataProvider/ParryGGDataProvider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index 67db28b6d..9858a2e44 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -159,7 +159,7 @@ def GetEntrants(self): # Extract avatar URL from images. for image in user.images: - if image.type == 'IMAGE_TYPE_AVATAR': + if image.type == ImageType.IMAGE_TYPE_AVATAR: formatted_entrant["avatar"] = image.url break From 7c8b22d7d5022ba093c12c9b3761ce978c97324d Mon Sep 17 00:00:00 2001 From: Vy Date: Wed, 17 Dec 2025 09:53:06 +1100 Subject: [PATCH 18/37] Better Errors Errors for: Tournament or Event Not Found Unauthenticated (Incorrect or No API Key) --- src/TSHTournamentDataProvider.py | 13 +++---- .../ParryGGDataProvider.py | 34 +++++++++++++------ 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/TSHTournamentDataProvider.py b/src/TSHTournamentDataProvider.py index df4f43ca5..5c93ef3fa 100644 --- a/src/TSHTournamentDataProvider.py +++ b/src/TSHTournamentDataProvider.py @@ -85,12 +85,13 @@ def SetTournament(self, url, initialLoading=False): if not SettingsManager.Get("api_keys.parrygg"): logger.error("ParryGG API key not set") TSHTournamentDataProvider.instance.provider = None - try: - TSHTournamentDataProvider.instance.provider = ParryGGDataProvider( - url, self.threadPool, self, SettingsManager.Get("api_keys.parrygg")) - except Exception as e: - logger.error(f"Failed to initialize ParryGG provider: {e}") - TSHTournamentDataProvider.instance.provider = None + else: + try: + TSHTournamentDataProvider.instance.provider = ParryGGDataProvider( + url, self.threadPool, self, SettingsManager.Get("api_keys.parrygg")) + except Exception as e: + logger.error(f"Failed to initialize ParryGG provider: {e}") + TSHTournamentDataProvider.instance.provider = None else: logger.error("Unsupported provider...") TSHTournamentDataProvider.instance.provider = None diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index 9858a2e44..5848b102a 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -58,7 +58,12 @@ def __init__(self, url, threadpool, tshTdp, api_key=None) -> None: super().__init__(url, threadpool, tshTdp) self.name = "ParryGG" - self.metadata = [("x-api-key", api_key)] + if api_key: + self.metadata = [("x-api-key", api_key)] + else: + logger.warning("No API key provided for ParryGG") + self.metadata = [] + self._get_slugs_and_ids() def _get_slugs_and_ids(self): @@ -67,17 +72,26 @@ def _get_slugs_and_ids(self): self._setup_service("Tournament") - get_tournament_request = GetTournamentRequest() - get_tournament_request.tournament_slug = self.tournament_slug - get_tournament_response = self.tournament_service.GetTournament(get_tournament_request, metadata=self.metadata, timeout=self._timeout) + try: + get_tournament_request = GetTournamentRequest() + get_tournament_request.tournament_slug = self.tournament_slug + get_tournament_response = self.tournament_service.GetTournament(get_tournament_request, metadata=self.metadata, timeout=self._timeout) - self.tournament_id = get_tournament_response.tournament.id + self.tournament_id = get_tournament_response.tournament.id - for event in get_tournament_response.tournament.events: - if event.slug == self.event_slug: - self.event_id = event.id - break - + for event in get_tournament_response.tournament.events: + if event.slug == self.event_slug: + self.event_id = event.id + break + except grpc.RpcError as e: + if e.code() == grpc.StatusCode.UNAUTHENTICATED: + # logger.error("ParryGG authentication failed - invalid API key") + raise Exception("Invalid API key") + elif e.code() == grpc.StatusCode.NOT_FOUND: + raise Exception("Tournament or Event not found") + else: + logger.error(f"ParryGG gRPC error: {e}") + def _setup_service(self, service_name): if not hasattr(self, 'channel') or self.channel is None: self.channel = grpc.secure_channel("api.parry.gg:443", grpc.ssl_channel_credentials()) From e0118863e5ea64d2c16e13e19433eb33b713fb89 Mon Sep 17 00:00:00 2001 From: Vy Date: Thu, 18 Dec 2025 14:56:07 +1100 Subject: [PATCH 19/37] Bring imports in line with StartGGDataProvider Likely unnecessary but good to do before I forget. --- src/TournamentDataProvider/ParryGGDataProvider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index 5848b102a..56c0cbae3 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -33,7 +33,7 @@ # from ..Helpers.TSHCountryHelper import TSHCountryHelper # from ..Helpers.TSHDictHelper import deep_get # from ..Helpers.TSHDirHelper import TSHResolve -# from ..Helpers.TSHQtHelper import invokeSlot +# from ..Helpers.TSHQtHelper import invokeSlot, gui_thread_sync # from ..TSHGameAssetManager import TSHGameAssetManager # import orjson # from ..Helpers.TSHLocaleHelper import TSHLocaleHelper From c9e7fd8986f06a5d6948e1a56db81a3cd03ce4fe Mon Sep 17 00:00:00 2001 From: Vy Date: Wed, 11 Feb 2026 16:23:31 +1100 Subject: [PATCH 20/37] Update .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 74d4af9f5..1e83ddb5c 100755 --- a/.gitignore +++ b/.gitignore @@ -59,7 +59,6 @@ main.spec logs/* Pipfile* env -.env desktop.ini assets/controller/* assets/*_tmp From 67d9e619a30532a564f83246443c55824acac890 Mon Sep 17 00:00:00 2001 From: Vy Date: Wed, 1 Apr 2026 13:51:52 +1100 Subject: [PATCH 21/37] Fix crash on defaulting some settings --- src/Settings/SettingsWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Settings/SettingsWidget.py b/src/Settings/SettingsWidget.py index b193a7dbd..c16320577 100644 --- a/src/Settings/SettingsWidget.py +++ b/src/Settings/SettingsWidget.py @@ -84,7 +84,7 @@ def AddSetting(self, name: str, setting: str, type: str, defaultValue, callback= resetButton.clicked.connect( lambda bt=None, setting=setting, settingWidget=settingWidget: [ settingWidget.setText(defaultValue), - callback() + callback() if callable(callback) else None ] ) elif type == "color": From c219d1b728d47d5d49ff0375673dd804ff861918 Mon Sep 17 00:00:00 2001 From: Vy Date: Wed, 1 Apr 2026 15:33:03 +1100 Subject: [PATCH 22/37] Basic Game IDs from parry.gg Need to be added manually into the game's config.json, but this is an issue for later in StreamHelperAssets Examples for ssbm and ssbu respectively if you want it to work in the meantime - "parrygg_game_id": "01920676-416e-7838-b6aa-8d4453256f05" - "parrygg_game_id": "01920676-416e-7b49-a669-7157bd4a6934" --- src/TSHGameAssetManager.py | 21 +++++++++++++++++++ src/TSHTournamentDataProvider.py | 7 +++---- .../ParryGGDataProvider.py | 6 ++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/TSHGameAssetManager.py b/src/TSHGameAssetManager.py index c180a936e..076551b5d 100644 --- a/src/TSHGameAssetManager.py +++ b/src/TSHGameAssetManager.py @@ -173,6 +173,27 @@ def detect_smashgg_id_match(game, id): if detect_smashgg_id_match(game, gameid): self.LoadGameAssets(i+1) break + + def SetGameFromParryGGId(self, gameid): + def detect_parrygg_id_match(game, id): + result = str(game.get("parrygg_game_id", "")) == str(id) + if not result: + alternates = game.get("alternate_versions", []) + alternates_ids = [] + for alternate in alternates: + if alternate.get("parrygg_game_id"): + alternates_ids.append( + str(alternate.get("parrygg_game_id"))) + result = str(id) in alternates_ids + return (result) + + if len(self.games.keys()) == 0: + return + + for i, game in enumerate(self.games.values()): + if detect_parrygg_id_match(game, gameid): + self.LoadGameAssets(i+1) + break def CopyCSS(self, game): # Make dir if doesn't exists diff --git a/src/TSHTournamentDataProvider.py b/src/TSHTournamentDataProvider.py index 5c93ef3fa..6a4874cb2 100644 --- a/src/TSHTournamentDataProvider.py +++ b/src/TSHTournamentDataProvider.py @@ -64,10 +64,9 @@ def SetGameFromProvider(self): if "start.gg" in self.provider.url: TSHGameAssetManager.instance.SetGameFromStartGGId( self.provider.videogame) - # TODO - # elif "parry.gg" in self.provider.url: - # TSHGameAssetManager.instance.SetGameFromParryGGId( - # self.provider.videogame) + elif "parry.gg" in self.provider.url: + TSHGameAssetManager.instance.SetGameFromParryGGId( + self.provider.videogame) else: logger.error("Unsupported provider...") diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index 56c0cbae3..74c853145 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -213,6 +213,12 @@ def GetTournamentData(self, progress_callback=None, cancel_event=None): if slug.type == SlugType.SLUG_TYPE_CUSTOM: tournament_info["shortLink"] = slug.slug break + + videogame = event_data.game.id + if videogame: + self.videogame = videogame + self.tshTdp.signals.game_changed.emit(videogame) + except Exception as e: logger.error(f"Error extracting tournament data: {e}") From c864cc338bb7f58476e9494613191e972c1409f7 Mon Sep 17 00:00:00 2001 From: Vy Date: Wed, 1 Apr 2026 15:48:22 +1100 Subject: [PATCH 23/37] Use parry.gg game slugs rather than ids Requested by parry.gg team. Examples are now: "parrygg_game_id": "super-smash-bros-ultimate" "parrygg_game_id": "super-smash-bros-melee" --- src/TournamentDataProvider/ParryGGDataProvider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index 74c853145..33d44ef0b 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -214,7 +214,7 @@ def GetTournamentData(self, progress_callback=None, cancel_event=None): tournament_info["shortLink"] = slug.slug break - videogame = event_data.game.id + videogame = event_data.game.slug if videogame: self.videogame = videogame self.tshTdp.signals.game_changed.emit(videogame) From 464ab3d2948c2745312cc60de4f27d34cc0f3cc1 Mon Sep 17 00:00:00 2001 From: Vy Date: Wed, 1 Apr 2026 15:50:04 +1100 Subject: [PATCH 24/37] Renamed parrygg_game_id to parrygg_game_slug One last example for a game's config.json: "parrygg_game_slug": "super-smash-bros-melee" --- src/TSHGameAssetManager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/TSHGameAssetManager.py b/src/TSHGameAssetManager.py index 076551b5d..2d7bedc45 100644 --- a/src/TSHGameAssetManager.py +++ b/src/TSHGameAssetManager.py @@ -176,14 +176,14 @@ def detect_smashgg_id_match(game, id): def SetGameFromParryGGId(self, gameid): def detect_parrygg_id_match(game, id): - result = str(game.get("parrygg_game_id", "")) == str(id) + result = str(game.get("parrygg_game_slug", "")) == str(id) if not result: alternates = game.get("alternate_versions", []) alternates_ids = [] for alternate in alternates: - if alternate.get("parrygg_game_id"): + if alternate.get("parrygg_game_slug"): alternates_ids.append( - str(alternate.get("parrygg_game_id"))) + str(alternate.get("parrygg_game_slug"))) result = str(id) in alternates_ids return (result) From 5b0495ecca944eb604543cb279240b046784f9a8 Mon Sep 17 00:00:00 2001 From: Vy Date: Wed, 1 Apr 2026 16:37:42 +1100 Subject: [PATCH 25/37] Check validity of StartGG-specific buttons Disable StartGG-specific buttons if StartGG isn't the bracket provider --- src/TournamentStreamHelper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TournamentStreamHelper.py b/src/TournamentStreamHelper.py index 9b5aa24aa..53260301e 100755 --- a/src/TournamentStreamHelper.py +++ b/src/TournamentStreamHelper.py @@ -931,7 +931,7 @@ def Signal_GameChange(self, url): TSHGameAssetManager.instance.selectedGame = {} def UpdateUserSetButton(self): - if SettingsManager.Get("StartGG_user"): + if SettingsManager.Get("StartGG_user") and TSHTournamentDataProvider.instance and TSHTournamentDataProvider.instance.provider and TSHTournamentDataProvider.instance.provider.name == "StartGG": self.btLoadPlayerSet.setText( QApplication.translate("app", "Load tournament and sets from StartGG user")+" "+QApplication.translate("punctuation", "(")+f"{SettingsManager.Get('StartGG_user')}"+QApplication.translate("punctuation", ")")) self.btLoadPlayerSet.setEnabled(True) From 87590be58cfe237eef17da6ac78025feb25b6cfd Mon Sep 17 00:00:00 2001 From: Vy Date: Wed, 1 Apr 2026 17:37:20 +1100 Subject: [PATCH 26/37] Disable Player Tracking for parry.gg Could be implemented in future, but for now it's not a focus. Trying to avoid crashes. --- src/TSHScoreboardWidget.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/TSHScoreboardWidget.py b/src/TSHScoreboardWidget.py index d645a18c4..ef368a740 100644 --- a/src/TSHScoreboardWidget.py +++ b/src/TSHScoreboardWidget.py @@ -735,12 +735,17 @@ def UpdateBottomButtons(self): self.btSelectSet.setText( QApplication.translate("app", "Load set from {0}").format(TSHTournamentDataProvider.instance.provider.url)) self.btSelectSet.setEnabled(True) - if self.scoreboardNumber <= 1 and not SettingsManager.Get("general.hide_track_player", False): - self.btLoadPlayerSet.setEnabled(True) + self.btLoadStationSet.setEnabled(True) + if TSHTournamentDataProvider.instance.provider.name == "StartGG": + if self.scoreboardNumber <= 1 and not SettingsManager.Get("general.hide_track_player", False): + self.btLoadPlayerSet.setEnabled(True) + elif TSHTournamentDataProvider.instance.provider.name == "ParryGG": + self.btLoadPlayerSet.setEnabled(False) else: self.btSelectSet.setText( QApplication.translate("app", "Load set")) self.btSelectSet.setEnabled(False) + self.btLoadStationSet.setEnabled(False) def SetCharacterNumber(self, value): # logger.info(f"TSHScoreboardWidget#SetCharacterNumber({value})") From c8994710fca330c1b27888d0a11b1eef07a24c82 Mon Sep 17 00:00:00 2001 From: Vy Date: Wed, 1 Apr 2026 17:37:47 +1100 Subject: [PATCH 27/37] Return empty lists to avoid crashes Specifically for GetMatches and GetStations --- src/TournamentDataProvider/ParryGGDataProvider.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index 33d44ef0b..33216f4f5 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -228,10 +228,13 @@ def GetMatch(self, setId, progress_callback=None, cancel_event=None): pass def GetMatches(self, getFinished=False, progress_callback=None, cancel_event=None): - pass + # TODO Get actual match data, returning an empty list avoids a crash for now. + return [] def GetStations(self, progress_callback=None, cancel_event=None): - pass + # TODO Get actual station/stream data, returning an empty list avoids a crash for now. + # Stations are not a short-term priority for parry.gg, but streams are actively being worked on. + return [] def GetStreamQueue(self, streamName=None, progress_callback=None, cancel_event=None): pass From 9c686ccf3132be18ded586e421ba628a2c5d6720 Mon Sep 17 00:00:00 2001 From: Vy Date: Mon, 6 Apr 2026 09:41:51 +1000 Subject: [PATCH 28/37] seed works now --- src/TournamentDataProvider/ParryGGDataProvider.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index 33216f4f5..c25612ade 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -51,8 +51,14 @@ class ParryGGDataProvider(TournamentDataProvider): user_service = None game_service = None + tournament_slug = None + event_slug = None + tournament_id = None + event_id = None + _timeout = 10 metadata = None + _initialized = False def __init__(self, url, threadpool, tshTdp, api_key=None) -> None: super().__init__(url, threadpool, tshTdp) @@ -168,7 +174,7 @@ def GetEntrants(self): "avatar": "", "country_code": user.location_country, "state_code": user.location_state, - "seed": 0 #entrant.seed + "seed": entrant.seed } # Extract avatar URL from images. From 34dde67b1b60e087e0151cceab785df1c5cd9b39 Mon Sep 17 00:00:00 2001 From: Vy Date: Mon, 6 Apr 2026 09:44:51 +1000 Subject: [PATCH 29/37] Implement GetStationMatchId Copied from StartGGDataProvider as it is a very simple function. Could maybe be moved into TournamentDataProvider instead --- src/TournamentDataProvider/ParryGGDataProvider.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index c25612ade..e3555f1cc 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -249,7 +249,9 @@ def GetStreamMatchId(self, streamName): pass def GetStationMatchId(self, stationId): - pass + sets = self.GetStationMatchsId(self, stationId) + + return sets[0] if len(sets) > 0 else None def GetStationMatchsId(self, stationId): pass From 7f5218615e1d448113cd0d8fe1b30e4dc0c7bc37 Mon Sep 17 00:00:00 2001 From: Vy Date: Mon, 6 Apr 2026 09:52:06 +1000 Subject: [PATCH 30/37] Only initialize get_slugs_and_ids once --- src/TournamentDataProvider/ParryGGDataProvider.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index e3555f1cc..113f9a13b 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -73,6 +73,9 @@ def __init__(self, url, threadpool, tshTdp, api_key=None) -> None: self._get_slugs_and_ids() def _get_slugs_and_ids(self): + if self._initialized: + return + self.tournament_slug = self.url.split("parry.gg/")[1].split("/")[0] self.event_slug = self.url.split("parry.gg/")[1].split("/")[1] @@ -89,6 +92,9 @@ def _get_slugs_and_ids(self): if event.slug == self.event_slug: self.event_id = event.id break + + self._initialized = True + except grpc.RpcError as e: if e.code() == grpc.StatusCode.UNAUTHENTICATED: # logger.error("ParryGG authentication failed - invalid API key") From d021f3b754a63caa2b693af61128341fec8fb810 Mon Sep 17 00:00:00 2001 From: Vy Date: Wed, 8 Apr 2026 12:31:58 +1000 Subject: [PATCH 31/37] Require parrygg >= 0.1.4 --- dependencies/requirements.txt | 2 +- src/TournamentDataProvider/ParryGGDataProvider.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/dependencies/requirements.txt b/dependencies/requirements.txt index 96d9a2796..e8bd17ad2 100644 --- a/dependencies/requirements.txt +++ b/dependencies/requirements.txt @@ -25,5 +25,5 @@ tzdata zstd==1.5.7.3 countryflag==1.1.1 grpcio -parrygg>=0.1.3 +parrygg>=0.1.4 python-dotenv diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index 113f9a13b..2412ea19a 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -58,11 +58,11 @@ class ParryGGDataProvider(TournamentDataProvider): _timeout = 10 metadata = None - _initialized = False def __init__(self, url, threadpool, tshTdp, api_key=None) -> None: super().__init__(url, threadpool, tshTdp) self.name = "ParryGG" + self._initialized = False if api_key: self.metadata = [("x-api-key", api_key)] @@ -245,21 +245,25 @@ def GetMatches(self, getFinished=False, progress_callback=None, cancel_event=Non def GetStations(self, progress_callback=None, cancel_event=None): # TODO Get actual station/stream data, returning an empty list avoids a crash for now. - # Stations are not a short-term priority for parry.gg, but streams are actively being worked on. + # NOTE Stations are not a short-term priority for parry.gg, but streams are actively being worked on. return [] def GetStreamQueue(self, streamName=None, progress_callback=None, cancel_event=None): + # NOTE Stations are not a short-term priority for parry.gg, but streams are actively being worked on. pass def GetStreamMatchId(self, streamName): + # NOTE Stations are not a short-term priority for parry.gg, but streams are actively being worked on. pass def GetStationMatchId(self, stationId): + # NOTE Stations are not a short-term priority for parry.gg, but streams are actively being worked on. sets = self.GetStationMatchsId(self, stationId) return sets[0] if len(sets) > 0 else None def GetStationMatchsId(self, stationId): + # NOTE Stations are not a short-term priority for parry.gg, but streams are actively being worked on. pass def GetUserMatchId(self, user): From 63f55b3a8c4a8b1f649a002862f7bb9bf3ef3178 Mon Sep 17 00:00:00 2001 From: Vy Date: Wed, 8 Apr 2026 13:38:59 +1000 Subject: [PATCH 32/37] Implement GetMatches --- .../ParryGGDataProvider.py | 58 ++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index 2412ea19a..b037d4691 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -23,7 +23,7 @@ from parrygg.services.game_service_pb2 import * from parrygg.models.slug_pb2 import SlugType -from parrygg.models.bracket_pb2 import BracketType +from parrygg.models.bracket_pb2 import BracketType, MatchState from parrygg.models.image_pb2 import ImageType # Other Imports used by StartGGDataProvider @@ -240,8 +240,60 @@ def GetMatch(self, setId, progress_callback=None, cancel_event=None): pass def GetMatches(self, getFinished=False, progress_callback=None, cancel_event=None): - # TODO Get actual match data, returning an empty list avoids a crash for now. - return [] + self._setup_service("Match") + final_data = [] + + try: + get_matches_request = GetMatchesRequest() + get_matches_request.filter.event.id = self.event_id + get_matches_response = self.match_service.GetMatches(get_matches_request, metadata=self.metadata, timeout=self._timeout) + + for match_context in get_matches_response.matches: + match = match_context.match + seeds = match_context.seeds + + if not getFinished and match.state == MatchState.MATCH_STATE_COMPLETED: + continue + elif len(seeds) == 0: + continue + + match_info = {} + + match_info["id"] = match.id + match_info["team1score"] = match.slots[0].score + match_info["team2score"] = match.slots[1].score + match_info["round_name"] = "" + match_info["tournament_phase"] = "" + match_info["bracket_type"] = "" + match_info["p1_name"] = seeds[0].event_entrant.entrant.users[0].gamer_tag if len(seeds) > 0 and seeds[0].HasField("event_entrant") and seeds[0].event_entrant.HasField("entrant") and len(seeds[0].event_entrant.entrant.users) > 0 else "" + match_info["p2_name"] = seeds[1].event_entrant.entrant.users[0].gamer_tag if len(seeds) > 1 and seeds[1].HasField("event_entrant") and seeds[1].event_entrant.HasField("entrant") and len(seeds[1].event_entrant.entrant.users) > 0 else "" + match_info["p1_seed"] = seeds[0].seed if len(seeds) > 0 else "" + match_info["p2_seed"] = seeds[1].seed if len(seeds) > 1 else "" + match_info["stream"] = "" + match_info["station"] = "" + match_info["isOnline"] = "" + match_info["isPools"] = "" + match_info["round"] = match.round + match_info["entrants"] = [[] for _ in range(len(seeds))] if len(seeds) > 0 else [[]] + + for i, seed in enumerate(seeds): + if seed.HasField("event_entrant") and seed.event_entrant.HasField("entrant"): + entrant = seed.event_entrant.entrant + if len(entrant.users) > 0: + user = entrant.users[0] + match_info["entrants"][i].append({ + "prefix": entrant.id, + "gamerTag": user.gamer_tag, + "name": (user.first_name + " " + user.last_name).strip(), + "id": [None, 0] + }) + + final_data.append(match_info) + + except Exception as e: + logger.error(f"Error processing matches: {e}") + + return final_data def GetStations(self, progress_callback=None, cancel_event=None): # TODO Get actual station/stream data, returning an empty list avoids a crash for now. From 9476cc8288abece6b0c15e7bfb28a75f73fc6373 Mon Sep 17 00:00:00 2001 From: Vy Date: Wed, 8 Apr 2026 13:58:11 +1000 Subject: [PATCH 33/37] Clearer errors for unimplemented functions --- .../ParryGGDataProvider.py | 51 ++++++++++++------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index b037d4691..906310327 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -237,7 +237,8 @@ def GetTournamentData(self, progress_callback=None, cancel_event=None): return tournament_info def GetMatch(self, setId, progress_callback=None, cancel_event=None): - pass + logger.error("GetMatch() called, returned {}") + return {} def GetMatches(self, getFinished=False, progress_callback=None, cancel_event=None): self._setup_service("Match") @@ -296,42 +297,52 @@ def GetMatches(self, getFinished=False, progress_callback=None, cancel_event=Non return final_data def GetStations(self, progress_callback=None, cancel_event=None): + logger.error("GetStations() called, returned []") # TODO Get actual station/stream data, returning an empty list avoids a crash for now. # NOTE Stations are not a short-term priority for parry.gg, but streams are actively being worked on. return [] def GetStreamQueue(self, streamName=None, progress_callback=None, cancel_event=None): - # NOTE Stations are not a short-term priority for parry.gg, but streams are actively being worked on. - pass + logger.error("GetStreamQueue() called, returned {}") + # NOTE Stations are not a short-term priority for parry.gg, but streams are actively being worked on. + return {} def GetStreamMatchId(self, streamName): - # NOTE Stations are not a short-term priority for parry.gg, but streams are actively being worked on. - pass + logger.error("GetStreamMatchId() called, returned ''") + # NOTE Stations are not a short-term priority for parry.gg, but streams are actively being worked on. + return "" def GetStationMatchId(self, stationId): - # NOTE Stations are not a short-term priority for parry.gg, but streams are actively being worked on. - sets = self.GetStationMatchsId(self, stationId) + # NOTE Stations are not a short-term priority for parry.gg, but streams are actively being worked on. + logger.error("GetStationMatchId() called") + sets = self.GetStationMatchsId(stationId) return sets[0] if len(sets) > 0 else None def GetStationMatchsId(self, stationId): - # NOTE Stations are not a short-term priority for parry.gg, but streams are actively being worked on. - pass + # NOTE Stations are not a short-term priority for parry.gg, but streams are actively being worked on. + logger.error("GetStationMatchsId() called, returned []") + return [] def GetUserMatchId(self, user): - pass + logger.error("GetUserMatchId() called, returned ''") + return "" def GetRecentSets(self, id1, id2, videogame, callback): - pass + logger.error("GetRecentSets() called, returned []") + return [] def GetLastSets(self, playerId, playerNumber): - pass + logger.error("GetLastSets() called, returned []") + return [] def GetCompletedSets(self): - pass + logger.error("GetCompletedSets() called, returned []") + return [] def GetPlayerHistoryStandings(self, playerId, playerNumber, gameType): - pass + logger.error("GetPlayerHistoryStandings() called, returned []") + return [] def GetTournamentPhases(self, progress_callback=None, cancel_event=None): self._setup_service("Event") @@ -367,16 +378,20 @@ def GetTournamentPhases(self, progress_callback=None, cancel_event=None): return phases def GetTournamentPhaseGroup(self, id, progress_callback=None, cancel_event=None): - pass + logger.error("GetTournamentPhaseGroup() called, returned {}") + return {} def GetStandings(self, playerNumber): - pass + logger.error("GetStandings() called, returned []") + return [] def GetFutureMatch(self, progrss_callback=None): - pass + logger.error("GetFutureMatch() called, returned {}") + return {} def GetFutureMatchesList(self, sets: object, progress_callback=None, cancel_event=None): - pass + logger.error("GetFutureMatchesList() called, returned []") + return [] def cleanup(self): """Properly cleanup gRPC channel and resources""" From 6e31153e44d4cf32eaf27d57e4fbdb6c8f8dafd4 Mon Sep 17 00:00:00 2001 From: Vy Date: Wed, 8 Apr 2026 14:16:38 +1000 Subject: [PATCH 34/37] (poorly) Implement GetMatch --- src/TournamentDataProvider/ParryGGDataProvider.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index 906310327..5a874d8d7 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -237,8 +237,16 @@ def GetTournamentData(self, progress_callback=None, cancel_event=None): return tournament_info def GetMatch(self, setId, progress_callback=None, cancel_event=None): - logger.error("GetMatch() called, returned {}") - return {} + # This is a really bad way of getting a match, but the GetMatch request + # doesn't include important data or provide a simple way to get it. + + matches = self.GetMatches() + + for match in matches: + if setId == match["id"]: + return match + + return None def GetMatches(self, getFinished=False, progress_callback=None, cancel_event=None): self._setup_service("Match") From 7b8132f90f9dad99412946bb04119df27e692892 Mon Sep 17 00:00:00 2001 From: Vy Date: Wed, 8 Apr 2026 14:16:54 +1000 Subject: [PATCH 35/37] Fix Sponsor Name --- src/TournamentDataProvider/ParryGGDataProvider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index 5a874d8d7..443222caf 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -291,7 +291,7 @@ def GetMatches(self, getFinished=False, progress_callback=None, cancel_event=Non if len(entrant.users) > 0: user = entrant.users[0] match_info["entrants"][i].append({ - "prefix": entrant.id, + "prefix": entrant.sponsor_name, "gamerTag": user.gamer_tag, "name": (user.first_name + " " + user.last_name).strip(), "id": [None, 0] From ff5979e578bc37b17da0a1c59e3b5803c19ef458 Mon Sep 17 00:00:00 2001 From: Vy Date: Wed, 8 Apr 2026 14:50:32 +1000 Subject: [PATCH 36/37] Misc Fixes Actually fix sponsor name Get Completed Matches when Getting a Match Make Score an Int Match StartGG method definitions --- .../ParryGGDataProvider.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index 443222caf..2c9a7aac6 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -240,13 +240,14 @@ def GetMatch(self, setId, progress_callback=None, cancel_event=None): # This is a really bad way of getting a match, but the GetMatch request # doesn't include important data or provide a simple way to get it. - matches = self.GetMatches() + matches = self.GetMatches(True) for match in matches: if setId == match["id"]: return match - - return None + + logger.error(f"Match {setId} not found") + return {} def GetMatches(self, getFinished=False, progress_callback=None, cancel_event=None): self._setup_service("Match") @@ -269,8 +270,8 @@ def GetMatches(self, getFinished=False, progress_callback=None, cancel_event=Non match_info = {} match_info["id"] = match.id - match_info["team1score"] = match.slots[0].score - match_info["team2score"] = match.slots[1].score + match_info["team1score"] = int(match.slots[0].score) + match_info["team2score"] = int(match.slots[1].score) match_info["round_name"] = "" match_info["tournament_phase"] = "" match_info["bracket_type"] = "" @@ -291,7 +292,7 @@ def GetMatches(self, getFinished=False, progress_callback=None, cancel_event=Non if len(entrant.users) > 0: user = entrant.users[0] match_info["entrants"][i].append({ - "prefix": entrant.sponsor_name, + "prefix": user.sponsor_name, "gamerTag": user.gamer_tag, "name": (user.first_name + " " + user.last_name).strip(), "id": [None, 0] @@ -310,7 +311,7 @@ def GetStations(self, progress_callback=None, cancel_event=None): # NOTE Stations are not a short-term priority for parry.gg, but streams are actively being worked on. return [] - def GetStreamQueue(self, streamName=None, progress_callback=None, cancel_event=None): + def GetStreamQueue(self, progress_callback=None, cancel_event=None): logger.error("GetStreamQueue() called, returned {}") # NOTE Stations are not a short-term priority for parry.gg, but streams are actively being worked on. return {} @@ -336,19 +337,19 @@ def GetUserMatchId(self, user): logger.error("GetUserMatchId() called, returned ''") return "" - def GetRecentSets(self, id1, id2, videogame, callback): + def GetRecentSets(self, id1, id2, videogame, callback, requestTime, progress_callback=None, cancel_event=None): logger.error("GetRecentSets() called, returned []") return [] - def GetLastSets(self, playerId, playerNumber): + def GetLastSets(self, playerID, playerNumber, callback, progress_callback=None, cancel_event=None): logger.error("GetLastSets() called, returned []") return [] - def GetCompletedSets(self): + def GetCompletedSets(self, progress_callback=None, cancel_event=None): logger.error("GetCompletedSets() called, returned []") return [] - def GetPlayerHistoryStandings(self, playerId, playerNumber, gameType): + def GetPlayerHistoryStandings(self, playerID, playerNumber, gameType, callback, progress_callback=None, cancel_event=None): logger.error("GetPlayerHistoryStandings() called, returned []") return [] @@ -393,11 +394,11 @@ def GetStandings(self, playerNumber): logger.error("GetStandings() called, returned []") return [] - def GetFutureMatch(self, progrss_callback=None): + def GetFutureMatch(self, matchId, progress_callback, cancel_event): logger.error("GetFutureMatch() called, returned {}") return {} - def GetFutureMatchesList(self, sets: object, progress_callback=None, cancel_event=None): + def GetFutureMatchesList(self, setsId, progress_callback, cancel_event): logger.error("GetFutureMatchesList() called, returned []") return [] From 879b79c4e8ea2afc57602ef2dbd7906ea212a407 Mon Sep 17 00:00:00 2001 From: Vy Date: Wed, 8 Apr 2026 15:57:54 +1000 Subject: [PATCH 37/37] Square Tournament Image/Icon --- src/TournamentDataProvider/ParryGGDataProvider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TournamentDataProvider/ParryGGDataProvider.py b/src/TournamentDataProvider/ParryGGDataProvider.py index 2c9a7aac6..8bedd3e58 100644 --- a/src/TournamentDataProvider/ParryGGDataProvider.py +++ b/src/TournamentDataProvider/ParryGGDataProvider.py @@ -149,7 +149,7 @@ def GetIconURL(self): for image in get_tournament_response.tournament.images: # Look for banner image, can be replaced in future if icons are added. if image.type == ImageType.IMAGE_TYPE_BANNER: - return image.url + return image.url + "?aspect_ratio=1:1" # Fallback to the ParryGG favicon if no suitable image is found. logger.warning("No banner image found.")