1+ import json
12from dataclasses import dataclass
2- from typing import Optional
3+ from typing import ClassVar , Dict , List
4+
5+ import requests
36
47
58@dataclass
69class Server :
710 # Network
811 address_ipv4 : str
912 port : int
10- address_ipv6 : Optional [ str ] = None
11- lip : Optional [ str ] = None
13+ address_ipv6 : str | None = None
14+ lip : str | None = None
1215
1316 # Game info
1417 game_name : str = ""
@@ -25,7 +28,7 @@ class Server:
2528 description : str = ""
2629 version : str = ""
2730 version_nr : int = 0
28- application_instance : Optional [ str ] = None
31+ application_instance : str | None = None
2932
3033 GAME_MODES = {
3134 1 : "Versus" ,
@@ -34,6 +37,9 @@ class Server:
3437 4 : "Survival"
3538 }
3639
40+ # Class-level cache for IP to country code mapping
41+ _country_cache : ClassVar [Dict [str , str ]] = {}
42+
3743 def __str__ (self ):
3844 return f"Server({ self .game_name } , { self .address_ipv4 } , { self .port } )"
3945
@@ -50,4 +56,118 @@ def is_full(self):
5056 @property
5157 def connection_string (self ):
5258 """Get connection string for the server."""
53- return f"{ self .address_ipv4 } :{ self .port } "
59+ return f"{ self .address_ipv4 } :{ self .port } "
60+
61+ @property
62+ def country_code (self ) -> str :
63+ """
64+ Get the country code for this server's IP address.
65+
66+ If the IP is not in cache, this will make an API request and block
67+ until the result is available. Uses single IP endpoint for efficiency.
68+
69+ Returns:
70+ Country code string (e.g., 'US', 'GB') or 'Unknown' if lookup fails
71+ """
72+ # Check if this IP is already cached
73+ if self .address_ipv4 in Server ._country_cache :
74+ return Server ._country_cache [self .address_ipv4 ]
75+
76+ # IP not in cache, use single IP lookup for efficiency
77+ return Server ._get_single_country_code (self .address_ipv4 )
78+
79+ @staticmethod
80+ def get_country_codes (servers : List ['Server' ]) -> Dict [str , str ]:
81+ """
82+ Retrieve country codes for a collection of Server instances using ip-api batch endpoint.
83+
84+ Args:
85+ servers: List of Server instances
86+
87+ Returns:
88+ Dictionary mapping IP addresses to country codes
89+ """
90+ if not servers :
91+ return {}
92+
93+ # Get unique IP addresses that aren't already cached
94+ unique_ips = set (server .address_ipv4 for server in servers )
95+ uncached_ips = [ip for ip in unique_ips if ip not in Server ._country_cache ]
96+
97+ # If all IPs are cached, return cached results
98+ if not uncached_ips :
99+ return {ip : Server ._country_cache [ip ] for ip in unique_ips }
100+
101+ # Prepare batch request for uncached IPs
102+ # ip-api batch endpoint expects a JSON array of IP addresses or objects
103+ batch_data = uncached_ips
104+
105+ try :
106+ response = requests .post (
107+ 'http://ip-api.com/batch?fields=16386' ,
108+ json = batch_data ,
109+ headers = {'Content-Type' : 'application/json' },
110+ timeout = 10
111+ )
112+ response .raise_for_status ()
113+
114+ batch_results = response .json ()
115+
116+ # Process results and update cache
117+ for i , result in enumerate (batch_results ):
118+ ip = uncached_ips [i ]
119+ if result .get ('status' ) == 'success' :
120+ country_code = result .get ('countryCode' , 'Unknown' )
121+ Server ._country_cache [ip ] = country_code
122+ else :
123+ # Cache failed lookups as 'Unknown' to avoid repeated requests
124+ Server ._country_cache [ip ] = 'Unknown'
125+
126+ except (requests .RequestException , json .JSONDecodeError , KeyError ) as e :
127+ # On error, cache all uncached IPs as 'Unknown' to avoid repeated failed requests
128+ for ip in uncached_ips :
129+ Server ._country_cache [ip ] = 'Unknown'
130+ print (f"Error fetching country codes: { e } " )
131+
132+ # Return results for all requested IPs (both cached and newly fetched)
133+ return {ip : Server ._country_cache [ip ] for ip in unique_ips }
134+
135+ @staticmethod
136+ def _get_single_country_code (ip : str ) -> str :
137+ """
138+ Fetch country code for a single IP address using the single query endpoint.
139+
140+ Args:
141+ ip: IP address to lookup
142+
143+ Returns:
144+ Country code string or 'Unknown' if lookup fails
145+ """
146+ try :
147+ response = requests .get (
148+ f'http://ip-api.com/json/{ ip } ?fields=16386' ,
149+ timeout = 10
150+ )
151+ response .raise_for_status ()
152+
153+ result = response .json ()
154+
155+ if result .get ('status' ) == 'success' :
156+ country_code = result .get ('countryCode' , 'Unknown' )
157+ Server ._country_cache [ip ] = country_code
158+ return country_code
159+ else :
160+ # Cache failed lookups as 'Unknown' to avoid repeated requests
161+ Server ._country_cache [ip ] = 'Unknown'
162+ return 'Unknown'
163+
164+ except (requests .RequestException , json .JSONDecodeError , KeyError ) as e :
165+ # On error, cache as 'Unknown' to avoid repeated failed requests
166+ Server ._country_cache [ip ] = 'Unknown'
167+ print (f"Error fetching country code for { ip } : { e } " )
168+ return 'Unknown'
169+
170+ @staticmethod
171+ def clear_country_cache ():
172+ """Clear the country code cache."""
173+ Server ._country_cache .clear ()
0 commit comments