· last year · Jun 12, 2024, 05:26 AM
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# Filename: nasa_epic_images.py
4# Version: 1.0.0
5# Author: Jeoi Reqi
6
7"""
8Description:
9 This script downloads images from NASA's EPIC (Earth Polychromatic Imaging Camera) satellite.
10 EPIC is a camera onboard the NOAA's DSCOVR (Deep Space Climate Observatory) spacecraft,
11 positioned at the Lagrange point 1 (L1), approximately one million miles from Earth.
12 The camera captures images of the entire sunlit side of Earth, providing a unique perspective on our planet.
13
14 The EPIC images are available in four different types:
15 - Natural: True-color images that show Earth as it appears to the human eye.
16 - Enhanced: Images that are adjusted to enhance specific features of the atmosphere and surface.
17 - Aerosol: Images that highlight the distribution and concentration of aerosols in the atmosphere.
18 - Cloud: Images that provide detailed views of cloud formations and their dynamics.
19
20 Users can specify a date range to download images for, and the images will be saved in the specified folder.
21
22Requirements:
23 - Python 3.x
24 - Required modules:
25 - requests
26 - datetime
27 - concurrent.futures
28 - time
29 - colorama
30Functions:
31 - Main:
32 - download_images_for_date:
33 Checks if images are available for a specific date and option, and downloads them if available.
34 - download_image:
35 Downloads a single image from the specified source URL and saves it to the destination folder.
36 - get_valid_start_date:
37 Prompts the user to enter a valid start date after the epoch date for the selected option.
38 - get_valid_end_date:
39 Prompts the user to enter a valid end date before today or 'today'.
40
41 - Header:
42 - generate_nasa_epic_header:
43 Generates the NASA EPIC header with color formatting.
44 - print_colored_header:
45 Prints the colored header with the NASA and EPIC logos.
46
47Classes:
48 - NASAEPICManager:
49 A class to manage NASA EPIC API requests and rate limits.
50
51Usage:
52 1. Ensure there's a file named 'nasa_api.txt' in the current working directory containing your NASA API Key(s).
53 The API key file contents must be separated by a new line if you have multiple keys.
54 2. Run the script.
55 3. Follow the on-screen prompts to select an option, specify the date range, and enter the download location.
56 4. The images will be downloaded and saved in folders according to the selected option and date.
57
58Additional Notes:
59 - You need to obtain a NASA API key to use this script.
60 - You can get it by registering on the [NASA API portal](https://api.nasa.gov/index.html?apply-for-an-api-key).
61 - After obtaining your API key, create a text file named 'nasa_api.txt' in the same directory as this script.
62 - Save your API key(s) in 'nasa_api.txt', with each key on a new line if you have multiple keys. Example:
63 - EXAMPLE:
64 - YOUR_FIRST_API_KEY
65 - YOUR_SECOND_API_KEY
66 - Colorama is initialized to automatically reset colors after each use.
67 - To download the metadata, you can go to URL: (https://pastebin.com/zAB34Acu)
68 - Save the script as 'nasa_epic_metadata.py' in your current working directory.
69 - Follow the detailed prompting from the program
70"""
71
72# Get Essential Imports
73import os
74import requests
75from datetime import datetime, timedelta
76from concurrent.futures import ThreadPoolExecutor
77import time
78from colorama import init, Fore, Style
79
80# Initialize colorama
81init(autoreset=True)
82
83class NASAEPICManager:
84 """
85 A class to manage NASA EPIC API requests and rate limits.
86
87 Attributes:
88 api_keys (list): List of API keys obtained from a file.
89 current_key_index (int): Index of the current API key being used.
90 hourly_limit (int): Maximum number of API calls allowed per hour.
91 calls_count (int): Number of API calls made in the current hour.
92 last_reset_time (float): Timestamp of the last reset time for the hourly limit.
93 """
94
95 def __init__(self, api_keys_file):
96 """
97 Initializes the NASAEPICManager with API keys and rate limit parameters.
98
99 Args:
100 api_keys_file (str): Path to the file containing API keys.
101 """
102 with open(api_keys_file, 'r', encoding='utf-8') as file:
103 self.api_keys = file.read().splitlines()
104 self.current_key_index = 0
105 self.hourly_limit = 1000 # Default value, will be updated based on the headers
106 self.calls_count = 0
107 self.last_reset_time = time.time()
108
109 def make_api_request(self, url):
110 """
111 Makes an API request to the specified URL.
112
113 Args:
114 url (str): The URL to make the API request to.
115
116 Returns:
117 dict or None: JSON response from the API if successful, otherwise None.
118 """
119 self.check_rate_limit()
120 current_api_key = self.api_keys[self.current_key_index]
121 params = {'api_key': current_api_key}
122 response = requests.get(url, params=params)
123
124 # Update rate limit counters based on response headers
125 self.calls_count += 1
126
127 # Handle non-JSON response (e.g., HTML error pages)
128 try:
129 response_json = response.json()
130 except requests.exceptions.JSONDecodeError:
131 response_json = None
132
133 remaining_requests = int(response.headers.get('X-RateLimit-Remaining', 0))
134 if self.calls_count == 1:
135 # Set the initial hourly limit based on the headers
136 self.hourly_limit = remaining_requests
137
138 self.last_reset_time = int(response.headers.get('X-RateLimit-Reset', self.last_reset_time))
139
140 print(f"API Key: {current_api_key}, \n\nRemaining Requests: {remaining_requests}")
141
142 return response_json
143
144 def check_rate_limit(self):
145 """
146 Checks and manages the API call rate limit.
147 Resets counters if more than one hour has passed or switches to the next API key if the hourly limit is reached.
148 """
149 current_time = time.time()
150 time_since_reset = current_time - self.last_reset_time
151
152 if time_since_reset >= 3600:
153 # Reset counters if more than one hour has passed
154 self.calls_count = 0
155 self.last_reset_time = current_time
156
157 if self.calls_count >= self.hourly_limit:
158 # Switch to the next API key if the hourly limit is reached
159 self.current_key_index = (self.current_key_index + 1) % len(self.api_keys)
160 self.calls_count = 0
161
162# Set epoch dates for the 4 options
163epoch_dates = {
164 'natural': '2015-06-13',
165 'enhanced': '2015-08-07',
166 'aerosol': '2020-09-04',
167 'cloud': '2023-01-03'
168}
169
170def get_valid_start_date(selected_option):
171 """
172 Prompt the user to enter a valid start date after the epoch date for the selected option.
173
174 Args:
175 selected_option (str): The selected option ('natural', 'enhanced', 'aerosol', or 'cloud').
176
177 Returns:
178 str: A valid start date in the format 'YYYY-MM-DD'.
179 """
180 while True:
181 start_date_input = input(f"\nEnter any start date after {epoch_dates[selected_option]} (YYYY-MM-DD) or type 'epoch': ")
182
183 if start_date_input.lower() == 'epoch':
184 return epoch_dates[selected_option]
185
186 try:
187 start_date = datetime.strptime(start_date_input, "%Y-%m-%d")
188 if start_date < datetime.strptime(epoch_dates[selected_option], "%Y-%m-%d"):
189 print(f"\nStart date must be after {epoch_dates[selected_option]}! Please try again.\n")
190 elif start_date > datetime.utcnow():
191 print("\nStart date cannot be in the future! Please try again.")
192 else:
193 return start_date.strftime("%Y-%m-%d")
194 except ValueError:
195 print("\nInvalid date format! (Try: 'YYYY-MM-DD) Please try again.")
196
197def get_valid_end_date():
198 """
199 Prompt the user to enter a valid end date before today or 'today'.
200
201 Returns:
202 str: A valid end date in the format 'YYYY-MM-DD'.
203 """
204 while True:
205 end_date_input = input("\nEnter any end date before today (YYYY-MM-DD) or type 'today': ")
206
207 if not end_date_input:
208 print("\nEnd date cannot be empty! Please enter a valid end date or type 'today' to use the current date.")
209 continue
210
211 if end_date_input.lower() == 'today':
212 return datetime.utcnow().strftime("%Y-%m-%d")
213
214 try:
215 end_date = datetime.strptime(end_date_input, "%Y-%m-%d")
216 if end_date > datetime.utcnow():
217 print("\nEnd date cannot be in the future! Please try again.")
218 else:
219 return end_date.strftime("%Y-%m-%d")
220 except ValueError:
221 print("\nInvalid date format! (Try: 'YYYY-MM-DD) Please try again.")
222
223def download_images_for_date(date_str, nasa_manager, selected_option, download_location):
224 """
225 Download images for a specified date and option from the NASA EPIC API.
226
227 Args:
228 date_str (str): The date in the format 'YYYY-MM-DD'.
229 nasa_manager (NASAEPICManager): An instance of the NASAEPICManager class.
230 selected_option (str): The selected option ('natural', 'enhanced', 'aerosol', or 'cloud').
231 download_location (str): The folder location where the images will be downloaded.
232
233 Returns:
234 bool: True if images are downloaded successfully, False otherwise.
235 """
236 print(f"\nChecking if images are available for date {date_str} and option {selected_option}...\n")
237
238 # Update the metadata URL based on the selected option
239 metadata_url = f"https://api.nasa.gov/EPIC/api/{selected_option}/date/{date_str}"
240 metadata = nasa_manager.make_api_request(metadata_url)
241
242 # Check if imagery is available for the date
243 if isinstance(metadata, list) and metadata:
244 # Check if there are images available in the metadata
245 if "\nNo imagery available for the date!\n" in metadata[0].get("reason", ""):
246 print(f"No imagery available for the date: {date_str}")
247 return False
248
249 # If imagery is available, create and download images to the folder
250 folder_name = f"{date_str[:4]}_{selected_option.upper()}"
251 folder_path = os.path.join(download_location, folder_name, date_str[5:7])
252 print("\nCreating folder and downloading images for date:", date_str)
253 os.makedirs(folder_path, exist_ok=True)
254
255 with ThreadPoolExecutor() as executor:
256 futures = []
257
258 for item in metadata:
259 name = item["image"] + '.png'
260 source = f"https://epic.gsfc.nasa.gov/archive/{selected_option}/{date_str[:4]}/{date_str[5:7]}/{date_str[8:10]}/png/{name}"
261 destination = os.path.join(folder_path, name)
262
263 futures.append(executor.submit(download_image, source, destination, nasa_manager))
264
265 for future in futures:
266 future.result()
267
268 print("\nImages downloaded successfully in folder:\n\n", folder_path)
269 return True
270 elif metadata is not None:
271 # Handle the case where the API request was successful but no metadata is available
272 print(f"\nNo metadata available for the date: {date_str}!\n")
273 return False
274 else:
275 # Handle the case where the API request failed (e.g., 503 error)
276 print(f"\nError fetching metadata for {date_str}! Please check if the date is valid or try again later.\n")
277 return False
278
279def download_image(source, destination, nasa_manager, max_retries=3):
280 """
281 Download an image from the specified source URL to the destination path.
282
283 Args:
284 source (str): The URL of the image to download.
285 destination (str): The local file path where the image will be saved.
286 nasa_manager (NASAEPICManager): An instance of the NASAEPICManager class for rate limit management.
287 max_retries (int): The maximum number of retry attempts in case of download failure. Default is 3.
288
289 Returns:
290 bool: True if the image is downloaded successfully, False otherwise.
291 """
292 for attempt in range(max_retries):
293 try:
294 response = requests.get(source, timeout=5) # (Default=5) Adjust the timeout value as needed (in seconds)
295 response.raise_for_status() # Check for request success
296
297 with open(destination, 'wb') as image_file:
298 image_file.write(response.content)
299
300 print("Downloaded:", os.path.basename(destination))
301 return True # Image downloaded successfully
302
303 except requests.exceptions.Timeout:
304 print(f"\nAttempt {attempt + 1}: Image download timed out! \n\nRetrying...\n")
305 continue # Retry the download
306
307 except requests.exceptions.RequestException as e:
308 print(f"\nError downloading image: {e}!\n")
309 if 'API Key' in str(e): # Retry with a new API key if the error is related to the API key
310 nasa_manager.check_rate_limit()
311 continue
312 return False # Failed to download image
313
314 print(f"\nMax retries reached! Failed to download image: \n\n{os.path.basename(destination)}\n")
315 return False
316
317# Define the header as a list of strings for easier manipulation
318header_lines = [
319 " ███ ██ █████ ███████ █████ ",
320 " ████ ██ ██ ██ ██ ██ ██ ",
321 " ██ ██ ██ ███████ ███████ ███████ ",
322 " ██ ██ ██ ██ ██ ██ ██ ██ ",
323 " ██ ████ ██ ██ ███████ ██ ██ ",
324 "███████ █████ ██████ ███████ █████ ██████ ██████ ███████ ",
325 " ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ",
326 " ██ █████ █████ ███████ ███████ █████ █████ █████ █████ ███████ ███████ ",
327 " ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ",
328 " ██ █████ ██████ ███████ █████ ██████ ██████ ███████ ",
329 " ███████ ██████ ██ ██████ ",
330 " ██ ██ ██ ██ ██ ",
331 " █████ ██████ ██ ██ ",
332 " ██ ██ ██ ██ ",
333 " ███████ ██ ██ ██████ (IMAGES) "
334]
335
336# Apply colors to the specified lines
337colored_header = []
338for i, line in enumerate(header_lines):
339 if 0 <= i < 5: # NASA
340 colored_header.append(Fore.RED + line + Style.RESET_ALL)
341 elif 5 <= i < 10: # 78-65-83-65 (ASCII CODE)
342 colored_header.append(Fore.WHITE + line + Style.RESET_ALL)
343 elif 10 <= i < 15: # EPIC
344 colored_header.append(Fore.BLUE + line + Style.RESET_ALL)
345 else:
346 colored_header.append(line)
347
348# Print the colored header
349for line in colored_header:
350 print(line)
351
352# Start menu
353while True:
354 """
355 Main loop to interact with the user, allowing them to select options for downloading EPIC images.
356
357 The loop continuously prompts the user to select an option for EPIC imagery download, input start and end dates,
358 and specify the download location. It then iterates through the specified date range, attempting to download
359 EPIC images for each date within the range.
360
361 The loop continues until the user decides to exit the program.
362
363 """
364 nasa_manager_loop = NASAEPICManager('nasa_api.txt')
365
366 # Display options for the user
367 print("_" * 83)
368 print("\nOptions:")
369 print("1. Natural")
370 print("2. Enhanced")
371 print("3. Cloud")
372 print("4. Aerosol")
373
374 # Get user's choice
375 option_loop = input("\nSelect an option (1-4): ")
376
377 # Map the user's choice to the corresponding URL component
378 options_mapping = {
379 '1': 'natural',
380 '2': 'enhanced',
381 '3': 'cloud',
382 '4': 'aerosol'
383 }
384
385 selected_option_loop = options_mapping.get(option_loop)
386
387 # Check if the selected option is valid
388 if not selected_option_loop:
389 # If the option is invalid, print an error message
390 print("\nInvalid option! Please enter a valid option number.")
391 # Since the option is invalid, continue to the next iteration of the loop
392 # to prompt the user again for a valid option
393 continue
394
395 # Get the start date from the user
396 start_date_str_loop = get_valid_start_date(selected_option_loop)
397
398 print(f"\nSelected Option: {selected_option_loop}, Start Date: {start_date_str_loop}, Epoch Date: {epoch_dates[selected_option_loop]}")
399
400 # Get the end date from the user
401 end_date_str_loop = get_valid_end_date()
402
403 # Get the download location from the user
404 download_location_loop = input("\nEnter the download location (e.g., 'EPIC_ARCHIVE'): ")
405 if not download_location_loop:
406 print("\nDownload location cannot be empty!\n")
407 continue
408
409 # Convert the start and end dates to datetime objects
410 start_date_loop = datetime.strptime(start_date_str_loop, "%Y-%m-%d")
411 end_date_loop = datetime.strptime(end_date_str_loop, "%Y-%m-%d")
412
413 # Iterate through the date range and download images for each date
414 current_date_loop = start_date_loop
415 while current_date_loop <= end_date_loop:
416 current_date_str_loop = current_date_loop.strftime("%Y-%m-%d")
417 if not download_images_for_date(current_date_str_loop, nasa_manager_loop, selected_option_loop, download_location_loop):
418 # If no images are available for the current date, move to the next date
419 current_date_loop += timedelta(days=1)
420 continue
421
422 # Move to the next date
423 current_date_loop += timedelta(days=1)
424
425 # Exit the loop if all dates have been processed
426 break
427
428# Once the loop exits, print a farewell message
429print("\nExiting Program... GoodBye!\n")
430