AK51 commited on
Commit
54016d6
ยท
verified ยท
1 Parent(s): 705d0b3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1168 -7
app.py CHANGED
@@ -1,7 +1,1168 @@
1
- import gradio as gr
2
-
3
- def greet(name):
4
- return "Hello " + name + "!!"
5
-
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ NASA Solar Image Downloader - Complete Gradio Web Interface
4
+ Web-based interface with all features from the original GUI application.
5
+ """
6
+
7
+ import sys
8
+ import os
9
+ from pathlib import Path
10
+ from datetime import datetime, timedelta
11
+ import subprocess
12
+ import shutil
13
+ import threading
14
+ import time
15
+
16
+ # Add src to Python path
17
+ sys.path.insert(0, str(Path(__file__).parent / "src"))
18
+
19
+ try:
20
+ import gradio as gr
21
+ from PIL import Image
22
+ import cv2
23
+ import numpy as np
24
+ except ImportError as e:
25
+ print(f"โŒ Required libraries not available: {e}")
26
+ print("๐Ÿ’ก Install with: pip install gradio pillow opencv-python")
27
+ sys.exit(1)
28
+
29
+ from src.downloader.directory_scraper import DirectoryScraper
30
+ from src.storage.storage_organizer import StorageOrganizer
31
+ from src.downloader.image_fetcher import ImageFetcher, DownloadManager
32
+
33
+
34
+ class NASADownloaderGradio:
35
+ """Complete Gradio web interface for NASA Solar Image Downloader."""
36
+
37
+ def __init__(self):
38
+ """Initialize the Gradio application."""
39
+ self.resolution = "1024"
40
+ self.solar_filter = "0211"
41
+
42
+ # Initialize components
43
+ self.storage = StorageOrganizer("data",
44
+ resolution=self.resolution,
45
+ solar_filter=self.solar_filter)
46
+ self.scraper = DirectoryScraper(rate_limit_delay=1.0,
47
+ resolution=self.resolution,
48
+ solar_filter=self.solar_filter)
49
+ self.fetcher = ImageFetcher(rate_limit_delay=1.0)
50
+ self.download_manager = DownloadManager(self.fetcher, self.storage)
51
+
52
+ # Filter data with full information and thumbnail paths
53
+ self.filter_data = {
54
+ "0193": {"name": "193 ร…", "desc": "Coronal loops", "color": "#ff6b6b", "image": "src/ui_img/20251220_000753_1024_0193.jpg"},
55
+ "0304": {"name": "304 ร…", "desc": "Chromosphere", "color": "#4ecdc4", "image": "src/ui_img/20251220_000854_1024_0304.jpg"},
56
+ "0171": {"name": "171 ร…", "desc": "Quiet corona", "color": "#45b7d1", "image": "src/ui_img/20251220_000658_1024_0171.jpg"},
57
+ "0211": {"name": "211 ร…", "desc": "Active regions", "color": "#f9ca24", "image": "src/ui_img/20251220_000035_1024_0211.jpg"},
58
+ "0131": {"name": "131 ร…", "desc": "Flaring regions", "color": "#f0932b", "image": "src/ui_img/20251220_000644_1024_0131.jpg"},
59
+ "0335": {"name": "335 ร…", "desc": "Active cores", "color": "#eb4d4b", "image": "src/ui_img/20251220_000114_1024_0335.jpg"},
60
+ "0094": {"name": "94 ร…", "desc": "Hot plasma", "color": "#6c5ce7", "image": "src/ui_img/20251220_000600_1024_0094.jpg"},
61
+ "1600": {"name": "1600 ร…", "desc": "Transition region", "color": "#a29bfe", "image": "src/ui_img/20251220_000151_1024_1600.jpg"},
62
+ "1700": {"name": "1700 ร…", "desc": "Temperature min", "color": "#fd79a8", "image": "src/ui_img/20251220_000317_1024_1700.jpg"},
63
+ "094335193": {"name": "094+335+193", "desc": "Composite: Hot plasma + Active cores + Coronal loops", "color": "#8e44ad", "image": "src/ui_img/20251219_000311_1024_094335193.jpg"},
64
+ "304211171": {"name": "304+211+171", "desc": "Composite: Chromosphere + Active regions + Quiet corona", "color": "#e67e22", "image": "src/ui_img/20251219_000311_1024_304211171.jpg"},
65
+ "211193171": {"name": "211+193+171", "desc": "Composite: Active regions + Coronal loops + Quiet corona", "color": "#27ae60", "image": "src/ui_img/20251219_001633_1024_211193171.jpg"}
66
+ }
67
+
68
+ # Custom keywords for advanced users
69
+ self.custom_keywords = {filter_num: filter_num for filter_num in self.filter_data.keys()}
70
+
71
+ # Image viewer state
72
+ self.current_images = []
73
+ self.current_image_index = 0
74
+ self.is_playing = False
75
+ self.play_speed = 120.0 # FPS for playback
76
+ self.last_update_time = 0 # Track last update time for playback
77
+
78
+ def set_date_range(self, days_back):
79
+ """Set date range for quick selection."""
80
+ end_date = datetime.now()
81
+ start_date = end_date - timedelta(days=days_back)
82
+ return start_date.strftime("%Y-%m-%d"), end_date.strftime("%Y-%m-%d")
83
+
84
+ def download_images(self, start_date, end_date, resolution, solar_filter, progress=gr.Progress()):
85
+ """Download images for the specified date range."""
86
+ try:
87
+ # Update settings
88
+ self.resolution = resolution
89
+ self.solar_filter = solar_filter
90
+
91
+ # Use custom keyword if available
92
+ search_keyword = self.custom_keywords.get(solar_filter, solar_filter)
93
+
94
+ self.scraper.update_filters(resolution, search_keyword)
95
+ self.storage.update_file_pattern(resolution, search_keyword)
96
+
97
+ # Parse dates
98
+ start = datetime.strptime(start_date, "%Y-%m-%d")
99
+ end = datetime.strptime(end_date, "%Y-%m-%d")
100
+
101
+ progress(0, desc="Scanning directories...")
102
+
103
+ # Get available images
104
+ available_images = self.scraper.get_available_images_for_date_range(start, end)
105
+
106
+ if not available_images:
107
+ return f"โŒ No images found for date range {start_date} to {end_date}", self.get_available_dates()
108
+
109
+ progress(0.2, desc=f"Found {len(available_images)} images")
110
+
111
+ # Filter new images
112
+ new_images = self.scraper.filter_new_images(available_images, self.storage)
113
+
114
+ if not new_images:
115
+ return f"โœ… All {len(available_images)} images already downloaded!", self.get_available_dates()
116
+
117
+ progress(0.3, desc=f"Downloading {len(new_images)} new images...")
118
+
119
+ # Create download tasks
120
+ tasks = self.scraper.create_download_tasks(new_images, self.storage)
121
+
122
+ # Download images
123
+ successful = 0
124
+ failed = 0
125
+
126
+ for i, task in enumerate(tasks):
127
+ progress((0.3 + (i / len(tasks)) * 0.6), desc=f"Downloading {i+1}/{len(tasks)}: {task.target_path.name}")
128
+
129
+ success = self.download_manager.download_and_save(task)
130
+
131
+ if success:
132
+ successful += 1
133
+ else:
134
+ failed += 1
135
+
136
+ progress(1.0, desc="Complete!")
137
+
138
+ result = f"โœ… Download complete!\n"
139
+ result += f"๐Ÿ“ฅ Downloaded: {successful} images\n"
140
+ result += f"โŒ Failed: {failed} images\n"
141
+ result += f"๐Ÿ“Š Total available: {len(available_images)} images\n"
142
+ result += f"๏ฟฝ DFilter: {self.filter_data[solar_filter]['name']} - {self.filter_data[solar_filter]['desc']}"
143
+
144
+ return result, self.get_available_dates()
145
+
146
+ except Exception as e:
147
+ return f"โŒ Error: {str(e)}", self.get_available_dates()
148
+
149
+ def get_latest_image(self):
150
+ """Get the most recent downloaded image."""
151
+ try:
152
+ data_dir = self.storage.base_data_dir
153
+
154
+ # Find the most recent image
155
+ all_images = []
156
+ for year_dir in sorted(data_dir.iterdir(), reverse=True):
157
+ if not year_dir.is_dir():
158
+ continue
159
+ for month_dir in sorted(year_dir.iterdir(), reverse=True):
160
+ if not month_dir.is_dir():
161
+ continue
162
+ for day_dir in sorted(month_dir.iterdir(), reverse=True):
163
+ if not day_dir.is_dir():
164
+ continue
165
+
166
+ images = list(day_dir.glob(f"*_{self.resolution}_*.jpg"))
167
+ if images:
168
+ all_images.extend(images)
169
+
170
+ if all_images:
171
+ # Sort by filename (which includes timestamp) and get the latest
172
+ latest = sorted(all_images, reverse=True)[0]
173
+ return str(latest)
174
+
175
+ return None
176
+
177
+ except Exception as e:
178
+ print(f"Error getting latest image: {e}")
179
+ return None
180
+
181
+ def get_available_dates(self, resolution=None, solar_filter=None):
182
+ """Get list of available dates with images for specific resolution and filter."""
183
+ dates = []
184
+ data_dir = self.storage.base_data_dir
185
+
186
+ # Use current settings if not specified
187
+ if resolution is None:
188
+ resolution = self.resolution
189
+ if solar_filter is None:
190
+ solar_filter = self.solar_filter
191
+
192
+ # Use custom keyword if available
193
+ search_keyword = self.custom_keywords.get(solar_filter, solar_filter)
194
+
195
+ if data_dir.exists():
196
+ for year_dir in data_dir.iterdir():
197
+ if not year_dir.is_dir() or not year_dir.name.isdigit():
198
+ continue
199
+
200
+ for month_dir in year_dir.iterdir():
201
+ if not month_dir.is_dir() or not month_dir.name.isdigit():
202
+ continue
203
+
204
+ for day_dir in month_dir.iterdir():
205
+ if not day_dir.is_dir() or not day_dir.name.isdigit():
206
+ continue
207
+
208
+ # Filter images by resolution and solar filter
209
+ all_images = list(day_dir.glob("*.jpg"))
210
+ filtered_images = [img for img in all_images
211
+ if f"_{resolution}_" in img.name and search_keyword in img.name]
212
+
213
+ if filtered_images:
214
+ try:
215
+ date = datetime(int(year_dir.name), int(month_dir.name), int(day_dir.name))
216
+ date_str = f"{date.strftime('%Y-%m-%d')} ({len(filtered_images)} images)"
217
+ dates.append(date_str)
218
+ except ValueError:
219
+ continue
220
+
221
+ return sorted(dates, reverse=True)
222
+
223
+ def load_images_for_date_range(self, from_date, to_date, resolution, solar_filter):
224
+ """Load images for a date range with specific resolution and filter."""
225
+ try:
226
+ # Update settings
227
+ self.resolution = resolution
228
+ self.solar_filter = solar_filter
229
+
230
+ # Use custom keyword if available
231
+ search_keyword = self.custom_keywords.get(solar_filter, solar_filter)
232
+
233
+ # Update storage pattern to match selected filter and resolution
234
+ self.storage.update_file_pattern(resolution, search_keyword)
235
+
236
+ start_date = datetime.strptime(from_date.split(' ')[0], "%Y-%m-%d")
237
+ end_date = datetime.strptime(to_date.split(' ')[0], "%Y-%m-%d")
238
+
239
+ # Ensure from_date is not after to_date
240
+ if start_date > end_date:
241
+ start_date, end_date = end_date, start_date
242
+
243
+ # Load images from all dates in the range
244
+ self.current_images = []
245
+ total_images = 0
246
+
247
+ # Get all dates in range
248
+ current_date = start_date
249
+ while current_date <= end_date:
250
+ images = self.storage.list_local_images(current_date)
251
+
252
+ if images:
253
+ date_path = self.storage.get_date_path(current_date)
254
+
255
+ # Filter images by resolution and solar filter
256
+ for filename in sorted(images):
257
+ # Check if filename matches the selected resolution and filter
258
+ if f"_{resolution}_" in filename and search_keyword in filename:
259
+ image_path = date_path / filename
260
+ self.current_images.append((str(image_path), filename, current_date))
261
+ total_images += 1
262
+
263
+ current_date += timedelta(days=1)
264
+
265
+ if not self.current_images:
266
+ filter_name = self.filter_data[solar_filter]['name']
267
+ return None, f"โŒ No images found for date range {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}\nResolution: {resolution}px, Filter: {filter_name}", "0 / 0"
268
+
269
+ # Sort all images by filename (which includes timestamp)
270
+ self.current_images.sort(key=lambda x: x[1])
271
+
272
+ self.current_image_index = 0
273
+
274
+ date_range_text = f"{start_date.strftime('%Y-%m-%d')}" if start_date == end_date else f"{start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}"
275
+ filter_name = self.filter_data[solar_filter]['name']
276
+
277
+ return self.current_images[0][0], f"โœ… Loaded {total_images} images for {date_range_text}\nResolution: {resolution}px, Filter: {filter_name}", f"1 / {len(self.current_images)}"
278
+
279
+ except Exception as e:
280
+ return None, f"โŒ Error: {str(e)}", "0 / 0"
281
+
282
+ def navigate_image(self, direction):
283
+ """Navigate through images."""
284
+ if not self.current_images:
285
+ return None, "No images loaded", "0 / 0", "โ–ถ Play"
286
+
287
+ if direction == "first":
288
+ self.current_image_index = 0
289
+ elif direction == "prev":
290
+ self.current_image_index = max(0, self.current_image_index - 1)
291
+ elif direction == "next":
292
+ self.current_image_index = min(len(self.current_images) - 1, self.current_image_index + 1)
293
+ elif direction == "last":
294
+ self.current_image_index = len(self.current_images) - 1
295
+
296
+ current_image = self.current_images[self.current_image_index]
297
+ image_path, filename, image_date = current_image
298
+
299
+ # Extract timestamp
300
+ timestamp = filename.split('_')[1] if '_' in filename else "Unknown"
301
+ if len(timestamp) == 6:
302
+ formatted_time = f"{timestamp[:2]}:{timestamp[2:4]}:{timestamp[4:6]}"
303
+ else:
304
+ formatted_time = timestamp
305
+
306
+ info_text = f"๐Ÿ“… {image_date.strftime('%Y-%m-%d')} โฐ {formatted_time}"
307
+ position_text = f"{self.current_image_index + 1} / {len(self.current_images)}"
308
+ play_button_text = "โธ Pause" if self.is_playing else "โ–ถ Play"
309
+
310
+ return image_path, info_text, position_text, play_button_text
311
+
312
+ def toggle_play(self):
313
+ """Toggle play/pause for image sequence."""
314
+ if not self.current_images:
315
+ return None, "No images loaded", "0 / 0", "โ–ถ Play", f"{self.play_speed:.1f} FPS"
316
+
317
+ if self.is_playing:
318
+ self.is_playing = False
319
+ play_button_text = "โ–ถ Play"
320
+ else:
321
+ self.is_playing = True
322
+ play_button_text = "โธ Pause"
323
+ self.last_update_time = time.time() # Reset timer
324
+
325
+ current_image = self.current_images[self.current_image_index]
326
+ image_path, filename, image_date = current_image
327
+
328
+ # Extract timestamp
329
+ timestamp = filename.split('_')[1] if '_' in filename else "Unknown"
330
+ if len(timestamp) == 6:
331
+ formatted_time = f"{timestamp[:2]}:{timestamp[2:4]}:{timestamp[4:6]}"
332
+ else:
333
+ formatted_time = timestamp
334
+
335
+ info_text = f"๐Ÿ“… {image_date.strftime('%Y-%m-%d')} โฐ {formatted_time}"
336
+ position_text = f"{self.current_image_index + 1} / {len(self.current_images)}"
337
+
338
+ return image_path, info_text, position_text, play_button_text, f"{self.play_speed:.1f} FPS"
339
+
340
+ def update_playback(self):
341
+ """Update playback - called by timer."""
342
+ if not self.is_playing or not self.current_images:
343
+ # Return current state without changes
344
+ if not self.current_images:
345
+ return None, "No images loaded", "0 / 0", "โ–ถ Play", f"{self.play_speed:.1f} FPS"
346
+
347
+ current_image = self.current_images[self.current_image_index]
348
+ image_path, filename, image_date = current_image
349
+
350
+ # Extract timestamp
351
+ timestamp = filename.split('_')[1] if '_' in filename else "Unknown"
352
+ if len(timestamp) == 6:
353
+ formatted_time = f"{timestamp[:2]}:{timestamp[2:4]}:{timestamp[4:6]}"
354
+ else:
355
+ formatted_time = timestamp
356
+
357
+ info_text = f"๐Ÿ“… {image_date.strftime('%Y-%m-%d')} โฐ {formatted_time}"
358
+ position_text = f"{self.current_image_index + 1} / {len(self.current_images)}"
359
+ play_button_text = "โธ Pause" if self.is_playing else "โ–ถ Play"
360
+
361
+ return image_path, info_text, position_text, play_button_text, f"{self.play_speed:.1f} FPS"
362
+
363
+ # Check if enough time has passed for next frame
364
+ current_time = time.time()
365
+ frame_interval = 1.0 / self.play_speed
366
+
367
+ if current_time - self.last_update_time >= frame_interval:
368
+ # Advance to next image
369
+ if self.current_image_index >= len(self.current_images) - 1:
370
+ self.current_image_index = 0 # Loop back to start
371
+ else:
372
+ self.current_image_index += 1
373
+
374
+ self.last_update_time = current_time
375
+
376
+ # Return current image
377
+ current_image = self.current_images[self.current_image_index]
378
+ image_path, filename, image_date = current_image
379
+
380
+ # Extract timestamp
381
+ timestamp = filename.split('_')[1] if '_' in filename else "Unknown"
382
+ if len(timestamp) == 6:
383
+ formatted_time = f"{timestamp[:2]}:{timestamp[2:4]}:{timestamp[4:6]}"
384
+ else:
385
+ formatted_time = timestamp
386
+
387
+ info_text = f"๐Ÿ“… {image_date.strftime('%Y-%m-%d')} โฐ {formatted_time}"
388
+ position_text = f"{self.current_image_index + 1} / {len(self.current_images)}"
389
+ play_button_text = "โธ Pause"
390
+
391
+ return image_path, info_text, position_text, play_button_text, f"{self.play_speed:.1f} FPS"
392
+
393
+ def update_play_speed(self, speed):
394
+ """Update playback speed."""
395
+ self.play_speed = float(speed)
396
+ return f"{self.play_speed:.1f} FPS"
397
+
398
+ def select_video_file(self):
399
+ """Select and preview video file."""
400
+ # This would be handled by Gradio's file upload component
401
+ # Return placeholder for now
402
+ return "Please use the file upload component to select an MP4 video"
403
+
404
+ def get_video_list(self):
405
+ """Get list of available video files."""
406
+ video_dir = Path("video")
407
+ if not video_dir.exists():
408
+ return []
409
+
410
+ videos = []
411
+ for video_file in video_dir.glob("*.mp4"):
412
+ try:
413
+ size_mb = video_file.stat().st_size / (1024 * 1024)
414
+ videos.append(f"{video_file.name} ({size_mb:.1f} MB)")
415
+ except:
416
+ videos.append(video_file.name)
417
+
418
+ return sorted(videos, reverse=True)
419
+
420
+ def open_data_folder(self):
421
+ """Open data folder (returns path for web interface)."""
422
+ data_dir = self.storage.base_data_dir
423
+ return f"๐Ÿ“ Data folder location: {data_dir.absolute()}"
424
+
425
+ def cleanup_corrupted_files(self):
426
+ """Clean up corrupted files."""
427
+ total_removed = 0
428
+ data_dir = self.storage.base_data_dir
429
+
430
+ if data_dir.exists():
431
+ for year_dir in data_dir.iterdir():
432
+ if not year_dir.is_dir():
433
+ continue
434
+ for month_dir in year_dir.iterdir():
435
+ if not month_dir.is_dir():
436
+ continue
437
+ for day_dir in month_dir.iterdir():
438
+ if not day_dir.is_dir():
439
+ continue
440
+
441
+ try:
442
+ date = datetime(int(year_dir.name), int(month_dir.name), int(day_dir.name))
443
+ removed = self.storage.cleanup_corrupted_files(date)
444
+ total_removed += removed
445
+ except:
446
+ continue
447
+
448
+ return f"๐Ÿงน Cleanup complete! Removed {total_removed} corrupted files."
449
+
450
+ def create_video(self, start_date, end_date, fps, resolution, solar_filter, progress=gr.Progress()):
451
+ """Create MP4 video from images."""
452
+ try:
453
+ # Update settings
454
+ self.resolution = resolution
455
+ self.solar_filter = solar_filter
456
+
457
+ # Use custom keyword if available
458
+ search_keyword = self.custom_keywords.get(solar_filter, solar_filter)
459
+
460
+ self.scraper.update_filters(resolution, search_keyword)
461
+ self.storage.update_file_pattern(resolution, search_keyword)
462
+
463
+ # Parse dates
464
+ start = datetime.strptime(start_date, "%Y-%m-%d")
465
+ end = datetime.strptime(end_date, "%Y-%m-%d")
466
+
467
+ progress(0.1, desc="Collecting images...")
468
+
469
+ # Collect all images from the date range
470
+ all_image_paths = []
471
+ current_date = start
472
+
473
+ while current_date <= end:
474
+ images = self.storage.list_local_images(current_date)
475
+ if images:
476
+ date_path = self.storage.get_date_path(current_date)
477
+ for filename in sorted(images):
478
+ image_path = date_path / filename
479
+ if image_path.exists():
480
+ all_image_paths.append(image_path)
481
+ current_date += timedelta(days=1)
482
+
483
+ if not all_image_paths:
484
+ return None, f"โŒ No images found for date range {start_date} to {end_date}"
485
+
486
+ progress(0.2, desc=f"Found {len(all_image_paths)} images. Creating video...")
487
+
488
+ # Create video directory
489
+ video_dir = Path("video")
490
+ video_dir.mkdir(exist_ok=True)
491
+
492
+ # Generate output filename
493
+ if start == end:
494
+ output_file = f"nasa_solar_{start.strftime('%Y%m%d')}.mp4"
495
+ else:
496
+ output_file = f"nasa_solar_{start.strftime('%Y%m%d')}_to_{end.strftime('%Y%m%d')}.mp4"
497
+
498
+ output_path = video_dir / output_file
499
+
500
+ # Create temporary directory for ffmpeg
501
+ temp_dir = Path("temp_video_frames")
502
+ temp_dir.mkdir(exist_ok=True)
503
+
504
+ try:
505
+ # Create sequential frame files
506
+ for i, src_path in enumerate(all_image_paths):
507
+ progress(0.2 + (i / len(all_image_paths)) * 0.5,
508
+ desc=f"Preparing frame {i+1}/{len(all_image_paths)}")
509
+
510
+ temp_path = temp_dir / f"frame_{i:06d}.jpg"
511
+ if temp_path.exists():
512
+ temp_path.unlink()
513
+
514
+ try:
515
+ temp_path.symlink_to(src_path.absolute())
516
+ except OSError:
517
+ shutil.copy2(src_path, temp_path)
518
+
519
+ progress(0.7, desc="Running FFmpeg to create video...")
520
+
521
+ # Run ffmpeg
522
+ input_pattern = str(temp_dir / "frame_%06d.jpg")
523
+ ffmpeg_cmd = [
524
+ 'ffmpeg', '-y', '-framerate', str(fps),
525
+ '-i', input_pattern, '-c:v', 'libx264',
526
+ '-pix_fmt', 'yuv420p', '-crf', '18',
527
+ str(output_path)
528
+ ]
529
+
530
+ result = subprocess.run(ffmpeg_cmd, capture_output=True, text=True)
531
+
532
+ progress(1.0, desc="Video creation complete!")
533
+
534
+ if result.returncode == 0:
535
+ size_mb = output_path.stat().st_size / (1024 * 1024)
536
+ duration = len(all_image_paths) / fps
537
+
538
+ message = f"โœ… Video created successfully!\n"
539
+ message += f"๐Ÿ“ File: {output_path.name}\n"
540
+ message += f"๐Ÿ’พ Size: {size_mb:.1f} MB\n"
541
+ message += f"๐ŸŽž๏ธ Frames: {len(all_image_paths)}\n"
542
+ message += f"โฑ๏ธ Duration: {duration:.1f} seconds\n"
543
+ message += f"๏ฟฝ๏ฟฝ๏ฟฝ๏ฟฝ Filter: {self.filter_data[solar_filter]['name']}"
544
+
545
+ return str(output_path), message
546
+ else:
547
+ return None, f"โŒ FFmpeg error: {result.stderr}"
548
+
549
+ finally:
550
+ if temp_dir.exists():
551
+ shutil.rmtree(temp_dir)
552
+
553
+ except Exception as e:
554
+ return None, f"โŒ Error: {str(e)}"
555
+
556
+ def get_system_info(self):
557
+ """Get system information."""
558
+ info = "๐Ÿ–ฅ๏ธ **System Information**\n\n"
559
+
560
+ # Check FFmpeg
561
+ try:
562
+ result = subprocess.run(['ffmpeg', '-version'], capture_output=True, timeout=5)
563
+ ffmpeg_status = "โœ… Available" if result.returncode == 0 else "โŒ Not found"
564
+ except:
565
+ ffmpeg_status = "โŒ Not found"
566
+
567
+ info += f"**FFmpeg**: {ffmpeg_status}\n"
568
+
569
+ # Check OpenCV
570
+ try:
571
+ import cv2
572
+ opencv_status = f"โœ… Available (v{cv2.__version__})"
573
+ except:
574
+ opencv_status = "โŒ Not found"
575
+
576
+ info += f"**OpenCV**: {opencv_status}\n"
577
+
578
+ # Check PIL
579
+ try:
580
+ from PIL import Image
581
+ pil_status = f"โœ… Available (v{Image.__version__})"
582
+ except:
583
+ pil_status = "โŒ Not found"
584
+
585
+ info += f"**Pillow**: {pil_status}\n\n"
586
+
587
+ # Data directory info
588
+ data_dir = self.storage.base_data_dir
589
+ if data_dir.exists():
590
+ info += f"**Data Directory**: {data_dir.absolute()}\n"
591
+
592
+ # Count total images
593
+ total_images = 0
594
+ for year_dir in data_dir.iterdir():
595
+ if year_dir.is_dir():
596
+ for month_dir in year_dir.iterdir():
597
+ if month_dir.is_dir():
598
+ for day_dir in month_dir.iterdir():
599
+ if day_dir.is_dir():
600
+ images = list(day_dir.glob("*.jpg"))
601
+ total_images += len(images)
602
+
603
+ info += f"**Total Images**: {total_images}\n"
604
+ else:
605
+ info += f"**Data Directory**: Not created yet\n"
606
+
607
+ info += f"\n**Created by Andy Kong**"
608
+
609
+ return info
610
+
611
+ def update_custom_keyword(self, filter_name, keyword):
612
+ """Update custom keyword for a filter."""
613
+ if filter_name in self.custom_keywords:
614
+ self.custom_keywords[filter_name] = keyword.strip() if keyword.strip() else filter_name
615
+ return f"โœ… Updated {filter_name} keyword to: {self.custom_keywords[filter_name]}"
616
+ return f"โŒ Invalid filter: {filter_name}"
617
+
618
+ def get_filter_gallery_data(self):
619
+ """Get gallery data for filter selection with thumbnails."""
620
+ gallery_data = []
621
+ for filter_key, data in self.filter_data.items():
622
+ # Create gallery item with image path and caption
623
+ caption = f"{data['name']}\n{data['desc']}"
624
+ gallery_data.append((data['image'], caption))
625
+ return gallery_data
626
+
627
+ def get_filter_key_from_gallery_index(self, index):
628
+ """Get filter key from gallery selection index."""
629
+ filter_keys = list(self.filter_data.keys())
630
+ if 0 <= index < len(filter_keys):
631
+ return filter_keys[index]
632
+ return "0211" # Default
633
+
634
+ def get_gallery_index_from_filter_key(self, filter_key):
635
+ """Get gallery index from filter key."""
636
+ filter_keys = list(self.filter_data.keys())
637
+ try:
638
+ return filter_keys.index(filter_key)
639
+ except ValueError:
640
+ return 3 # Default to 0211 (index 3)
641
+
642
+ def on_filter_gallery_select(self, evt: gr.SelectData):
643
+ """Handle filter gallery selection."""
644
+ if evt.index is not None:
645
+ filter_key = self.get_filter_key_from_gallery_index(evt.index)
646
+ filter_data = self.filter_data[filter_key]
647
+ info_text = f"**Selected:** {filter_data['name']} - {filter_data['desc']}"
648
+ return filter_key, info_text
649
+ return "0211", "**Selected:** 211 ร… - Active regions"
650
+
651
+ def reset_custom_keywords(self):
652
+ """Reset all custom keywords to defaults."""
653
+ self.custom_keywords = {filter_num: filter_num for filter_num in self.filter_data.keys()}
654
+ return "โœ… All keywords reset to defaults"
655
+
656
+ def create_interface(self):
657
+ """Create the complete Gradio interface with all features."""
658
+
659
+ with gr.Blocks(title="๐ŸŒž NASA Solar Image Downloader") as app:
660
+ gr.Markdown("# ๐ŸŒž NASA Solar Image Downloader")
661
+ gr.Markdown("**Complete web interface** - Download, view, and create videos from NASA Solar Dynamics Observatory images")
662
+
663
+ with gr.Tabs():
664
+ # Download Tab
665
+ with gr.Tab("๐Ÿ“ฅ Download Images"):
666
+ gr.Markdown("### Download NASA Solar Images")
667
+
668
+ with gr.Row():
669
+ with gr.Column():
670
+ gr.Markdown("#### ๐Ÿ—“๏ธ Quick Date Selection")
671
+ with gr.Row():
672
+ today_btn = gr.Button("Today", size="sm")
673
+ last3_btn = gr.Button("Last 3 Days", size="sm")
674
+ lastweek_btn = gr.Button("Last Week", size="sm")
675
+
676
+ gr.Markdown("#### ๐Ÿ“… Custom Date Range")
677
+ download_start_date = gr.Textbox(
678
+ label="Start Date (YYYY-MM-DD)",
679
+ value=datetime.now().strftime("%Y-%m-%d")
680
+ )
681
+ download_end_date = gr.Textbox(
682
+ label="End Date (YYYY-MM-DD)",
683
+ value=datetime.now().strftime("%Y-%m-%d")
684
+ )
685
+
686
+ gr.Markdown("#### ๐Ÿ”ง Image Settings")
687
+ download_resolution = gr.Dropdown(
688
+ choices=["1024", "2048", "4096"],
689
+ value="1024",
690
+ label="Resolution (pixels)"
691
+ )
692
+
693
+ download_btn = gr.Button("๐Ÿ” Find & Download Images", variant="primary", size="lg")
694
+
695
+ with gr.Column():
696
+ gr.Markdown("#### ๐ŸŒž Solar Filter Selection")
697
+ gr.Markdown("*Click on a thumbnail to select a solar filter*")
698
+
699
+ download_filter_gallery = gr.Gallery(
700
+ value=self.get_filter_gallery_data(),
701
+ label="Solar Filters",
702
+ columns=4,
703
+ rows=3,
704
+ height="auto",
705
+ object_fit="contain",
706
+ show_label=False,
707
+ selected_index=3 # Default to 0211
708
+ )
709
+
710
+ # Hidden state to store the selected filter key
711
+ download_filter = gr.State(value="0211")
712
+
713
+ # Display selected filter info
714
+ download_filter_info = gr.Markdown("**Selected:** 211 ร… - Active regions")
715
+
716
+ download_output = gr.Textbox(label="Download Status", lines=8)
717
+
718
+ # Quick date button actions
719
+ today_btn.click(
720
+ fn=lambda: self.set_date_range(0),
721
+ outputs=[download_start_date, download_end_date]
722
+ )
723
+ last3_btn.click(
724
+ fn=lambda: self.set_date_range(2),
725
+ outputs=[download_start_date, download_end_date]
726
+ )
727
+ lastweek_btn.click(
728
+ fn=lambda: self.set_date_range(6),
729
+ outputs=[download_start_date, download_end_date]
730
+ )
731
+
732
+ # Store available dates for refresh
733
+ available_dates_state = gr.State(value=self.get_available_dates())
734
+
735
+ # Gallery selection event for download tab
736
+ download_filter_gallery.select(
737
+ fn=self.on_filter_gallery_select,
738
+ outputs=[download_filter, download_filter_info]
739
+ )
740
+
741
+ download_btn.click(
742
+ fn=self.download_images,
743
+ inputs=[download_start_date, download_end_date, download_resolution, download_filter],
744
+ outputs=[download_output, available_dates_state]
745
+ )
746
+
747
+ # View Images Tab
748
+ with gr.Tab("๐Ÿ‘๏ธ View Images"):
749
+ gr.Markdown("### View Downloaded Images with Full Playback Controls")
750
+
751
+ with gr.Row():
752
+ with gr.Column():
753
+ gr.Markdown("#### ๐Ÿ”ง Image Settings")
754
+ with gr.Row():
755
+ view_resolution = gr.Dropdown(
756
+ choices=["1024", "2048", "4096"],
757
+ value="1024",
758
+ label="Resolution (pixels)",
759
+ scale=1
760
+ )
761
+
762
+ gr.Markdown("#### ๐ŸŒž Solar Filter Selection")
763
+ gr.Markdown("*Click on a thumbnail to select a solar filter*")
764
+
765
+ view_filter_gallery = gr.Gallery(
766
+ value=self.get_filter_gallery_data(),
767
+ label="Solar Filters",
768
+ columns=4,
769
+ rows=3,
770
+ height="auto",
771
+ object_fit="contain",
772
+ show_label=False,
773
+ selected_index=3 # Default to 0211
774
+ )
775
+
776
+ # Hidden state to store the selected filter key
777
+ view_filter = gr.State(value="0211")
778
+
779
+ # Display selected filter info
780
+ view_filter_info = gr.Markdown("**Selected:** 211 ร… - Active regions")
781
+
782
+ gr.Markdown("#### ๐Ÿ“… Date Range Selection")
783
+ with gr.Row():
784
+ view_from_date = gr.Dropdown(
785
+ choices=self.get_available_dates(),
786
+ label="From Date",
787
+ info="Select starting date"
788
+ )
789
+ view_to_date = gr.Dropdown(
790
+ choices=self.get_available_dates(),
791
+ label="To Date",
792
+ info="Select ending date"
793
+ )
794
+
795
+ with gr.Row():
796
+ refresh_dates_btn = gr.Button("๐Ÿ”„ Refresh Dates", size="sm")
797
+ load_images_btn = gr.Button("๐Ÿ“‚ Load Images", variant="primary")
798
+
799
+ view_status = gr.Textbox(label="Status", lines=3)
800
+
801
+ image_position = gr.Textbox(label="Position", value="0 / 0", interactive=False)
802
+
803
+ with gr.Column():
804
+ gr.Markdown("#### ๐ŸŽฎ Playback Controls")
805
+ with gr.Row():
806
+ first_btn = gr.Button("โฎ First", size="sm")
807
+ prev_btn = gr.Button("โช Prev", size="sm")
808
+ play_btn = gr.Button("โ–ถ Play", variant="primary")
809
+ next_btn = gr.Button("Next โฉ", size="sm")
810
+ last_btn = gr.Button("Last โญ", size="sm")
811
+
812
+ view_image = gr.Image(label="Solar Image", type="filepath", height=600)
813
+ image_info = gr.Textbox(label="Image Information", interactive=False)
814
+
815
+ gr.Markdown("#### โšก Speed Control")
816
+ with gr.Row():
817
+ speed_slider = gr.Slider(
818
+ minimum=0.5,
819
+ maximum=240.0,
820
+ value=120.0,
821
+ step=0.1,
822
+ label="Playback Speed (FPS)",
823
+ info="Frames per second during playback"
824
+ )
825
+ speed_display = gr.Textbox(
826
+ value="120.0 FPS",
827
+ label="Current Speed",
828
+ interactive=False,
829
+ scale=0
830
+ )
831
+
832
+ # Gallery selection event
833
+ view_filter_gallery.select(
834
+ fn=self.on_filter_gallery_select,
835
+ outputs=[view_filter, view_filter_info]
836
+ )
837
+
838
+ refresh_dates_btn.click(
839
+ fn=lambda res, filt: [gr.Dropdown(choices=self.get_available_dates(res, filt))] * 2,
840
+ inputs=[view_resolution, view_filter],
841
+ outputs=[view_from_date, view_to_date]
842
+ )
843
+
844
+ # Auto-refresh dates when resolution or filter changes
845
+ view_resolution.change(
846
+ fn=lambda res, filt: [gr.Dropdown(choices=self.get_available_dates(res, filt))] * 2,
847
+ inputs=[view_resolution, view_filter],
848
+ outputs=[view_from_date, view_to_date]
849
+ )
850
+
851
+ load_images_btn.click(
852
+ fn=self.load_images_for_date_range,
853
+ inputs=[view_from_date, view_to_date, view_resolution, view_filter],
854
+ outputs=[view_image, view_status, image_position]
855
+ )
856
+
857
+ # Navigation buttons
858
+ first_btn.click(
859
+ fn=lambda: self.navigate_image("first"),
860
+ outputs=[view_image, image_info, image_position, play_btn]
861
+ )
862
+ prev_btn.click(
863
+ fn=lambda: self.navigate_image("prev"),
864
+ outputs=[view_image, image_info, image_position, play_btn]
865
+ )
866
+ next_btn.click(
867
+ fn=lambda: self.navigate_image("next"),
868
+ outputs=[view_image, image_info, image_position, play_btn]
869
+ )
870
+ last_btn.click(
871
+ fn=lambda: self.navigate_image("last"),
872
+ outputs=[view_image, image_info, image_position, play_btn]
873
+ )
874
+
875
+ # Play/Pause button
876
+ play_btn.click(
877
+ fn=self.toggle_play,
878
+ outputs=[view_image, image_info, image_position, play_btn, speed_display]
879
+ )
880
+
881
+ # Speed control
882
+ speed_slider.change(
883
+ fn=self.update_play_speed,
884
+ inputs=[speed_slider],
885
+ outputs=[speed_display]
886
+ )
887
+
888
+ # Auto-update timer for playback (every 100ms)
889
+ play_timer = gr.Timer(0.1) # 100ms interval
890
+ play_timer.tick(
891
+ fn=self.update_playback,
892
+ outputs=[view_image, image_info, image_position, play_btn, speed_display]
893
+ )
894
+
895
+ # Create Video Tab
896
+ with gr.Tab("๐ŸŽฌ Create Videos"):
897
+ gr.Markdown("### Create MP4 Time-lapse Videos")
898
+
899
+ with gr.Row():
900
+ with gr.Column():
901
+ gr.Markdown("#### ๐Ÿ—“๏ธ Quick Date Selection")
902
+ with gr.Row():
903
+ video_today_btn = gr.Button("Today", size="sm")
904
+ video_last3_btn = gr.Button("Last 3 Days", size="sm")
905
+ video_lastweek_btn = gr.Button("Last Week", size="sm")
906
+
907
+ gr.Markdown("#### ๐Ÿ“… Video Date Range")
908
+ video_start_date = gr.Textbox(
909
+ label="Start Date (YYYY-MM-DD)",
910
+ value=datetime.now().strftime("%Y-%m-%d")
911
+ )
912
+ video_end_date = gr.Textbox(
913
+ label="End Date (YYYY-MM-DD)",
914
+ value=datetime.now().strftime("%Y-%m-%d")
915
+ )
916
+
917
+ gr.Markdown("#### ๐ŸŽฌ Video Settings")
918
+ video_fps = gr.Slider(
919
+ minimum=1,
920
+ maximum=120,
921
+ value=10,
922
+ step=1,
923
+ label="FPS (Frames Per Second)",
924
+ info="Higher FPS = smoother but faster playback"
925
+ )
926
+ video_resolution = gr.Dropdown(
927
+ choices=["1024", "2048", "4096"],
928
+ value="1024",
929
+ label="Resolution"
930
+ )
931
+
932
+ gr.Markdown("#### ๐ŸŒž Solar Filter Selection")
933
+ gr.Markdown("*Click on a thumbnail to select a solar filter*")
934
+
935
+ video_filter_gallery = gr.Gallery(
936
+ value=self.get_filter_gallery_data(),
937
+ label="Solar Filters",
938
+ columns=4,
939
+ rows=3,
940
+ height="auto",
941
+ object_fit="contain",
942
+ show_label=False,
943
+ selected_index=3 # Default to 0211
944
+ )
945
+
946
+ # Hidden state to store the selected filter key
947
+ video_filter = gr.State(value="0211")
948
+
949
+ # Display selected filter info
950
+ video_filter_info = gr.Markdown("**Selected:** 211 ร… - Active regions")
951
+
952
+ with gr.Row():
953
+ create_video_btn = gr.Button("๐ŸŽฌ Create Video for Date Range", variant="primary")
954
+ create_all_btn = gr.Button("๐ŸŽฌ Create Combined Video (All Available)")
955
+
956
+ with gr.Column():
957
+ video_output = gr.Textbox(label="Video Creation Status", lines=8)
958
+ video_player = gr.Video(label="Created Video", height=400)
959
+
960
+ # Video Playback Section
961
+ gr.Markdown("### ๐ŸŽฅ Play MP4 Videos")
962
+
963
+ with gr.Row():
964
+ with gr.Column():
965
+ gr.Markdown("#### ๐Ÿ“ Video Selection")
966
+ video_file_upload = gr.File(
967
+ label="Select MP4 File",
968
+ file_types=[".mp4"],
969
+ type="filepath"
970
+ )
971
+
972
+ # Or select from created videos
973
+ available_videos = gr.Dropdown(
974
+ choices=self.get_video_list(),
975
+ label="Or Select from Created Videos",
976
+ info="Choose from previously created videos"
977
+ )
978
+
979
+ refresh_videos_btn = gr.Button("๐Ÿ”„ Refresh Video List", size="sm")
980
+
981
+ gr.Markdown("#### ๐ŸŽฎ Video Controls")
982
+ with gr.Row():
983
+ video_play_btn = gr.Button("โ–ถ Play Video", variant="primary")
984
+ video_stop_btn = gr.Button("โน Stop")
985
+ video_fullscreen_btn = gr.Button("๐Ÿ”ณ Fullscreen")
986
+
987
+ video_info = gr.Textbox(label="Video Information", lines=3)
988
+
989
+ # Quick date button actions for video
990
+ video_today_btn.click(
991
+ fn=lambda: self.set_date_range(0),
992
+ outputs=[video_start_date, video_end_date]
993
+ )
994
+ video_last3_btn.click(
995
+ fn=lambda: self.set_date_range(2),
996
+ outputs=[video_start_date, video_end_date]
997
+ )
998
+ video_lastweek_btn.click(
999
+ fn=lambda: self.set_date_range(6),
1000
+ outputs=[video_start_date, video_end_date]
1001
+ )
1002
+
1003
+ # Gallery selection event for video tab
1004
+ video_filter_gallery.select(
1005
+ fn=self.on_filter_gallery_select,
1006
+ outputs=[video_filter, video_filter_info]
1007
+ )
1008
+
1009
+ create_video_btn.click(
1010
+ fn=self.create_video,
1011
+ inputs=[video_start_date, video_end_date, video_fps, video_resolution, video_filter],
1012
+ outputs=[video_player, video_output]
1013
+ )
1014
+
1015
+ # Settings Tab
1016
+ with gr.Tab("โš™๏ธ Settings"):
1017
+ gr.Markdown("### Application Settings & System Information")
1018
+
1019
+ with gr.Row():
1020
+ with gr.Column():
1021
+ gr.Markdown("#### ๐Ÿ”ง Download Settings")
1022
+ rate_limit = gr.Slider(
1023
+ minimum=0.5,
1024
+ maximum=5.0,
1025
+ value=1.0,
1026
+ step=0.1,
1027
+ label="Rate Limit Delay (seconds)",
1028
+ info="Delay between downloads to be respectful to NASA servers"
1029
+ )
1030
+
1031
+ gr.Markdown("#### ๐Ÿ” Custom Keyword Search")
1032
+ gr.Markdown("*Customize search keywords for each solar filter. Leave empty to use defaults.*")
1033
+
1034
+ # Create custom keyword inputs for each filter
1035
+ keyword_inputs = {}
1036
+ for filter_num, data in list(self.filter_data.items())[:6]: # First 6 filters
1037
+ with gr.Row():
1038
+ gr.Markdown(f"**{data['name']}** ({filter_num})")
1039
+ keyword_inputs[filter_num] = gr.Textbox(
1040
+ value=filter_num,
1041
+ placeholder=f"Default: {filter_num}",
1042
+ scale=2
1043
+ )
1044
+
1045
+ with gr.Row():
1046
+ reset_keywords_btn = gr.Button("๐Ÿ”„ Reset to Defaults", size="sm")
1047
+ apply_keywords_btn = gr.Button("โœ… Apply Keywords", variant="primary")
1048
+
1049
+ keyword_status = gr.Textbox(label="Keyword Status", lines=2)
1050
+
1051
+ with gr.Column():
1052
+ gr.Markdown("#### ๐Ÿ“ Data Management")
1053
+ with gr.Row():
1054
+ open_data_btn = gr.Button("๐Ÿ“ Open Data Folder")
1055
+ cleanup_btn = gr.Button("๐Ÿงน Clean Up Corrupted Files")
1056
+
1057
+ data_management_output = gr.Textbox(label="Data Management Status", lines=2)
1058
+
1059
+ gr.Markdown("#### ๐Ÿ–ฅ๏ธ System Information")
1060
+ system_info = gr.Markdown(self.get_system_info())
1061
+ refresh_info_btn = gr.Button("๐Ÿ”„ Refresh System Info")
1062
+
1063
+ # Keyword management
1064
+ reset_keywords_btn.click(
1065
+ fn=self.reset_custom_keywords,
1066
+ outputs=[keyword_status]
1067
+ )
1068
+
1069
+ # Data management
1070
+ open_data_btn.click(
1071
+ fn=self.open_data_folder,
1072
+ outputs=[data_management_output]
1073
+ )
1074
+
1075
+ cleanup_btn.click(
1076
+ fn=self.cleanup_corrupted_files,
1077
+ outputs=[data_management_output]
1078
+ )
1079
+
1080
+ refresh_info_btn.click(
1081
+ fn=self.get_system_info,
1082
+ outputs=[system_info]
1083
+ )
1084
+
1085
+ # Video controls
1086
+ refresh_videos_btn.click(
1087
+ fn=self.get_video_list,
1088
+ outputs=[available_videos]
1089
+ )
1090
+
1091
+ # About Tab
1092
+ with gr.Tab("โ„น๏ธ About"):
1093
+ gr.Markdown("""
1094
+ ## About NASA Solar Image Downloader
1095
+
1096
+ This comprehensive web application downloads and processes images from NASA's Solar Dynamics Observatory (SDO).
1097
+
1098
+ ### ๐ŸŒŸ Features
1099
+ - **Download Management**: Bulk download solar images for any date range
1100
+ - **Image Viewer**: Browse images with full playback controls (First, Previous, Next, Last)
1101
+ - **Video Creation**: Create time-lapse MP4 videos with customizable FPS
1102
+ - **Multiple Filters**: 12 different wavelengths and composite filters
1103
+ - **High Resolution**: Support for 1024, 2048, and 4096 pixel images
1104
+ - **Custom Keywords**: Advanced search customization
1105
+ - **Progress Tracking**: Real-time progress for all operations
1106
+
1107
+ ### ๐Ÿ”ฌ Solar Filters Explained
1108
+
1109
+ **Individual Wavelengths:**
1110
+ - **193 ร…**: Shows coronal loops and hot active regions
1111
+ - **304 ร…**: Reveals the chromosphere and filament channels
1112
+ - **171 ร…**: Displays quiet corona and coronal holes
1113
+ - **211 ร…**: Highlights active regions and hot plasma
1114
+ - **131 ร…**: Shows flaring regions and very hot plasma
1115
+ - **335 ร…**: Reveals active region cores
1116
+ - **94 ร…**: Shows extremely hot plasma and flare ribbons
1117
+ - **1600 ร…**: Displays transition region and upper photosphere
1118
+ - **1700 ร…**: Shows temperature minimum and photosphere
1119
+
1120
+ **Composite Filters:**
1121
+ - **094+335+193**: Multi-wavelength view of hot plasma structures
1122
+ - **304+211+171**: Comprehensive view from chromosphere to corona
1123
+ - **211+193+171**: Active regions with coronal context
1124
+
1125
+ ### ๐Ÿš€ Quick Start Guide
1126
+ 1. **Download Images**: Select date range, choose filter, click download
1127
+ 2. **View Images**: Load images and use playback controls to browse
1128
+ 3. **Create Videos**: Set date range and FPS, create time-lapse videos
1129
+ 4. **Customize**: Use Settings tab for advanced configuration
1130
+
1131
+ ### ๐Ÿ“Š Data Source
1132
+ All images are sourced from NASA's Solar Dynamics Observatory (SDO), which provides continuous observations of the Sun in multiple wavelengths.
1133
+
1134
+ **๐Ÿ† Created by Andy Kong**
1135
+
1136
+ ---
1137
+ *This application provides the same functionality as the desktop version but accessible from any web browser.*
1138
+ """)
1139
+
1140
+ return app
1141
+
1142
+ def launch(self, share=False, server_port=7860):
1143
+ """Launch the Gradio interface."""
1144
+ app = self.create_interface()
1145
+ app.launch(share=share, server_port=server_port, theme=gr.themes.Soft())
1146
+
1147
+
1148
+ def main():
1149
+ """Main application entry point."""
1150
+ try:
1151
+ print("๐Ÿš€ Starting NASA Solar Image Downloader (Gradio Web Interface)")
1152
+ print("=" * 60)
1153
+
1154
+ # Create and launch the Gradio app
1155
+ gradio_app = NASADownloaderGradio()
1156
+
1157
+ # Launch with share=True to create a public link
1158
+ # Set share=False for local-only access
1159
+ gradio_app.launch(share=False, server_port=None) # Auto-find available port
1160
+
1161
+ except Exception as e:
1162
+ print(f"โŒ Error starting application: {e}")
1163
+ import traceback
1164
+ traceback.print_exc()
1165
+
1166
+
1167
+ if __name__ == "__main__":
1168
+ main()