#!/usr/bin/env python3 """ NASA Solar Image Downloader - Complete GUI Application Combines downloading, viewing, video creation, and MP4 playback. """ import sys import os import threading import subprocess import shutil from pathlib import Path from datetime import datetime, timedelta # Add src to Python path sys.path.insert(0, str(Path(__file__).parent / "src")) try: import tkinter as tk from tkinter import ttk, messagebox, filedialog from PIL import Image, ImageTk import cv2 HAS_GUI = True except ImportError as e: print(f"โŒ GUI libraries not available: {e}") print("๐Ÿ’ก Install with: pip install pillow opencv-python") HAS_GUI = False sys.exit(1) from src.downloader.directory_scraper import DirectoryScraper from src.storage.storage_organizer import StorageOrganizer from src.downloader.image_fetcher import ImageFetcher, DownloadManager class NASADownloaderGUI: """Complete NASA Solar Image Downloader GUI.""" def __init__(self): """Initialize the GUI application.""" self.root = tk.Tk() self.root.title("๐ŸŒž NASA Solar Image Downloader") self.root.geometry("1200x800") # Load and set background image self.setup_background_image() # Configure ttk styles for modern appearance with transparency style = ttk.Style() style.theme_use('clam') # Configure custom styles with semi-transparent backgrounds style.configure('Title.TLabel', font=('Arial', 16, 'bold'), foreground='#ecf0f1', background='#2c3e50') style.configure('Heading.TLabel', font=('Arial', 12, 'bold'), foreground='#34495e') style.configure('TNotebook', background='rgba(236, 240, 241, 0.95)', borderwidth=0) style.configure('TNotebook.Tab', padding=[20, 10], font=('Arial', 10, 'bold')) style.configure('TButton', font=('Arial', 9, 'bold'), padding=(10, 5)) # Maximize the window self.root.state('zoomed') # Windows try: self.root.attributes('-zoomed', True) # Linux except: pass # Image filter settings (initialize before components) self.resolution_var = tk.StringVar(value="1024") self.solar_filter_var = tk.StringVar(value="0211") # Initialize filter data and buttons dictionary self.filter_data = { "0193": {"name": "193 ร…", "desc": "Coronal loops", "color": "#ff6b6b"}, "0304": {"name": "304 ร…", "desc": "Chromosphere", "color": "#4ecdc4"}, "0171": {"name": "171 ร…", "desc": "Quiet corona", "color": "#45b7d1"}, "0211": {"name": "211 ร…", "desc": "Active regions", "color": "#f9ca24"}, "0131": {"name": "131 ร…", "desc": "Flaring regions", "color": "#f0932b"}, "0335": {"name": "335 ร…", "desc": "Active cores", "color": "#eb4d4b"}, "0094": {"name": "94 ร…", "desc": "Hot plasma", "color": "#6c5ce7"}, "1600": {"name": "1600 ร…", "desc": "Transition region", "color": "#a29bfe"}, "1700": {"name": "1700 ร…", "desc": "Temperature min", "color": "#fd79a8"}, "094335193": {"name": "094+335+193", "desc": "Composite: Hot plasma + Active cores + Coronal loops", "color": "#8e44ad"}, "304211171": {"name": "304+211+171", "desc": "Composite: Chromosphere + Active regions + Quiet corona", "color": "#e67e22"}, "211193171": {"name": "211+193+171", "desc": "Composite: Active regions + Coronal loops + Quiet corona", "color": "#27ae60"} } self.filter_buttons = {} self._filter_initialized = False # Initialize components self.storage = StorageOrganizer("data", resolution=self.resolution_var.get(), solar_filter=self.solar_filter_var.get()) self.scraper = DirectoryScraper(rate_limit_delay=1.0, resolution=self.resolution_var.get(), solar_filter=self.solar_filter_var.get()) self.fetcher = ImageFetcher(rate_limit_delay=1.0) self.download_manager = DownloadManager(self.fetcher, self.storage) # GUI state self.current_images = [] self.current_image_index = 0 self.is_playing = False self.play_thread = None self.download_thread = None # Video playback state self.video_cap = None self.video_playing = False self.video_thread = None self.selected_video_path = None self.fullscreen_mode = False self.fullscreen_window = None self.setup_ui() self.refresh_available_dates() def setup_background_image(self): """Set up the background image for the GUI.""" try: # Load the background image background_path = Path("background_solar.jpg") if background_path.exists(): # Load and resize the image to fit the screen pil_image = Image.open(background_path) # Get screen dimensions screen_width = self.root.winfo_screenwidth() screen_height = self.root.winfo_screenheight() # Resize image to cover the screen while maintaining aspect ratio pil_image = pil_image.resize((screen_width, screen_height), Image.Resampling.LANCZOS) # Apply a stronger semi-transparent overlay to make text more readable overlay = Image.new('RGBA', (screen_width, screen_height), (0, 0, 0, 150)) # Darker overlay pil_image = pil_image.convert('RGBA') pil_image = Image.alpha_composite(pil_image, overlay) # Convert to PhotoImage self.background_image = ImageTk.PhotoImage(pil_image) # Create a label to hold the background image self.background_label = tk.Label(self.root, image=self.background_image) self.background_label.place(x=0, y=0, relwidth=1, relheight=1) # Make sure the background stays behind other widgets self.background_label.lower() print("โœ… Background image loaded successfully") else: print("โš ๏ธ Background image not found, using default styling") self.root.configure(bg="#2c3e50") except Exception as e: print(f"โŒ Error loading background image: {e}") self.root.configure(bg="#2c3e50") def setup_ui(self): """Set up the user interface.""" # Create notebook for tabs with modern styling and transparency self.notebook = ttk.Notebook(self.root) self.notebook.pack(fill=tk.BOTH, expand=True, padx=15, pady=15) # Configure notebook styling with transparency style = ttk.Style() # Create all tab frames first to ensure consistent sizing self.download_frame = tk.Frame(self.notebook, bg='#1a1a1a', highlightthickness=0) self.viewer_frame = tk.Frame(self.notebook, bg='#1a1a1a', highlightthickness=0) self.video_frame = tk.Frame(self.notebook, bg='#1a1a1a', highlightthickness=0) self.settings_frame = tk.Frame(self.notebook, bg='#1a1a1a', highlightthickness=0) # Configure frames with semi-transparent dark background for frame in [self.download_frame, self.viewer_frame, self.video_frame, self.settings_frame]: frame.configure(bg='#1a1a1a') # Dark semi-transparent background # Add tabs to notebook self.notebook.add(self.download_frame, text="๐Ÿ“ฅ Download Images") self.notebook.add(self.viewer_frame, text="๐Ÿ‘๏ธ View Images") self.notebook.add(self.video_frame, text="๐ŸŽฌ Videos") self.notebook.add(self.settings_frame, text="โš™๏ธ Settings") # Configure all frames to have consistent sizing for frame in [self.download_frame, self.viewer_frame, self.video_frame, self.settings_frame]: frame.grid_rowconfigure(0, weight=1) frame.grid_columnconfigure(0, weight=1) # Create tab content self.create_download_tab() self.create_viewer_tab() self.create_video_tab() self.create_settings_tab() def create_download_tab(self): """Create the download tab.""" # Use the pre-created frame download_frame = self.download_frame # Create a full-width container that ignores frame padding title_container = tk.Frame(download_frame, height=150) title_container.pack(fill=tk.X, padx=0, pady=(0, 20)) title_container.pack_propagate(False) # Title with modern styling and background image title_frame = tk.Frame(title_container, height=150) title_frame.place(x=0, y=0, relwidth=1.0, relheight=1.0) # Try to load a background image try: # Use the specified background image bg_image_file = Path("src/ui_img/background.png") if bg_image_file.exists(): # Load the background image pil_bg_image = Image.open(bg_image_file) # Get the actual available width (full window width minus notebook padding) # Use a callback to update the image when the window is resized def update_banner_image(event=None): try: # Get the actual width of the title container actual_width = title_container.winfo_width() if actual_width <= 1: # Not yet rendered, use default actual_width = 1200 - 30 # Account for notebook padding # Set banner height banner_height = 150 # Resize to fill the full width resized_image = pil_bg_image.resize((actual_width, banner_height), Image.Resampling.LANCZOS) # Apply a dark overlay to make text readable overlay = Image.new('RGBA', resized_image.size, (0, 0, 0, 150)) # Semi-transparent black resized_image = resized_image.convert('RGBA') resized_image = Image.alpha_composite(resized_image, overlay) bg_photo = ImageTk.PhotoImage(resized_image) # Update or create background label if hasattr(title_frame, 'bg_label'): title_frame.bg_label.config(image=bg_photo) title_frame.bg_label.image = bg_photo # Keep reference else: title_frame.bg_label = tk.Label(title_frame, image=bg_photo) title_frame.bg_label.image = bg_photo # Keep reference title_frame.bg_label.place(x=0, y=0, relwidth=1, relheight=1) except Exception as e: print(f"Error updating banner image: {e}") # Initial image setup update_banner_image() # Bind to configure event to update when window is resized title_container.bind('', update_banner_image) # Create title label with dark semi-transparent background title_label = tk.Label(title_frame, text="๐ŸŒž NASA Solar Image Downloader", font=("Arial", 20, "bold"), fg="white", bg="#1a1a1a") title_label.place(relx=0.5, rely=0.4, anchor=tk.CENTER) # Add subtitle subtitle_label = tk.Label(title_frame, text="Explore the Sun's Dynamic Activity", font=("Arial", 12, "italic"), fg="#f39c12", bg="#1a1a1a") subtitle_label.place(relx=0.5, rely=0.65, anchor=tk.CENTER) else: # Fallback to original design if image not found title_frame.configure(bg="#3498db") title_label = tk.Label(title_frame, text="๐ŸŒž NASA Solar Image Downloader", font=("Arial", 18, "bold"), fg="white", bg="#3498db") title_label.pack(expand=True) except Exception as e: print(f"Error loading banner background: {e}") # Fallback to original design title_frame.configure(bg="#3498db") title_label = tk.Label(title_frame, text="๐ŸŒž NASA Solar Image Downloader", font=("Arial", 18, "bold"), fg="white", bg="#3498db") title_label.pack(expand=True) # Image filter settings for download tab (moved before date selection) download_filter_frame = ttk.LabelFrame(download_frame, text="Image Filters", padding=15) download_filter_frame.pack(fill=tk.X, padx=20, pady=10) # Resolution selection resolution_frame = ttk.Frame(download_filter_frame) resolution_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Label(resolution_frame, text="Resolution:", font=("Arial", 10, "bold")).pack(side=tk.LEFT, padx=(0, 10)) resolution_combo = ttk.Combobox(resolution_frame, textvariable=self.resolution_var, values=["1024", "2048", "4096"], state="readonly", width=10) resolution_combo.pack(side=tk.LEFT, padx=(0, 20)) resolution_combo.bind('<>', self.on_filter_change) # Solar filter selection with visual preview filter_label = ttk.Label(download_filter_frame, text="Solar Filter:", font=("Arial", 10, "bold")) filter_label.pack(anchor=tk.W, pady=(0, 5)) # Create the visual filter selection UI self.create_filter_selection_ui(download_filter_frame) # Date selection frame with modern styling (moved after image filters) date_frame = ttk.LabelFrame(download_frame, text="Select Date Range", padding=15) date_frame.pack(fill=tk.X, padx=20, pady=10) # Quick options with modern buttons quick_frame = ttk.Frame(date_frame) quick_frame.pack(fill=tk.X, pady=(0, 15)) ttk.Button(quick_frame, text="Today", command=lambda: self.set_date_range(0)).pack(side=tk.LEFT, padx=(0, 10)) ttk.Button(quick_frame, text="Last 3 Days", command=lambda: self.set_date_range(2)).pack(side=tk.LEFT, padx=(0, 10)) ttk.Button(quick_frame, text="Last Week", command=lambda: self.set_date_range(6)).pack(side=tk.LEFT, padx=(0, 10)) # Custom date selection with better styling custom_frame = ttk.Frame(date_frame) custom_frame.pack(fill=tk.X) ttk.Label(custom_frame, text="From:", font=("Arial", 10, "bold")).pack(side=tk.LEFT, padx=(0, 8)) self.start_date_var = tk.StringVar(value=datetime.now().strftime("%Y-%m-%d")) self.start_date_entry = ttk.Entry(custom_frame, textvariable=self.start_date_var, width=12, font=("Arial", 10)) self.start_date_entry.pack(side=tk.LEFT, padx=(0, 20)) ttk.Label(custom_frame, text="To:", font=("Arial", 10, "bold")).pack(side=tk.LEFT, padx=(0, 8)) self.end_date_var = tk.StringVar(value=datetime.now().strftime("%Y-%m-%d")) self.end_date_entry = ttk.Entry(custom_frame, textvariable=self.end_date_var, width=12, font=("Arial", 10)) self.end_date_entry.pack(side=tk.LEFT, padx=(0, 20)) # Download button with modern styling self.download_btn = ttk.Button(date_frame, text="๐Ÿ” Find & Download Images", command=self.start_download) self.download_btn.pack(pady=15) # Progress frame with modern styling progress_frame = ttk.LabelFrame(download_frame, text="Download Progress", padding=15) progress_frame.pack(fill=tk.X, padx=20, pady=10) self.progress_var = tk.DoubleVar() self.progress_bar = ttk.Progressbar(progress_frame, variable=self.progress_var, maximum=100, length=500) self.progress_bar.pack(pady=(0, 10)) self.status_label = ttk.Label(progress_frame, text="Ready to download", font=("Arial", 10)) self.status_label.pack() # Log frame with modern styling log_frame = ttk.LabelFrame(download_frame, text="Download Log", padding=15) log_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=10) # Create text widget with scrollbar log_text_frame = ttk.Frame(log_frame) log_text_frame.pack(fill=tk.BOTH, expand=True) self.log_text = tk.Text(log_text_frame, height=10, wrap=tk.WORD) log_scrollbar = ttk.Scrollbar(log_text_frame, orient=tk.VERTICAL, command=self.log_text.yview) self.log_text.configure(yscrollcommand=log_scrollbar.set) self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) log_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) def create_viewer_tab(self): """Create the image viewer tab.""" # Use the pre-created frame viewer_frame = self.viewer_frame # Create a scrollable container for all viewer content # Create canvas and scrollbar for scrolling viewer_canvas = tk.Canvas(viewer_frame, bg="#f0f0f0") viewer_v_scrollbar = ttk.Scrollbar(viewer_frame, orient="vertical", command=viewer_canvas.yview) viewer_scrollable_frame = ttk.Frame(viewer_canvas) # Configure scrolling def configure_viewer_scroll_region(event=None): viewer_canvas.configure(scrollregion=viewer_canvas.bbox("all")) viewer_scrollable_frame.bind("", configure_viewer_scroll_region) viewer_canvas.create_window((0, 0), window=viewer_scrollable_frame, anchor="nw") viewer_canvas.configure(yscrollcommand=viewer_v_scrollbar.set) # Make the scrollable frame expand to full canvas width def configure_viewer_canvas_width(event): # Get the canvas width and set the scrollable frame to match canvas_width = event.width if viewer_canvas.find_all(): viewer_canvas.itemconfig(viewer_canvas.find_all()[0], width=canvas_width) viewer_canvas.bind('', configure_viewer_canvas_width) # Pack canvas and scrollbar to occupy full width viewer_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) viewer_v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # Enable mouse wheel scrolling def _on_viewer_mousewheel(event): viewer_canvas.yview_scroll(int(-1*(event.delta/120)), "units") # Bind mouse wheel events for different platforms viewer_canvas.bind("", _on_viewer_mousewheel) # Windows viewer_canvas.bind("", lambda e: viewer_canvas.yview_scroll(-1, "units")) # Linux scroll up viewer_canvas.bind("", lambda e: viewer_canvas.yview_scroll(1, "units")) # Linux scroll down # Make canvas focusable and bind focus events viewer_canvas.focus_set() viewer_canvas.bind("", lambda e: viewer_canvas.focus_set()) # Also bind to the scrollable frame to catch events viewer_scrollable_frame.bind("", _on_viewer_mousewheel) viewer_scrollable_frame.bind("", lambda e: viewer_canvas.yview_scroll(-1, "units")) viewer_scrollable_frame.bind("", lambda e: viewer_canvas.yview_scroll(1, "units")) # Enable middle button scrolling (same as scroll bar up/down) def _on_viewer_middle_button_click(event): viewer_canvas.yview_scroll(-3, "units") # Scroll up like scroll bar viewer_canvas.bind("", _on_viewer_middle_button_click) # Now use viewer_scrollable_frame instead of viewer_frame for all content # Date selection date_select_frame = ttk.LabelFrame(viewer_scrollable_frame, text="Select Date Range", padding=10) date_select_frame.pack(fill=tk.X, padx=10, pady=5) # From date selection from_frame = ttk.Frame(date_select_frame) from_frame.pack(side=tk.LEFT, padx=(0, 15)) ttk.Label(from_frame, text="From:").pack(side=tk.LEFT, padx=(0, 5)) self.viewer_from_date_var = tk.StringVar() self.viewer_from_date_combo = ttk.Combobox(from_frame, textvariable=self.viewer_from_date_var, state="readonly", width=20) self.viewer_from_date_combo.pack(side=tk.LEFT) # To date selection to_frame = ttk.Frame(date_select_frame) to_frame.pack(side=tk.LEFT, padx=(0, 15)) ttk.Label(to_frame, text="To:").pack(side=tk.LEFT, padx=(0, 5)) self.viewer_to_date_var = tk.StringVar() self.viewer_to_date_combo = ttk.Combobox(to_frame, textvariable=self.viewer_to_date_var, state="readonly", width=20) self.viewer_to_date_combo.pack(side=tk.LEFT) # Control buttons button_frame = ttk.Frame(date_select_frame) button_frame.pack(side=tk.LEFT) ttk.Button(button_frame, text="Load Images", command=self.load_images_for_viewer).pack(side=tk.LEFT, padx=(0, 5)) ttk.Button(button_frame, text="Refresh Dates", command=self.refresh_available_dates).pack(side=tk.LEFT) # Image filter settings for viewer tab viewer_filter_frame = ttk.LabelFrame(viewer_scrollable_frame, text="Image Filters", padding=15) viewer_filter_frame.pack(fill=tk.X, padx=10, pady=5) # Resolution selection resolution_frame = ttk.Frame(viewer_filter_frame) resolution_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Label(resolution_frame, text="Resolution:", font=("Arial", 10, "bold")).pack(side=tk.LEFT, padx=(0, 10)) resolution_combo = ttk.Combobox(resolution_frame, textvariable=self.resolution_var, values=["1024", "2048", "4096"], state="readonly", width=10) resolution_combo.pack(side=tk.LEFT, padx=(0, 20)) resolution_combo.bind('<>', self.on_filter_change) # Solar filter selection with visual preview filter_label = ttk.Label(viewer_filter_frame, text="Solar Filter:", font=("Arial", 10, "bold")) filter_label.pack(anchor=tk.W, pady=(0, 5)) # Create the visual filter selection UI self.create_filter_selection_ui(viewer_filter_frame) # Image display image_display_frame = ttk.LabelFrame(viewer_scrollable_frame, text="Solar Image", padding=10) image_display_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) self.image_display_label = tk.Label(image_display_frame, text="No image loaded", background="black", foreground="white", justify=tk.CENTER, compound=tk.CENTER) self.image_display_label.pack(fill=tk.BOTH, expand=True) # Controls controls_frame = ttk.LabelFrame(viewer_scrollable_frame, text="Playback Controls", padding=10) controls_frame.pack(fill=tk.X, padx=10, pady=5) # Playback buttons btn_frame = ttk.Frame(controls_frame) btn_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Button(btn_frame, text="โฎ First", command=self.first_image).pack(side=tk.LEFT, padx=(0, 5)) ttk.Button(btn_frame, text="โช Prev", command=self.prev_image).pack(side=tk.LEFT, padx=(0, 5)) self.play_btn = ttk.Button(btn_frame, text="โ–ถ Play", command=self.toggle_play) self.play_btn.pack(side=tk.LEFT, padx=(0, 5)) ttk.Button(btn_frame, text="Next โฉ", command=self.next_image).pack(side=tk.LEFT, padx=(0, 5)) ttk.Button(btn_frame, text="Last โญ", command=self.last_image).pack(side=tk.LEFT) # Speed and progress control_bottom_frame = ttk.Frame(controls_frame) control_bottom_frame.pack(fill=tk.X) ttk.Label(control_bottom_frame, text="Speed:").pack(side=tk.LEFT, padx=(0, 5)) self.speed_var = tk.DoubleVar(value=2.0) speed_scale = ttk.Scale(control_bottom_frame, from_=0.5, to=120.0, variable=self.speed_var, orient=tk.HORIZONTAL, length=200, command=self.update_speed_display) speed_scale.pack(side=tk.LEFT, padx=(0, 10)) self.speed_display = ttk.Label(control_bottom_frame, text="2.0 FPS") self.speed_display.pack(side=tk.LEFT, padx=(0, 20)) # Image info self.image_info_label = ttk.Label(control_bottom_frame, text="No images loaded") self.image_info_label.pack(side=tk.RIGHT) # Progress bar for images self.image_progress_var = tk.DoubleVar() self.image_progress_bar = ttk.Progressbar(controls_frame, variable=self.image_progress_var, maximum=100) self.image_progress_bar.pack(fill=tk.X, pady=(10, 0)) def create_video_tab(self): """Create the video creation and playback tab.""" # Use the pre-created frame video_frame = self.video_frame # Create a scrollable container for all video content # Create canvas and scrollbar for scrolling video_canvas = tk.Canvas(video_frame, bg="#f0f0f0") video_v_scrollbar = ttk.Scrollbar(video_frame, orient="vertical", command=video_canvas.yview) video_scrollable_frame = ttk.Frame(video_canvas) # Configure scrolling def configure_video_scroll_region(event=None): video_canvas.configure(scrollregion=video_canvas.bbox("all")) video_scrollable_frame.bind("", configure_video_scroll_region) video_canvas.create_window((0, 0), window=video_scrollable_frame, anchor="nw") video_canvas.configure(yscrollcommand=video_v_scrollbar.set) # Make the scrollable frame expand to full canvas width def configure_video_canvas_width(event): # Get the canvas width and set the scrollable frame to match canvas_width = event.width if video_canvas.find_all(): video_canvas.itemconfig(video_canvas.find_all()[0], width=canvas_width) video_canvas.bind('', configure_video_canvas_width) # Pack canvas and scrollbar to occupy full width video_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) video_v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # Enable mouse wheel scrolling def _on_video_mousewheel(event): video_canvas.yview_scroll(int(-1*(event.delta/120)), "units") # Bind mouse wheel events for different platforms video_canvas.bind("", _on_video_mousewheel) # Windows video_canvas.bind("", lambda e: video_canvas.yview_scroll(-1, "units")) # Linux scroll up video_canvas.bind("", lambda e: video_canvas.yview_scroll(1, "units")) # Linux scroll down # Make canvas focusable and bind focus events video_canvas.focus_set() video_canvas.bind("", lambda e: video_canvas.focus_set()) # Also bind to the scrollable frame to catch events video_scrollable_frame.bind("", _on_video_mousewheel) video_scrollable_frame.bind("", lambda e: video_canvas.yview_scroll(-1, "units")) video_scrollable_frame.bind("", lambda e: video_canvas.yview_scroll(1, "units")) # Enable middle button scrolling (same as scroll bar up/down) def _on_video_middle_button_click(event): video_canvas.yview_scroll(-3, "units") # Scroll up like scroll bar video_canvas.bind("", _on_video_middle_button_click) # Now use video_scrollable_frame instead of video_frame for all content # Video creation section creation_frame = ttk.LabelFrame(video_scrollable_frame, text="Create MP4 Videos", padding=10) creation_frame.pack(fill=tk.X, padx=10, pady=5) # Date selection for video video_date_frame = ttk.LabelFrame(creation_frame, text="Select Date Range", padding=10) video_date_frame.pack(fill=tk.X, pady=(0, 10)) # Quick options with modern buttons video_quick_frame = ttk.Frame(video_date_frame) video_quick_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Button(video_quick_frame, text="Today", command=lambda: self.set_video_date_range(0)).pack(side=tk.LEFT, padx=(0, 10)) ttk.Button(video_quick_frame, text="Last 3 Days", command=lambda: self.set_video_date_range(2)).pack(side=tk.LEFT, padx=(0, 10)) ttk.Button(video_quick_frame, text="Last Week", command=lambda: self.set_video_date_range(6)).pack(side=tk.LEFT, padx=(0, 10)) # Custom date selection video_custom_frame = ttk.Frame(video_date_frame) video_custom_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Label(video_custom_frame, text="From:", font=("Arial", 10, "bold")).pack(side=tk.LEFT, padx=(0, 8)) self.video_start_date_var = tk.StringVar(value=datetime.now().strftime("%Y-%m-%d")) self.video_start_date_entry = ttk.Entry(video_custom_frame, textvariable=self.video_start_date_var, width=12, font=("Arial", 10)) self.video_start_date_entry.pack(side=tk.LEFT, padx=(0, 20)) ttk.Label(video_custom_frame, text="To:", font=("Arial", 10, "bold")).pack(side=tk.LEFT, padx=(0, 8)) self.video_end_date_var = tk.StringVar(value=datetime.now().strftime("%Y-%m-%d")) self.video_end_date_entry = ttk.Entry(video_custom_frame, textvariable=self.video_end_date_var, width=12, font=("Arial", 10)) self.video_end_date_entry.pack(side=tk.LEFT, padx=(0, 20)) # FPS setting ttk.Label(video_custom_frame, text="FPS:", font=("Arial", 10, "bold")).pack(side=tk.LEFT, padx=(10, 5)) self.video_fps_var = tk.IntVar(value=10) fps_spinbox = ttk.Spinbox(video_custom_frame, from_=1, to=120, width=5, textvariable=self.video_fps_var) fps_spinbox.pack(side=tk.LEFT, padx=(0, 10)) # Video creation buttons video_btn_frame = ttk.Frame(creation_frame) video_btn_frame.pack(fill=tk.X) ttk.Button(video_btn_frame, text="๐ŸŽฌ Create Video for Date Range", command=self.create_date_range_video).pack(side=tk.LEFT, padx=(0, 10)) ttk.Button(video_btn_frame, text="๐ŸŽฌ Create Combined Video (All Available)", command=self.create_all_videos).pack(side=tk.LEFT) # Video creation progress video_progress_frame = ttk.LabelFrame(creation_frame, text="Video Creation Progress", padding=10) video_progress_frame.pack(fill=tk.X, pady=(10, 0)) self.video_progress_var = tk.DoubleVar() self.video_progress_bar = ttk.Progressbar(video_progress_frame, variable=self.video_progress_var, maximum=100, length=500) self.video_progress_bar.pack(pady=(0, 10)) self.video_status_label = ttk.Label(video_progress_frame, text="Ready to create video", font=("Arial", 10)) self.video_status_label.pack() # Image filter settings for video tab video_filter_frame = ttk.LabelFrame(video_scrollable_frame, text="Image Filters", padding=15) video_filter_frame.pack(fill=tk.X, padx=10, pady=10) # Resolution selection video_resolution_frame = ttk.Frame(video_filter_frame) video_resolution_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Label(video_resolution_frame, text="Resolution:", font=("Arial", 10, "bold")).pack(side=tk.LEFT, padx=(0, 10)) resolution_combo = ttk.Combobox(video_resolution_frame, textvariable=self.resolution_var, values=["1024", "2048", "4096"], state="readonly", width=10) resolution_combo.pack(side=tk.LEFT, padx=(0, 20)) resolution_combo.bind('<>', self.on_filter_change) # Solar filter selection with visual preview filter_label = ttk.Label(video_filter_frame, text="Solar Filter:", font=("Arial", 10, "bold")) filter_label.pack(anchor=tk.W, pady=(0, 5)) # Create the visual filter selection UI self.create_filter_selection_ui(video_filter_frame) # Video playback section playback_frame = ttk.LabelFrame(video_scrollable_frame, text="Play MP4 Videos", padding=10) playback_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) # Video file selection video_select_frame = ttk.Frame(playback_frame) video_select_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Button(video_select_frame, text="Select MP4 File", command=self.select_video_file).pack(side=tk.LEFT, padx=(0, 10)) self.selected_video_label = ttk.Label(video_select_frame, text="No video selected") self.selected_video_label.pack(side=tk.LEFT) # Video display area - Fixed size 1024x1024 pixels self.video_display_frame = ttk.Frame(playback_frame, relief=tk.SUNKEN, borderwidth=2) self.video_display_frame.pack(pady=(0, 10)) self.video_display_frame.pack_propagate(False) # Prevent frame from shrinking self.video_display_frame.configure(width=1024, height=1024) # Fixed size self.video_display_label = tk.Label(self.video_display_frame, text="Select a video to play", background="black", foreground="white", justify=tk.CENTER, compound=tk.CENTER) self.video_display_label.pack(fill=tk.BOTH, expand=True) # Video controls video_controls_frame = ttk.Frame(playback_frame) video_controls_frame.pack(fill=tk.X) self.video_play_btn = ttk.Button(video_controls_frame, text="โ–ถ Play Video", command=self.play_video, state=tk.DISABLED) self.video_play_btn.pack(side=tk.LEFT, padx=(0, 5)) ttk.Button(video_controls_frame, text="โน Stop", command=self.stop_video).pack(side=tk.LEFT, padx=(0, 5)) self.fullscreen_btn = ttk.Button(video_controls_frame, text="๐Ÿ”ณ Fullscreen", command=self.toggle_fullscreen, state=tk.DISABLED) self.fullscreen_btn.pack(side=tk.LEFT, padx=(0, 5)) ttk.Button(video_controls_frame, text="๐Ÿ“ Open Video Folder", command=self.open_video_folder).pack(side=tk.RIGHT) def create_settings_tab(self): """Create the settings tab.""" # Use the pre-created frame settings_frame = self.settings_frame # Create a full-width container that ignores notebook padding full_width_container = tk.Frame(settings_frame, bg="#f0f0f0") full_width_container.place(x=0, y=0, relwidth=1.0, relheight=1.0) # Create a scrollable container for all settings content # Create canvas and scrollbar for scrolling settings_canvas = tk.Canvas(full_width_container, bg="#f0f0f0") settings_v_scrollbar = ttk.Scrollbar(full_width_container, orient="vertical", command=settings_canvas.yview) settings_scrollable_frame = ttk.Frame(settings_canvas) # Configure scrolling def configure_scroll_region(event=None): settings_canvas.configure(scrollregion=settings_canvas.bbox("all")) settings_scrollable_frame.bind("", configure_scroll_region) settings_canvas.create_window((0, 0), window=settings_scrollable_frame, anchor="nw") settings_canvas.configure(yscrollcommand=settings_v_scrollbar.set) # Make the scrollable frame expand to full canvas width def configure_canvas_width(event): # Get the canvas width and set the scrollable frame to match canvas_width = event.width if settings_canvas.find_all(): settings_canvas.itemconfig(settings_canvas.find_all()[0], width=canvas_width) settings_canvas.bind('', configure_canvas_width) # Pack canvas and scrollbar to occupy full width settings_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) settings_v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # Enable mouse wheel scrolling def _on_mousewheel(event): settings_canvas.yview_scroll(int(-1*(event.delta/120)), "units") # Bind mouse wheel events for different platforms settings_canvas.bind("", _on_mousewheel) # Windows settings_canvas.bind("", lambda e: settings_canvas.yview_scroll(-1, "units")) # Linux scroll up settings_canvas.bind("", lambda e: settings_canvas.yview_scroll(1, "units")) # Linux scroll down # Make canvas focusable and bind focus events settings_canvas.focus_set() settings_canvas.bind("", lambda e: settings_canvas.focus_set()) # Also bind to the scrollable frame to catch events settings_scrollable_frame.bind("", _on_mousewheel) settings_scrollable_frame.bind("", lambda e: settings_canvas.yview_scroll(-1, "units")) settings_scrollable_frame.bind("", lambda e: settings_canvas.yview_scroll(1, "units")) # Enable middle button scrolling (same as scroll bar up/down) def _on_settings_middle_button_click(event): settings_canvas.yview_scroll(-3, "units") # Scroll up like scroll bar settings_canvas.bind("", _on_settings_middle_button_click) # Now use settings_scrollable_frame instead of settings_frame for all content # Download settings download_settings_frame = ttk.LabelFrame(settings_scrollable_frame, text="Download Settings", padding=10) download_settings_frame.pack(fill=tk.X, pady=5) ttk.Label(download_settings_frame, text="Rate Limit Delay (seconds):").pack(anchor=tk.W) self.rate_limit_var = tk.DoubleVar(value=1.0) rate_limit_scale = ttk.Scale(download_settings_frame, from_=0.5, to=5.0, variable=self.rate_limit_var, orient=tk.HORIZONTAL) rate_limit_scale.pack(fill=tk.X, pady=(0, 10)) # Image filter settings filter_settings_frame = ttk.LabelFrame(download_settings_frame, text="Image Filters", padding=10) filter_settings_frame.pack(fill=tk.X, pady=(10, 0)) # Resolution selection resolution_frame = ttk.Frame(filter_settings_frame) resolution_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Label(resolution_frame, text="Resolution:", font=("Arial", 10, "bold")).pack(side=tk.LEFT, padx=(0, 10)) resolution_combo = ttk.Combobox(resolution_frame, textvariable=self.resolution_var, values=["1024", "2048", "4096"], state="readonly", width=10) resolution_combo.pack(side=tk.LEFT, padx=(0, 20)) resolution_combo.bind('<>', self.on_filter_change) # Solar filter selection with visual preview filter_label = ttk.Label(filter_settings_frame, text="Solar Filter:", font=("Arial", 10, "bold")) filter_label.pack(anchor=tk.W, pady=(0, 5)) # Create the visual filter selection UI self.create_filter_selection_ui(filter_settings_frame) # Custom keyword search settings keyword_settings_frame = ttk.LabelFrame(settings_scrollable_frame, text="Custom Keyword Search", padding=10) keyword_settings_frame.pack(fill=tk.X, pady=10) # Description desc_label = ttk.Label(keyword_settings_frame, text="Customize search keywords for each solar filter. Leave empty to use default filter numbers.", font=("Arial", 9), foreground="gray") desc_label.pack(anchor=tk.W, pady=(0, 10)) # Initialize custom keywords dictionary if not exists if not hasattr(self, 'custom_keywords'): self.custom_keywords = {} for filter_num in self.filter_data.keys(): self.custom_keywords[filter_num] = tk.StringVar(value=filter_num) # Default to filter number # Create keyword input fields for each filter keyword_grid_frame = ttk.Frame(keyword_settings_frame) keyword_grid_frame.pack(fill=tk.X) # Create a scrollable frame for keyword inputs keyword_canvas = tk.Canvas(keyword_grid_frame, height=200, bg="#f8f9fa") keyword_v_scrollbar = ttk.Scrollbar(keyword_grid_frame, orient="vertical", command=keyword_canvas.yview) keyword_scroll_frame = ttk.Frame(keyword_canvas) keyword_scroll_frame.bind( "", lambda e: keyword_canvas.configure(scrollregion=keyword_canvas.bbox("all")) ) keyword_canvas.create_window((0, 0), window=keyword_scroll_frame, anchor="nw") keyword_canvas.configure(yscrollcommand=keyword_v_scrollbar.set) keyword_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) keyword_v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # Create input fields for each filter (4 columns for better width utilization) for i, (filter_num, data) in enumerate(self.filter_data.items()): row = i // 4 # 4 columns instead of 3 col = i % 4 filter_frame = ttk.Frame(keyword_scroll_frame) filter_frame.grid(row=row, column=col, padx=5, pady=5, sticky="ew") # Filter label with color filter_label = tk.Label(filter_frame, text=f"{data['name']} ({filter_num})", font=("Arial", 9, "bold"), fg="white", bg=data["color"], padx=5, pady=2) filter_label.pack(anchor=tk.W) # Keyword input keyword_entry = ttk.Entry(filter_frame, textvariable=self.custom_keywords[filter_num], width=12, font=("Arial", 9)) # Slightly smaller width for 4 columns keyword_entry.pack(fill=tk.X, pady=(2, 0)) # Bind change event self.custom_keywords[filter_num].trace('w', self.on_keyword_change) # Configure grid weights for proper spacing (4 columns now) for col in range(4): keyword_scroll_frame.grid_columnconfigure(col, weight=1) # Reset and Apply buttons keyword_btn_frame = ttk.Frame(keyword_settings_frame) keyword_btn_frame.pack(fill=tk.X, pady=(10, 0)) ttk.Button(keyword_btn_frame, text="๐Ÿ”„ Reset to Defaults", command=self.reset_keywords_to_default).pack(side=tk.LEFT, padx=(0, 10)) ttk.Button(keyword_btn_frame, text="โœ… Apply Keywords", command=self.apply_custom_keywords).pack(side=tk.LEFT) # Data directory settings data_settings_frame = ttk.LabelFrame(settings_scrollable_frame, text="Data Directory", padding=10) data_settings_frame.pack(fill=tk.X, pady=5) current_dir_frame = ttk.Frame(data_settings_frame) current_dir_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Label(current_dir_frame, text="Current data directory:").pack(anchor=tk.W) self.data_dir_label = ttk.Label(current_dir_frame, text=str(self.storage.base_data_dir.absolute()), foreground="blue") self.data_dir_label.pack(anchor=tk.W, pady=(5, 0)) ttk.Button(data_settings_frame, text="๐Ÿ“ Open Data Folder", command=self.open_data_folder).pack(side=tk.LEFT, padx=(0, 10)) ttk.Button(data_settings_frame, text="๐Ÿงน Clean Up Corrupted Files", command=self.cleanup_files).pack(side=tk.LEFT) # System info info_frame = ttk.LabelFrame(settings_scrollable_frame, text="System Information", padding=10) info_frame.pack(fill=tk.X, pady=5) self.check_system_requirements(info_frame) # Credit text credit_frame = ttk.Frame(settings_scrollable_frame) credit_frame.pack(fill=tk.X, pady=(20, 10)) credit_label = ttk.Label(credit_frame, text="Created by Andy Kong", font=("Arial", 9, "italic"), foreground="gray") credit_label.pack(anchor=tk.CENTER) def create_filter_selection_ui(self, parent_frame): """Create the visual filter selection UI that can be reused in multiple tabs.""" # Create scrollable frame for filter selection filter_canvas = tk.Canvas(parent_frame, height=120, bg="#f0f0f0") filter_scrollbar = ttk.Scrollbar(parent_frame, orient="horizontal", command=filter_canvas.xview) filter_scroll_frame = ttk.Frame(filter_canvas) filter_scroll_frame.bind( "", lambda e: filter_canvas.configure(scrollregion=filter_canvas.bbox("all")) ) filter_canvas.create_window((0, 0), window=filter_scroll_frame, anchor="nw") filter_canvas.configure(xscrollcommand=filter_scrollbar.set) filter_canvas.pack(fill=tk.X, pady=(0, 5)) filter_scrollbar.pack(fill=tk.X) # Create visual filter selection buttons for i, (filter_num, data) in enumerate(self.filter_data.items()): filter_btn_frame = ttk.Frame(filter_scroll_frame) filter_btn_frame.grid(row=0, column=i, padx=5, pady=5) # Try to load preview image preview_image = None ui_img_path = Path("src/ui_img") for img_file in ui_img_path.glob(f"*_{filter_num}.jpg"): try: pil_img = Image.open(img_file) pil_img.thumbnail((80, 80), Image.Resampling.LANCZOS) preview_image = ImageTk.PhotoImage(pil_img) break except: continue # Create filter button if preview_image: filter_btn = tk.Button(filter_btn_frame, image=preview_image, command=lambda f=filter_num: self.select_filter(f), relief=tk.RAISED, bd=2, bg=data["color"], activebackground=data["color"]) filter_btn.image = preview_image # Keep reference else: filter_btn = tk.Button(filter_btn_frame, text=filter_num, command=lambda f=filter_num: self.select_filter(f), relief=tk.RAISED, bd=2, bg=data["color"], activebackground=data["color"], width=8, height=4) filter_btn.pack() # Filter info info_label = ttk.Label(filter_btn_frame, text=data["name"], font=("Arial", 8, "bold"), anchor=tk.CENTER) info_label.pack() desc_label = ttk.Label(filter_btn_frame, text=data["desc"], font=("Arial", 7), anchor=tk.CENTER, foreground="gray") desc_label.pack() self.filter_buttons[filter_num] = filter_btn # Update initial selection if not already done if not self._filter_initialized: # Set the filter without triggering refresh during initialization self.solar_filter_var.set("0211") # Update button appearances for fnum, btn in self.filter_buttons.items(): if fnum == "0211": btn.config(relief=tk.SUNKEN, bd=3) else: btn.config(relief=tk.RAISED, bd=2) self._filter_initialized = True def select_filter(self, filter_num): """Select a solar filter and update the UI.""" self.solar_filter_var.set(filter_num) # Update button appearances for fnum, btn in self.filter_buttons.items(): if fnum == filter_num: btn.config(relief=tk.SUNKEN, bd=3) else: btn.config(relief=tk.RAISED, bd=2) # Trigger filter change self.on_filter_change() def on_filter_change(self, event=None): """Handle changes to resolution or solar filter settings.""" resolution = self.resolution_var.get() solar_filter = self.solar_filter_var.get() # Get custom keyword if available search_keyword = self.get_current_search_keyword() # Update scraper with new settings (using custom keyword) self.scraper.update_filters(resolution, search_keyword) # Update storage organizer with new pattern (using custom keyword) self.storage.update_file_pattern(resolution, search_keyword) # Refresh available dates to reflect new filter (only if UI is fully initialized) if hasattr(self, 'viewer_from_date_combo'): self.refresh_available_dates() def on_keyword_change(self, *args): """Handle changes to custom keyword settings.""" # This method is called when any keyword is modified # We don't need to do anything immediately, changes are applied when user clicks Apply pass def reset_keywords_to_default(self): """Reset all custom keywords to their default filter numbers.""" for filter_num in self.filter_data.keys(): self.custom_keywords[filter_num].set(filter_num) messagebox.showinfo("Keywords Reset", "All keywords have been reset to default filter numbers.") def apply_custom_keywords(self): """Apply the custom keywords to the search system.""" try: # Get current settings resolution = self.resolution_var.get() # Update the scraper and storage with custom keywords # For now, we'll use the currently selected filter's custom keyword current_filter = self.solar_filter_var.get() custom_keyword = self.custom_keywords[current_filter].get().strip() if not custom_keyword: custom_keyword = current_filter # Fallback to default # Update components with custom keyword self.scraper.update_filters(resolution, custom_keyword) self.storage.update_file_pattern(resolution, custom_keyword) # Refresh available dates if hasattr(self, 'viewer_from_date_combo'): self.refresh_available_dates() messagebox.showinfo("Keywords Applied", f"Custom keywords applied successfully!\n" f"Current search keyword: '{custom_keyword}'") except Exception as e: messagebox.showerror("Error", f"Failed to apply custom keywords: {str(e)}") def get_current_search_keyword(self): """Get the current search keyword for the selected filter.""" current_filter = self.solar_filter_var.get() if hasattr(self, 'custom_keywords') and current_filter in self.custom_keywords: custom_keyword = self.custom_keywords[current_filter].get().strip() return custom_keyword if custom_keyword else current_filter return current_filter def check_system_requirements(self, parent_frame): """Check and display system requirements.""" # Check FFmpeg try: result = subprocess.run(['ffmpeg', '-version'], capture_output=True, timeout=5) ffmpeg_status = "โœ… Available" if result.returncode == 0 else "โŒ Not found" except: ffmpeg_status = "โŒ Not found" ttk.Label(parent_frame, text=f"FFmpeg: {ffmpeg_status}").pack(anchor=tk.W) # Check OpenCV try: import cv2 opencv_status = f"โœ… Available (v{cv2.__version__})" except: opencv_status = "โŒ Not found" ttk.Label(parent_frame, text=f"OpenCV: {opencv_status}").pack(anchor=tk.W) # Check PIL try: from PIL import Image pil_status = f"โœ… Available (v{Image.__version__})" except: pil_status = "โŒ Not found" ttk.Label(parent_frame, text=f"Pillow: {pil_status}").pack(anchor=tk.W) def log_message(self, message): """Add message to download log.""" timestamp = datetime.now().strftime("%H:%M:%S") self.log_text.insert(tk.END, f"[{timestamp}] {message}\n") self.log_text.see(tk.END) self.root.update_idletasks() def set_date_range(self, days_back): """Set date range for quick selection.""" end_date = datetime.now() start_date = end_date - timedelta(days=days_back) self.start_date_var.set(start_date.strftime("%Y-%m-%d")) self.end_date_var.set(end_date.strftime("%Y-%m-%d")) def set_video_date_range(self, days_back): """Set video date range for quick selection.""" end_date = datetime.now() start_date = end_date - timedelta(days=days_back) self.video_start_date_var.set(start_date.strftime("%Y-%m-%d")) self.video_end_date_var.set(end_date.strftime("%Y-%m-%d")) def start_download(self): """Start the download process in a background thread.""" if self.download_thread and self.download_thread.is_alive(): messagebox.showwarning("Download in Progress", "A download is already in progress!") return self.download_btn.config(state=tk.DISABLED) self.log_text.delete(1.0, tk.END) self.download_thread = threading.Thread(target=self._download_worker, daemon=True) self.download_thread.start() def _download_worker(self): """Download worker running in background thread.""" try: # Parse dates start_date = datetime.strptime(self.start_date_var.get(), "%Y-%m-%d") end_date = datetime.strptime(self.end_date_var.get(), "%Y-%m-%d") self.root.after(0, lambda: self.log_message(f"Starting download for {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")) self.root.after(0, lambda: self.status_label.config(text="Scanning directories...")) # Get available images available_images = self.scraper.get_available_images_for_date_range(start_date, end_date) self.root.after(0, lambda: self.log_message(f"Found {len(available_images)} total images")) if not available_images: self.root.after(0, lambda: self.log_message("No images found for selected date range")) self.root.after(0, lambda: self.status_label.config(text="No images found")) return # Filter new images new_images = self.scraper.filter_new_images(available_images, self.storage) self.root.after(0, lambda: self.log_message(f"Found {len(new_images)} new images to download")) if not new_images: self.root.after(0, lambda: self.log_message("All images already downloaded!")) self.root.after(0, lambda: self.status_label.config(text="All images up to date")) return # Create download tasks tasks = self.scraper.create_download_tasks(new_images, self.storage) # Download images successful = 0 failed = 0 for i, task in enumerate(tasks): progress = (i / len(tasks)) * 100 self.root.after(0, lambda p=progress: self.progress_var.set(p)) filename = task.target_path.name self.root.after(0, lambda f=filename, idx=i+1, total=len(tasks): self.status_label.config(text=f"Downloading {idx}/{total}: {f}")) success = self.download_manager.download_and_save(task) if success: successful += 1 self.root.after(0, lambda f=filename: self.log_message(f"โœ… Downloaded: {f}")) else: failed += 1 self.root.after(0, lambda f=filename, err=task.error_message: self.log_message(f"โŒ Failed: {f} - {err}")) # Final status self.root.after(0, lambda: self.progress_var.set(100)) self.root.after(0, lambda: self.log_message(f"Download complete: {successful} successful, {failed} failed")) self.root.after(0, lambda: self.status_label.config(text=f"Complete: {successful}/{len(tasks)} downloaded")) self.root.after(0, self.refresh_available_dates) except Exception as e: self.root.after(0, lambda: self.log_message(f"โŒ Error: {str(e)}")) self.root.after(0, lambda: self.status_label.config(text="Download failed")) finally: self.root.after(0, lambda: self.download_btn.config(state=tk.NORMAL)) def refresh_available_dates(self): """Refresh the list of available dates.""" dates = [] data_dir = self.storage.base_data_dir if data_dir.exists(): for year_dir in data_dir.iterdir(): if not year_dir.is_dir() or not year_dir.name.isdigit(): continue for month_dir in year_dir.iterdir(): if not month_dir.is_dir() or not month_dir.name.isdigit(): continue for day_dir in month_dir.iterdir(): if not day_dir.is_dir() or not day_dir.name.isdigit(): continue images = list(day_dir.glob(f"*_{self.resolution_var.get()}_{self.solar_filter_var.get()}.jpg")) if images: try: date = datetime(int(year_dir.name), int(month_dir.name), int(day_dir.name)) date_str = f"{date.strftime('%Y-%m-%d')} ({len(images)} images)" dates.append((date, date_str)) except ValueError: continue if dates: dates.sort(reverse=True) # Most recent first date_strings = [date_str for _, date_str in dates] # Update all date combo boxes (only viewer tab now has combo boxes) if hasattr(self, 'viewer_from_date_combo'): self.viewer_from_date_combo['values'] = date_strings self.viewer_to_date_combo['values'] = date_strings if date_strings: self.viewer_from_date_combo.current(0) self.viewer_to_date_combo.current(0) self.available_dates = {date_str: date for date, date_str in dates} else: if hasattr(self, 'viewer_from_date_combo'): self.viewer_from_date_combo['values'] = [] self.viewer_to_date_combo['values'] = [] self.available_dates = {} def load_images_for_viewer(self): """Load images for the viewer tab.""" from_selected = self.viewer_from_date_var.get() to_selected = self.viewer_to_date_var.get() if not from_selected or from_selected not in self.available_dates: messagebox.showerror("Error", "Please select a valid 'From' date") return if not to_selected or to_selected not in self.available_dates: messagebox.showerror("Error", "Please select a valid 'To' date") return from_date = self.available_dates[from_selected] to_date = self.available_dates[to_selected] # Ensure from_date is not after to_date if from_date > to_date: from_date, to_date = to_date, from_date # Load images from all dates in the range self.current_images = [] total_images = 0 # Get all dates in range current_date = from_date while current_date <= to_date: image_files = self.storage.list_local_images(current_date) if image_files: date_path = self.storage.get_date_path(current_date) for filename in sorted(image_files): image_path = date_path / filename self.current_images.append((image_path, filename, current_date)) total_images += 1 current_date += timedelta(days=1) if not self.current_images: messagebox.showerror("Error", f"No images found for date range {from_date.strftime('%Y-%m-%d')} to {to_date.strftime('%Y-%m-%d')}") return # Sort all images by filename (which includes timestamp) self.current_images.sort(key=lambda x: x[1]) self.current_image_index = 0 self.update_image_display() date_range_text = f"{from_date.strftime('%Y-%m-%d')}" if from_date == to_date else f"{from_date.strftime('%Y-%m-%d')} to {to_date.strftime('%Y-%m-%d')}" self.image_info_label.config(text=f"Loaded {total_images} images for {date_range_text}") def update_image_display(self): """Update the image display in viewer tab.""" if not self.current_images: return # Handle both old format (path, filename) and new format (path, filename, date) current_item = self.current_images[self.current_image_index] if len(current_item) == 3: image_path, filename, image_date = current_item else: image_path, filename = current_item image_date = None try: # Load and resize image pil_image = Image.open(image_path) # Calculate size to fit in display area display_size = (600, 600) pil_image.thumbnail(display_size, Image.Resampling.LANCZOS) # Convert to PhotoImage photo = ImageTk.PhotoImage(pil_image) # Update display self.image_display_label.config(image=photo, text="") self.image_display_label.image = photo # Keep reference # Update progress and info progress = (self.current_image_index + 1) / len(self.current_images) * 100 self.image_progress_var.set(progress) # Extract timestamp timestamp = filename.split('_')[1] if '_' in filename else "Unknown" if len(timestamp) == 6: formatted_time = f"{timestamp[:2]}:{timestamp[2:4]}:{timestamp[4:6]}" else: formatted_time = timestamp # Include date in info if available if image_date: info_text = f"Image {self.current_image_index + 1}/{len(self.current_images)} - {image_date.strftime('%Y-%m-%d')} Time: {formatted_time}" else: info_text = f"Image {self.current_image_index + 1}/{len(self.current_images)} - Time: {formatted_time}" self.image_info_label.config(text=info_text) # Update speed display speed = self.speed_var.get() self.speed_display.config(text=f"{speed:.1f} FPS") except Exception as e: self.image_display_label.config(text=f"Error loading image: {e}") def update_speed_display(self, value=None): """Update the speed display when slider changes.""" speed = self.speed_var.get() self.speed_display.config(text=f"{speed:.1f} FPS") def first_image(self): """Go to first image.""" if self.current_images: self.current_image_index = 0 self.update_image_display() def prev_image(self): """Go to previous image.""" if self.current_images and self.current_image_index > 0: self.current_image_index -= 1 self.update_image_display() def next_image(self): """Go to next image.""" if self.current_images and self.current_image_index < len(self.current_images) - 1: self.current_image_index += 1 self.update_image_display() def last_image(self): """Go to last image.""" if self.current_images: self.current_image_index = len(self.current_images) - 1 self.update_image_display() def toggle_play(self): """Toggle play/pause for image sequence.""" if self.is_playing: self.stop_play() else: self.start_play() def start_play(self): """Start playing image sequence.""" if not self.current_images: messagebox.showwarning("No Images", "Please load images first") return self.is_playing = True self.play_btn.config(text="โธ Pause") self.play_thread = threading.Thread(target=self._play_loop, daemon=True) self.play_thread.start() def stop_play(self): """Stop playing image sequence.""" self.is_playing = False self.play_btn.config(text="โ–ถ Play") def _play_loop(self): """Play loop for image sequence.""" while self.is_playing and self.current_images: if self.current_image_index >= len(self.current_images) - 1: self.current_image_index = 0 # Loop back else: self.current_image_index += 1 self.root.after(0, self.update_image_display) # Wait based on FPS fps = self.speed_var.get() delay = 1.0 / fps import time time.sleep(delay) def create_date_range_video(self): """Create video for selected date range.""" try: # Parse dates start_date = datetime.strptime(self.video_start_date_var.get(), "%Y-%m-%d") end_date = datetime.strptime(self.video_end_date_var.get(), "%Y-%m-%d") except ValueError: messagebox.showerror("Invalid Date", "Please enter valid dates in YYYY-MM-DD format") return if start_date > end_date: messagebox.showerror("Invalid Date Range", "Start date must be before or equal to end date") return fps = self.video_fps_var.get() # Check if ffmpeg is available try: subprocess.run(['ffmpeg', '-version'], capture_output=True, timeout=5) except: messagebox.showerror("FFmpeg Not Found", "FFmpeg is required to create videos.\n\n" "Please install FFmpeg:\n" "โ€ข Windows: Download from https://ffmpeg.org/\n" "โ€ข Or use: winget install FFmpeg") return # Create video in background thread threading.Thread(target=self._create_date_range_video_worker, args=(start_date, end_date, fps), daemon=True).start() def _create_date_range_video_worker(self, start_date, end_date, fps): """Create video for date range in background thread.""" try: # Reset progress bar self.root.after(0, lambda: self.video_progress_var.set(0)) self.root.after(0, lambda: self.video_status_label.config(text="Initializing video creation...")) # Create video directory if it doesn't exist video_dir = Path("video") video_dir.mkdir(exist_ok=True) # Generate output filename with date range if start_date == end_date: output_file = f"nasa_solar_{start_date.strftime('%Y%m%d')}.mp4" else: output_file = f"nasa_solar_{start_date.strftime('%Y%m%d')}_to_{end_date.strftime('%Y%m%d')}.mp4" output_path = video_dir / output_file 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')}" # Update progress: 10% self.root.after(0, lambda: self.video_progress_var.set(10)) self.root.after(0, lambda: self.video_status_label.config(text="Collecting images...")) # Collect all images from the date range all_image_paths = [] current_date = start_date while current_date <= end_date: images = self.storage.list_local_images(current_date) if images: date_path = self.storage.get_date_path(current_date) # Add all images from this date for filename in sorted(images): image_path = date_path / filename if image_path.exists(): all_image_paths.append(image_path) current_date += timedelta(days=1) if not all_image_paths: self.root.after(0, lambda: self.video_status_label.config(text="No images found")) self.root.after(0, lambda: messagebox.showerror("Error", f"No images found for date range {date_range_text}")) return # Update progress: 20% self.root.after(0, lambda: self.video_progress_var.set(20)) self.root.after(0, lambda: self.video_status_label.config(text=f"Found {len(all_image_paths)} images. Preparing frames...")) # Create temporary directory for ffmpeg temp_dir = Path("temp_video_frames") temp_dir.mkdir(exist_ok=True) try: # Create sequential frame files for all images with progress updates total_images = len(all_image_paths) for i, src_path in enumerate(all_image_paths): temp_path = temp_dir / f"frame_{i:06d}.jpg" # Remove existing file if it exists if temp_path.exists(): temp_path.unlink() try: temp_path.symlink_to(src_path.absolute()) except OSError: shutil.copy2(src_path, temp_path) # Update progress: 20% to 70% for frame preparation progress = 20 + (i / total_images) * 50 self.root.after(0, lambda p=progress: self.video_progress_var.set(p)) self.root.after(0, lambda idx=i+1, total=total_images: self.video_status_label.config(text=f"Preparing frame {idx}/{total}...")) # Update progress: 70% self.root.after(0, lambda: self.video_progress_var.set(70)) self.root.after(0, lambda: self.video_status_label.config(text="Running FFmpeg to create video...")) # Run ffmpeg input_pattern = str(temp_dir / "frame_%06d.jpg") ffmpeg_cmd = [ 'ffmpeg', '-y', '-framerate', str(fps), '-i', input_pattern, '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-crf', '18', str(output_path) ] result = subprocess.run(ffmpeg_cmd, capture_output=True, text=True) # Update progress: 90% self.root.after(0, lambda: self.video_progress_var.set(90)) self.root.after(0, lambda: self.video_status_label.config(text="Finalizing video...")) if result.returncode == 0: # Update progress: 100% self.root.after(0, lambda: self.video_progress_var.set(100)) self.root.after(0, lambda: self.video_status_label.config(text="Video created successfully!")) size_mb = output_path.stat().st_size / (1024 * 1024) duration = len(all_image_paths) / fps self.root.after(0, lambda: messagebox.showinfo("Success", f"Video created successfully!\n\n" f"File: {output_path}\n" f"Size: {size_mb:.1f} MB\n" f"Total frames: {len(all_image_paths)}\n" f"Duration: {duration:.1f} seconds\n" f"Date range: {date_range_text}")) else: self.root.after(0, lambda: self.video_status_label.config(text="Video creation failed")) self.root.after(0, lambda: messagebox.showerror("Error", f"FFmpeg error:\n{result.stderr}")) finally: if temp_dir.exists(): shutil.rmtree(temp_dir) except Exception as e: self.root.after(0, lambda: self.video_status_label.config(text="Video creation failed")) self.root.after(0, lambda: messagebox.showerror("Error", f"Video creation failed: {str(e)}")) def create_single_video(self): """Create video for selected date.""" selected = self.video_date_var.get() if not selected or selected not in self.available_dates: messagebox.showerror("Error", "Please select a valid date") return date = self.available_dates[selected] fps = self.video_fps_var.get() # Check if ffmpeg is available try: subprocess.run(['ffmpeg', '-version'], capture_output=True, timeout=5) except: messagebox.showerror("FFmpeg Not Found", "FFmpeg is required to create videos.\n\n" "Please install FFmpeg:\n" "โ€ข Windows: Download from https://ffmpeg.org/\n" "โ€ข Or use: winget install FFmpeg") return # Create video in background thread threading.Thread(target=self._create_video_worker, args=(date, fps), daemon=True).start() def _create_video_worker(self, date, fps): """Create video in background thread.""" try: # Create video directory if it doesn't exist video_dir = Path("video") video_dir.mkdir(exist_ok=True) output_file = f"nasa_solar_{date.strftime('%Y%m%d')}.mp4" output_path = video_dir / output_file self.root.after(0, lambda: messagebox.showinfo("Creating Video", f"Creating video for {date.strftime('%Y-%m-%d')}...\n" f"This may take a few minutes.")) # Get images images = self.storage.list_local_images(date) if not images: self.root.after(0, lambda: messagebox.showerror("Error", "No images found for selected date")) return date_path = self.storage.get_date_path(date) # Create temporary directory for ffmpeg temp_dir = Path("temp_video_frames") temp_dir.mkdir(exist_ok=True) try: # Create sequential frame files sorted_images = sorted(images) for i, image in enumerate(sorted_images): src_path = date_path / image temp_path = temp_dir / f"frame_{i:06d}.jpg" if temp_path.exists(): temp_path.unlink() try: temp_path.symlink_to(src_path.absolute()) except OSError: shutil.copy2(src_path, temp_path) # Run ffmpeg input_pattern = str(temp_dir / "frame_%06d.jpg") ffmpeg_cmd = [ 'ffmpeg', '-y', '-framerate', str(fps), '-i', input_pattern, '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-crf', '18', str(output_path) ] result = subprocess.run(ffmpeg_cmd, capture_output=True, text=True) if result.returncode == 0: size_mb = output_path.stat().st_size / (1024 * 1024) self.root.after(0, lambda: messagebox.showinfo("Success", f"Video created successfully!\n\n" f"File: {output_path}\n" f"Size: {size_mb:.1f} MB\n" f"Duration: ~{len(sorted_images)/fps:.1f} seconds")) else: self.root.after(0, lambda: messagebox.showerror("Error", f"FFmpeg error:\n{result.stderr}")) finally: if temp_dir.exists(): shutil.rmtree(temp_dir) except Exception as e: self.root.after(0, lambda: messagebox.showerror("Error", f"Video creation failed: {str(e)}")) def create_all_videos(self): """Create one combined video with all images from all dates.""" if not self.available_dates: messagebox.showwarning("No Data", "No downloaded images found") return fps = self.video_fps_var.get() # Count total images total_images = 0 for date_str, date in self.available_dates.items(): images = self.storage.list_local_images(date) total_images += len(images) result = messagebox.askyesno("Create Combined Video", f"Create one combined video with images from all {len(self.available_dates)} dates?\n" f"Total images: {total_images}\n" f"Estimated duration: ~{total_images/fps:.1f} seconds\n" f"This may take a long time.") if not result: return threading.Thread(target=self._create_combined_video_worker, args=(fps,), daemon=True).start() def _create_combined_video_worker(self, fps): """Create combined video in background thread.""" try: # Reset progress bar self.root.after(0, lambda: self.video_progress_var.set(0)) self.root.after(0, lambda: self.video_status_label.config(text="Initializing combined video creation...")) # Create video directory if it doesn't exist video_dir = Path("video") video_dir.mkdir(exist_ok=True) # Generate output filename with date range dates = sorted(self.available_dates.values()) start_date = dates[0] end_date = dates[-1] if start_date == end_date: output_file = f"nasa_solar_combined_{start_date.strftime('%Y%m%d')}.mp4" else: output_file = f"nasa_solar_combined_{start_date.strftime('%Y%m%d')}_to_{end_date.strftime('%Y%m%d')}.mp4" output_path = video_dir / output_file # Update progress: 10% self.root.after(0, lambda: self.video_progress_var.set(10)) self.root.after(0, lambda: self.video_status_label.config(text="Collecting images from all dates...")) # Collect all images from all dates all_image_paths = [] for date in sorted(self.available_dates.values()): images = self.storage.list_local_images(date) if images: date_path = self.storage.get_date_path(date) # Add all images from this date for filename in sorted(images): image_path = date_path / filename all_image_paths.append(image_path) if not all_image_paths: self.root.after(0, lambda: self.video_status_label.config(text="No images found")) self.root.after(0, lambda: messagebox.showerror("Error", "No images found")) return # Update progress: 20% self.root.after(0, lambda: self.video_progress_var.set(20)) self.root.after(0, lambda: self.video_status_label.config(text=f"Found {len(all_image_paths)} images. Preparing frames...")) # Create temporary directory for ffmpeg temp_dir = Path("temp_combined_video_frames") temp_dir.mkdir(exist_ok=True) try: # Create sequential frame files for all images with progress updates total_images = len(all_image_paths) for i, src_path in enumerate(all_image_paths): temp_path = temp_dir / f"frame_{i:06d}.jpg" # Remove existing file if it exists if temp_path.exists(): temp_path.unlink() try: temp_path.symlink_to(src_path.absolute()) except OSError: shutil.copy2(src_path, temp_path) # Update progress: 20% to 70% for frame preparation progress = 20 + (i / total_images) * 50 self.root.after(0, lambda p=progress: self.video_progress_var.set(p)) self.root.after(0, lambda idx=i+1, total=total_images: self.video_status_label.config(text=f"Preparing frame {idx}/{total}...")) # Update progress: 70% self.root.after(0, lambda: self.video_progress_var.set(70)) self.root.after(0, lambda: self.video_status_label.config(text="Running FFmpeg to create combined video...")) # Run ffmpeg input_pattern = str(temp_dir / "frame_%06d.jpg") ffmpeg_cmd = [ 'ffmpeg', '-y', '-framerate', str(fps), '-i', input_pattern, '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-crf', '18', str(output_path) ] result = subprocess.run(ffmpeg_cmd, capture_output=True, text=True) # Update progress: 90% self.root.after(0, lambda: self.video_progress_var.set(90)) self.root.after(0, lambda: self.video_status_label.config(text="Finalizing combined video...")) if result.returncode == 0: # Update progress: 100% self.root.after(0, lambda: self.video_progress_var.set(100)) self.root.after(0, lambda: self.video_status_label.config(text="Combined video created successfully!")) size_mb = output_path.stat().st_size / (1024 * 1024) duration = len(all_image_paths) / fps self.root.after(0, lambda: messagebox.showinfo("Success", f"Combined video created successfully!\n\n" f"File: {output_path}\n" f"Size: {size_mb:.1f} MB\n" f"Total frames: {len(all_image_paths)}\n" f"Duration: {duration:.1f} seconds\n" f"Date range: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")) else: self.root.after(0, lambda: self.video_status_label.config(text="Combined video creation failed")) self.root.after(0, lambda: messagebox.showerror("Error", f"FFmpeg error:\n{result.stderr}")) finally: if temp_dir.exists(): shutil.rmtree(temp_dir) except Exception as e: self.root.after(0, lambda: self.video_status_label.config(text="Combined video creation failed")) self.root.after(0, lambda: messagebox.showerror("Error", f"Combined video creation failed: {str(e)}")) def select_video_file(self): """Select MP4 file for playback.""" video_dir = Path("video") initial_dir = str(video_dir) if video_dir.exists() else str(Path.cwd()) file_path = filedialog.askopenfilename( title="Select MP4 Video", initialdir=initial_dir, filetypes=[("MP4 files", "*.mp4"), ("All files", "*.*")] ) if file_path: # Stop any current video playback if self.video_playing: self.stop_video() self.selected_video_path = file_path filename = Path(file_path).name self.selected_video_label.config(text=f"Selected: {filename}") self.video_play_btn.config(state=tk.NORMAL) self.fullscreen_btn.config(state=tk.NORMAL) # Show video info and preview frame try: cap = cv2.VideoCapture(file_path) if cap.isOpened(): fps = cap.get(cv2.CAP_PROP_FPS) frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) duration = frame_count / fps if fps > 0 else 0 width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) info_text = f"Selected: {filename}\nDuration: {duration:.1f}s, Size: {width}x{height}, FPS: {fps:.1f}" self.selected_video_label.config(text=info_text) # Get first frame for preview with 1024x1024 display size ret, frame = cap.read() if ret: # Convert BGR to RGB frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) pil_image = Image.fromarray(frame_rgb) # Use fixed display size of 1024x1024 pixels display_width = 1024 display_height = 1024 # Calculate the best fit size while maintaining aspect ratio original_width, original_height = pil_image.size aspect_ratio = original_width / original_height # Calculate scaled dimensions to fit within 1024x1024 display area if aspect_ratio > 1.0: # Video is wider - fit to width new_width = display_width new_height = int(display_width / aspect_ratio) else: # Video is taller or square - fit to height new_height = display_height new_width = int(display_height * aspect_ratio) # Resize the preview image maintaining aspect ratio preview_image = pil_image.resize((new_width, new_height), Image.Resampling.LANCZOS) preview_photo = ImageTk.PhotoImage(preview_image) # Show preview frame self.video_display_label.config(image=preview_photo, text="") self.video_display_label.image = preview_photo # Keep reference cap.release() else: self.selected_video_label.config(text=f"Selected: {filename} (Could not read video info)") self.video_display_label.config(image="", text="Could not load video preview") except Exception as e: self.selected_video_label.config(text=f"Selected: {filename}") self.video_display_label.config(image="", text=f"Error loading preview: {str(e)}") def play_video(self): """Play the selected MP4 video embedded in the GUI.""" if not self.selected_video_path: messagebox.showwarning("No Video", "Please select a video file first") return if self.video_playing: self.stop_video() return try: # Open video file with OpenCV self.video_cap = cv2.VideoCapture(self.selected_video_path) if not self.video_cap.isOpened(): messagebox.showerror("Error", "Could not open video file") return # Get video properties fps = self.video_cap.get(cv2.CAP_PROP_FPS) frame_count = int(self.video_cap.get(cv2.CAP_PROP_FRAME_COUNT)) duration = frame_count / fps if fps > 0 else 0 self.video_playing = True self.video_play_btn.config(text="โธ Pause") # Start video playback thread self.video_thread = threading.Thread(target=self._video_playback_loop, args=(fps,), daemon=True) self.video_thread.start() except Exception as e: messagebox.showerror("Error", f"Could not play video: {str(e)}") def stop_video(self): """Stop video playback.""" self.video_playing = False self.video_play_btn.config(text="โ–ถ Play Video") # Exit fullscreen if active if self.fullscreen_mode: self.exit_fullscreen() if self.video_cap: self.video_cap.release() self.video_cap = None # Keep the last frame displayed instead of clearing # The last frame is already shown in self.video_display_label at 1024x1024 # So we don't need to clear it - just leave it as is to maintain the size def toggle_fullscreen(self): """Toggle fullscreen mode for video playback.""" if not self.selected_video_path: messagebox.showwarning("No Video", "Please select a video file first") return if self.fullscreen_mode: self.exit_fullscreen() else: self.enter_fullscreen() def enter_fullscreen(self): """Enter fullscreen mode.""" if self.fullscreen_mode: return self.fullscreen_mode = True self.fullscreen_btn.config(text="๐Ÿ”ฒ Exit Fullscreen") # Create fullscreen window self.fullscreen_window = tk.Toplevel(self.root) self.fullscreen_window.title("NASA Solar Video - Fullscreen") self.fullscreen_window.configure(bg='black') # Make it fullscreen self.fullscreen_window.attributes('-fullscreen', True) self.fullscreen_window.attributes('-topmost', True) # Create fullscreen video label self.fullscreen_video_label = tk.Label(self.fullscreen_window, text="Loading video...", background="black", foreground="white", font=("Arial", 24)) self.fullscreen_video_label.pack(fill=tk.BOTH, expand=True) # Bind escape key to exit fullscreen self.fullscreen_window.bind('', lambda e: self.exit_fullscreen()) self.fullscreen_window.bind('', lambda e: self.exit_fullscreen() if e.keysym == 'Escape' else None) self.fullscreen_window.focus_set() # Handle window close self.fullscreen_window.protocol("WM_DELETE_WINDOW", self.exit_fullscreen) # Start video if not already playing if not self.video_playing: self.play_video() def exit_fullscreen(self): """Exit fullscreen mode.""" if not self.fullscreen_mode: return self.fullscreen_mode = False self.fullscreen_btn.config(text="๐Ÿ”ณ Fullscreen") if self.fullscreen_window: self.fullscreen_window.destroy() self.fullscreen_window = None self.fullscreen_video_label = None def _video_playback_loop(self, fps): """Video playback loop running in background thread.""" frame_delay = 1.0 / fps if fps > 0 else 1.0 / 30 # Default to 30 FPS if unknown while self.video_playing and self.video_cap: ret, frame = self.video_cap.read() if not ret: # End of video, loop back to beginning self.video_cap.set(cv2.CAP_PROP_POS_FRAMES, 0) continue try: # Convert BGR to RGB frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # Convert to PIL Image pil_image = Image.fromarray(frame_rgb) # Create different sized images for regular and fullscreen display if self.fullscreen_mode and self.fullscreen_window: # Get screen size for fullscreen screen_width = self.root.winfo_screenwidth() screen_height = self.root.winfo_screenheight() fullscreen_size = (screen_width, screen_height) # Create fullscreen image fullscreen_image = pil_image.copy() fullscreen_image.thumbnail(fullscreen_size, Image.Resampling.LANCZOS) fullscreen_photo = ImageTk.PhotoImage(fullscreen_image) # Update both displays self.root.after(0, self._update_video_frame, pil_image, fullscreen_photo) else: # Regular display only self.root.after(0, self._update_video_frame, pil_image, None) # Wait for next frame import time time.sleep(frame_delay) except Exception as e: print(f"Error displaying video frame: {e}") break # Cleanup when done self.root.after(0, self._video_playback_finished) def _update_video_frame(self, pil_image, fullscreen_photo=None): """Update video frame in the GUI (called from main thread).""" if self.video_playing: # Use fixed display size of 1024x1024 pixels display_width = 1024 display_height = 1024 # Calculate the best fit size while maintaining aspect ratio original_width, original_height = pil_image.size aspect_ratio = original_width / original_height # Calculate scaled dimensions to fit within 1024x1024 display area if aspect_ratio > 1.0: # Video is wider - fit to width new_width = display_width new_height = int(display_width / aspect_ratio) else: # Video is taller or square - fit to height new_height = display_height new_width = int(display_height * aspect_ratio) # Resize the image maintaining aspect ratio regular_image = pil_image.copy() regular_image = regular_image.resize((new_width, new_height), Image.Resampling.LANCZOS) regular_photo = ImageTk.PhotoImage(regular_image) self.video_display_label.config(image=regular_photo, text="") self.video_display_label.image = regular_photo # Keep reference # Update fullscreen display if active if self.fullscreen_mode and self.fullscreen_window and fullscreen_photo: try: self.fullscreen_video_label.config(image=fullscreen_photo, text="") self.fullscreen_video_label.image = fullscreen_photo # Keep reference except: pass # Fullscreen window might have been closed def _video_playback_finished(self): """Called when video playback finishes.""" self.video_playing = False self.video_play_btn.config(text="โ–ถ Play Video") if self.video_cap: self.video_cap.release() self.video_cap = None def open_video_folder(self): """Open the folder containing videos.""" try: video_dir = Path("video") video_dir.mkdir(exist_ok=True) # Create if it doesn't exist if sys.platform.startswith('win'): os.startfile(video_dir) elif sys.platform.startswith('darwin'): subprocess.run(['open', str(video_dir)]) else: subprocess.run(['xdg-open', str(video_dir)]) except Exception as e: messagebox.showerror("Error", f"Could not open video folder: {str(e)}") def open_data_folder(self): """Open the data folder.""" try: data_dir = self.storage.base_data_dir if sys.platform.startswith('win'): os.startfile(data_dir) elif sys.platform.startswith('darwin'): subprocess.run(['open', str(data_dir)]) else: subprocess.run(['xdg-open', str(data_dir)]) except Exception as e: messagebox.showerror("Error", f"Could not open data folder: {str(e)}") def cleanup_files(self): """Clean up corrupted files.""" result = messagebox.askyesno("Clean Up Files", "This will remove corrupted (zero-size) image files.\n" "Continue?") if not result: return total_removed = 0 data_dir = self.storage.base_data_dir if data_dir.exists(): for year_dir in data_dir.iterdir(): if not year_dir.is_dir(): continue for month_dir in year_dir.iterdir(): if not month_dir.is_dir(): continue for day_dir in month_dir.iterdir(): if not day_dir.is_dir(): continue try: date = datetime(int(year_dir.name), int(month_dir.name), int(day_dir.name)) removed = self.storage.cleanup_corrupted_files(date) total_removed += removed except: continue messagebox.showinfo("Cleanup Complete", f"Removed {total_removed} corrupted files") self.refresh_available_dates() def run(self): """Run the GUI application.""" # Set up cleanup on window close self.root.protocol("WM_DELETE_WINDOW", self._on_closing) self.root.mainloop() def _on_closing(self): """Handle application closing.""" # Exit fullscreen mode if self.fullscreen_mode: self.exit_fullscreen() # Stop video playback if self.video_playing: self.stop_video() # Stop image playback if self.is_playing: self.stop_play() # Close the application self.root.destroy() def main(): """Main application entry point.""" if not HAS_GUI: return try: # Install required packages if missing try: import cv2 except ImportError: print("Installing opencv-python...") subprocess.run([sys.executable, '-m', 'pip', 'install', 'opencv-python']) app = NASADownloaderGUI() app.run() except Exception as e: print(f"โŒ Error starting GUI: {e}") import traceback traceback.print_exc() if __name__ == "__main__": main()