240 lines
10 KiB
Python
240 lines
10 KiB
Python
"""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
|
|
from app.core.satellite_tle import build_tle_lines_from_elements
|
|
|
|
|
|
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:
|
|
tle_line1, tle_line2 = build_tle_lines_from_elements(
|
|
norad_cat_id=item.get("NORAD_CAT_ID"),
|
|
epoch=item.get("EPOCH"),
|
|
inclination=item.get("INCLINATION"),
|
|
raan=item.get("RAAN"),
|
|
eccentricity=item.get("ECCENTRICITY"),
|
|
arg_of_perigee=item.get("ARG_OF_PERIGEE"),
|
|
mean_anomaly=item.get("MEAN_ANOMALY"),
|
|
mean_motion=item.get("MEAN_MOTION"),
|
|
)
|
|
transformed.append(
|
|
{
|
|
"name": item.get("OBJECT_NAME", "Unknown"),
|
|
"reference_date": item.get("EPOCH", ""),
|
|
"metadata": {
|
|
"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"),
|
|
# Prefer original lines from the source, but keep a backend-built pair as a stable fallback.
|
|
"tle_line1": item.get("TLE_LINE1") or item.get("TLE1") or tle_line1,
|
|
"tle_line2": item.get("TLE_LINE2") or item.get("TLE2") or tle_line2,
|
|
},
|
|
}
|
|
)
|
|
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,
|
|
},
|
|
]
|