"""Space-Track TLE Collector Collects satellite TLE (Two-Line Element) data from Space-Track.org. API documentation: https://www.space-track.org/documentation """ import json from typing import Dict, Any, List import httpx from app.services.collectors.base import BaseCollector from app.core.data_sources import get_data_sources_config class SpaceTrackTLECollector(BaseCollector): name = "spacetrack_tle" priority = "P2" module = "L3" frequency_hours = 24 data_type = "satellite_tle" @property def base_url(self) -> str: config = get_data_sources_config() if self._resolved_url: return self._resolved_url return config.get_yaml_url("spacetrack_tle") async def fetch(self) -> List[Dict[str, Any]]: from app.core.config import settings username = settings.SPACETRACK_USERNAME password = settings.SPACETRACK_PASSWORD if not username or not password: print("SPACETRACK: No credentials configured, using sample data") return self._get_sample_data() print(f"SPACETRACK: Attempting to fetch TLE data with username: {username}") try: async with httpx.AsyncClient( timeout=120.0, follow_redirects=True, headers={ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Accept": "application/json, text/html, */*", "Accept-Language": "en-US,en;q=0.9", "Referer": "https://www.space-track.org/", }, ) as client: await client.get("https://www.space-track.org/") login_response = await client.post( "https://www.space-track.org/ajaxauth/login", data={ "identity": username, "password": password, }, ) print(f"SPACETRACK: Login response status: {login_response.status_code}") print(f"SPACETRACK: Login response URL: {login_response.url}") if login_response.status_code == 403: print("SPACETRACK: Trying alternate login method...") async with httpx.AsyncClient( timeout=120.0, follow_redirects=True, ) as alt_client: await alt_client.get("https://www.space-track.org/") form_data = { "username": username, "password": password, "query": "class/gp/NORAD_CAT_ID/25544/format/json", } alt_login = await alt_client.post( "https://www.space-track.org/ajaxauth/login", data={ "identity": username, "password": password, }, ) print(f"SPACETRACK: Alt login status: {alt_login.status_code}") if alt_login.status_code == 200: tle_response = await alt_client.get( "https://www.space-track.org/basicspacedata/query/class/gp/NORAD_CAT_ID/25544/format/json" ) if tle_response.status_code == 200: data = tle_response.json() print(f"SPACETRACK: Received {len(data)} records via alt method") return data if login_response.status_code != 200: print(f"SPACETRACK: Login failed, using sample data") return self._get_sample_data() tle_response = await client.get( "https://www.space-track.org/basicspacedata/query/class/gp/NORAD_CAT_ID/25544/format/json" ) print(f"SPACETRACK: TLE query status: {tle_response.status_code}") if tle_response.status_code != 200: print(f"SPACETRACK: Query failed, using sample data") return self._get_sample_data() data = tle_response.json() print(f"SPACETRACK: Received {len(data)} records") return data except Exception as e: print(f"SPACETRACK: Error - {e}, using sample data") return self._get_sample_data() print(f"SPACETRACK: Attempting to fetch TLE data with username: {username}") try: async with httpx.AsyncClient( timeout=120.0, follow_redirects=True, headers={ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "application/json, text/html, */*", "Accept-Language": "en-US,en;q=0.9", }, ) as client: # First, visit the main page to get any cookies await client.get("https://www.space-track.org/") # Login to get session cookie login_response = await client.post( "https://www.space-track.org/ajaxauth/login", data={ "identity": username, "password": password, }, ) print(f"SPACETRACK: Login response status: {login_response.status_code}") print(f"SPACETRACK: Login response URL: {login_response.url}") print(f"SPACETRACK: Login response body: {login_response.text[:500]}") if login_response.status_code != 200: print(f"SPACETRACK: Login failed, using sample data") return self._get_sample_data() # Query for TLE data (get first 1000 satellites) tle_response = await client.get( "https://www.space-track.org/basicspacedata/query" "/class/gp" "/orderby/EPOCH%20desc" "/limit/1000" "/format/json" ) print(f"SPACETRACK: TLE query status: {tle_response.status_code}") if tle_response.status_code != 200: print(f"SPACETRACK: Query failed, using sample data") return self._get_sample_data() data = tle_response.json() print(f"SPACETRACK: Received {len(data)} records") return data except Exception as e: print(f"SPACETRACK: Error - {e}, using sample data") return self._get_sample_data() def transform(self, raw_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """Transform TLE data to internal format""" transformed = [] for item in raw_data: transformed.append( { "name": item.get("OBJECT_NAME", "Unknown"), "norad_cat_id": item.get("NORAD_CAT_ID"), "international_designator": item.get("INTL_DESIGNATOR"), "epoch": item.get("EPOCH"), "mean_motion": item.get("MEAN_MOTION"), "eccentricity": item.get("ECCENTRICITY"), "inclination": item.get("INCLINATION"), "raan": item.get("RAAN"), "arg_of_perigee": item.get("ARG_OF_PERIGEE"), "mean_anomaly": item.get("MEAN_ANOMALY"), "ephemeris_type": item.get("EPHEMERIS_TYPE"), "classification_type": item.get("CLASSIFICATION_TYPE"), "element_set_no": item.get("ELEMENT_SET_NO"), "rev_at_epoch": item.get("REV_AT_EPOCH"), "bstar": item.get("BSTAR"), "mean_motion_dot": item.get("MEAN_MOTION_DOT"), "mean_motion_ddot": item.get("MEAN_MOTION_DDOT"), } ) return transformed def _get_sample_data(self) -> List[Dict[str, Any]]: """Return sample TLE data for testing""" return [ { "name": "ISS (ZARYA)", "norad_cat_id": 25544, "international_designator": "1998-067A", "epoch": "2026-03-13T00:00:00Z", "mean_motion": 15.49872723, "eccentricity": 0.0006292, "inclination": 51.6400, "raan": 315.0000, "arg_of_perigee": 100.0000, "mean_anomaly": 260.0000, }, { "name": "STARLINK-1000", "norad_cat_id": 44720, "international_designator": "2019-029AZ", "epoch": "2026-03-13T00:00:00Z", "mean_motion": 15.79234567, "eccentricity": 0.0001234, "inclination": 53.0000, "raan": 120.0000, "arg_of_perigee": 90.0000, "mean_anomaly": 270.0000, }, ]