""" Диалог для создания таймлапс-анимации """ from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QGroupBox, QComboBox, QDateEdit, QPushButton, QLabel, QProgressBar, QFileDialog, QMessageBox, QCheckBox, QTextEdit ) from PySide6.QtCore import Qt, QDateTime, QSettings, QThread, Signal from pathlib import Path from datetime import datetime, timedelta import tempfile import shutil class TimelapseWorker(QThread): progress = Signal(int, int, str) log = Signal(str) finished = Signal(bool, str) def __init__(self, source_id, start_date, end_date, output_path, fps=10): super().__init__() self.source_id = source_id self.start_date = start_date self.end_date = end_date self.output_path = output_path self.fps = fps self._is_cancelled = False def cancel(self): self._is_cancelled = True def run(self): from models.api_model import HelioviewerAPI from utils.video_creator import VideoCreator try: # Создаем временную папку temp_dir = Path(tempfile.mkdtemp(prefix="helioviewer_timelapse_")) self.log.emit(f"Создана временная папка: {temp_dir}") # Генерируем даты - ИСПРАВЛЕНО dates = [] # Проверяем тип и преобразуем в datetime если нужно from datetime import datetime as dt if isinstance(self.start_date, dt): current = self.start_date.replace(hour=12, minute=0, second=0) else: # Если это date, конвертируем в datetime current = dt.combine(self.start_date, dt.min.time()).replace(hour=12) # Проверяем конец if isinstance(self.end_date, dt): end = self.end_date else: end = dt.combine(self.end_date, dt.max.time()) while current <= end: dates.append(current) current += timedelta(days=1) total = len(dates) self.log.emit(f"Всего файлов в очереди: {total}") downloaded = [] for i, date in enumerate(dates): if self._is_cancelled: self.log.emit("Отменено пользователем") shutil.rmtree(temp_dir, ignore_errors=True) self.finished.emit(False, "Отменено") return current_num = i + 1 percent = int(current_num / total * 100) self.progress.emit(current_num, total, f"Скачивание {current_num}/{total} ({percent}%)") self.log.emit(f"Скачивание {current_num}/{total}: {date.strftime('%Y-%m-%d')}") filepath = HelioviewerAPI.download_image(self.source_id, date, temp_dir) if filepath: downloaded.append(filepath) self.log.emit(f" ✓ Успешно") else: self.log.emit(f" ✗ Ошибка") if self._is_cancelled: self.log.emit("Отменено пользователем") shutil.rmtree(temp_dir, ignore_errors=True) self.finished.emit(False, "Отменено") return if not downloaded: shutil.rmtree(temp_dir, ignore_errors=True) self.finished.emit(False, "Не удалось скачать ни одного изображения") return self.log.emit(f"Создание видео из {len(downloaded)} кадров...") self.progress.emit(total, total, "Создание видео...") video_path = VideoCreator.create_timelapse(downloaded, self.output_path, self.fps) # Очищаем временную папку shutil.rmtree(temp_dir, ignore_errors=True) self.log.emit(f"Временные файлы удалены") self.finished.emit(True, str(video_path)) except Exception as e: self.log.emit(f"Ошибка: {str(e)}") self.finished.emit(False, str(e)) class TimelapseDialog(QDialog): """Диалог для создания таймлапса""" def __init__(self, controller, parent=None): super().__init__(parent) self.controller = controller self.settings = QSettings("SolarViewer", "Helioviewer") self.worker = None self.init_ui() self.load_settings() def init_ui(self): """Инициализация UI""" self.setWindowTitle("Создание таймлапс-анимации") self.setModal(True) self.setMinimumWidth(500) layout = QVBoxLayout(self) # Группа выбора спектра spectrum_group = QGroupBox("🌞 Спектральный канал") spectrum_layout = QVBoxLayout(spectrum_group) self.spectrum_combo = QComboBox() self.populate_spectrums() spectrum_layout.addWidget(self.spectrum_combo) layout.addWidget(spectrum_group) # Группа выбора дат date_group = QGroupBox("📅 Период") date_layout = QVBoxLayout(date_group) # Начальная дата start_layout = QHBoxLayout() start_layout.addWidget(QLabel("Начало:")) self.start_date = QDateEdit() self.start_date.setDateTime(QDateTime.currentDateTime().addDays(-7)) self.start_date.setCalendarPopup(True) start_layout.addWidget(self.start_date) date_layout.addLayout(start_layout) # Конечная дата end_layout = QHBoxLayout() end_layout.addWidget(QLabel("Конец:")) self.end_date = QDateEdit() self.end_date.setDateTime(QDateTime.currentDateTime()) self.end_date.setCalendarPopup(True) end_layout.addWidget(self.end_date) date_layout.addLayout(end_layout) layout.addWidget(date_group) # Параметры видео video_group = QGroupBox("Параметры видео") video_layout = QHBoxLayout(video_group) video_layout.addWidget(QLabel("FPS:")) self.fps_spin = QComboBox() self.fps_spin.addItems(['5', '10', '15', '24', '30']) self.fps_spin.setCurrentText('10') video_layout.addWidget(self.fps_spin) video_layout.addStretch() layout.addWidget(video_group) # Выбор папки сохранения folder_group = QGroupBox("Папка сохранения") folder_layout = QVBoxLayout(folder_group) folder_select_layout = QHBoxLayout() self.folder_path = QLabel("Не выбрана") self.folder_path.setStyleSheet("color: gray;") folder_select_layout.addWidget(self.folder_path, 1) browse_button = QPushButton("Обзор...") browse_button.clicked.connect(self.browse_folder) folder_select_layout.addWidget(browse_button) folder_layout.addLayout(folder_select_layout) # Чекбокс "Запомнить путь" self.remember_path_checkbox = QCheckBox("Запомнить этот путь") self.remember_path_checkbox.setChecked(True) folder_layout.addWidget(self.remember_path_checkbox) layout.addWidget(folder_group) # Прогресс бар self.progress_bar = QProgressBar() self.progress_bar.setVisible(False) layout.addWidget(self.progress_bar) # Лог сообщений self.log_text = QTextEdit() self.log_text.setMaximumHeight(150) self.log_text.setReadOnly(True) self.log_text.setVisible(False) layout.addWidget(self.log_text) # Кнопки button_layout = QHBoxLayout() self.create_button = QPushButton("Создать") self.create_button.clicked.connect(self.start_timelapse) button_layout.addWidget(self.create_button) self.cancel_button = QPushButton("Отмена") self.cancel_button.clicked.connect(self.cancel_timelapse) self.cancel_button.setEnabled(False) button_layout.addWidget(self.cancel_button) close_button = QPushButton("Закрыть") close_button.clicked.connect(self.reject) button_layout.addWidget(close_button) layout.addLayout(button_layout) self.selected_folder = None def populate_spectrums(self): """Заполняет список доступных спектров""" from models.api_model import HelioviewerAPI sources = HelioviewerAPI.get_available_sources() for source_id, info in sources.items(): display_text = f"{info['observatory']} - {info['name']} ({info['wavelength']})" self.spectrum_combo.addItem(display_text, source_id) def browse_folder(self): """Выбор папки для сохранения""" folder = QFileDialog.getExistingDirectory(self, "Выберите папку для сохранения") if folder: self.selected_folder = Path(folder) self.folder_path.setText(str(self.selected_folder)) self.folder_path.setStyleSheet("color: green;") if self.remember_path_checkbox.isChecked(): self.save_settings() def save_settings(self): """Сохраняет настройки""" if self.selected_folder: self.settings.setValue("timelapse/last_folder", str(self.selected_folder)) self.settings.setValue("timelapse/remember_path", self.remember_path_checkbox.isChecked()) def load_settings(self): """Загружает настройки""" remember = self.settings.value("timelapse/remember_path", True, type=bool) self.remember_path_checkbox.setChecked(remember) if remember: last_folder = self.settings.value("timelapse/last_folder", "") if last_folder and Path(last_folder).exists(): self.selected_folder = Path(last_folder) self.folder_path.setText(str(self.selected_folder)) self.folder_path.setStyleSheet("color: green;") def start_timelapse(self): """Запускает создание таймлапса""" if not self.selected_folder: QMessageBox.warning(self, "Внимание", "Выберите папку для сохранения") return source_id = self.spectrum_combo.currentData() start_date = self.start_date.date().toPython() end_date = self.end_date.date().toPython() fps = int(self.fps_spin.currentText()) from datetime import datetime as dt # Конвертируем date в datetime с правильным временем start_datetime = dt(start_date.year, start_date.month, start_date.day, 12, 0, 0) end_datetime = dt(end_date.year, end_date.month, end_date.day, 23, 59, 59) days = (end_date - start_date).days + 1 filename = f"timelapse_{source_id}_{start_date.strftime('%Y%m%d')}_to_{end_date.strftime('%Y%m%d')}.mp4" output_path = self.selected_folder / filename self.save_settings() # Обновляем UI self.create_button.setEnabled(False) self.cancel_button.setEnabled(True) self.progress_bar.setVisible(True) self.log_text.setVisible(True) self.progress_bar.setValue(0) self.log_text.clear() self.add_log(f"🚀 Запуск таймлапса: {days} файлов") self.add_log(f"📁 Папка: {self.selected_folder}") # Создаем и запускаем поток self.worker = TimelapseWorker(source_id, start_datetime, end_datetime, output_path, fps) self.worker.progress.connect(self.update_progress) self.worker.log.connect(self.add_log) self.worker.finished.connect(self.on_finished) self.worker.start() def cancel_timelapse(self): """Отмена создания таймлапса""" if self.worker and self.worker.isRunning(): reply = QMessageBox.question( self, "Отмена", "Вы уверены? Все уже скачанные файлы будут удалены.", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: self.worker.cancel() self.add_log("⏹️ Отмена процесса...") self.cancel_button.setEnabled(False) def update_progress(self, current, total, message): """Обновление прогресса""" if total > 0: percent = int((current / total) * 100) self.progress_bar.setValue(percent) def add_log(self, message): """Добавление сообщения в лог""" timestamp = datetime.now().strftime("%H:%M:%S") self.log_text.append(f"[{timestamp}] {message}") self.log_text.verticalScrollBar().setValue( self.log_text.verticalScrollBar().maximum() ) def on_finished(self, success, message): """Обработка завершения""" self.create_button.setEnabled(True) self.cancel_button.setEnabled(False) if success: self.add_log(f"✅ ГОТОВО: {message}") QMessageBox.information(self, "Готово", f"Таймлапс успешно создан!\n{message}") self.accept() else: self.add_log(f"❌ Ошибка: {message}") QMessageBox.critical(self, "Ошибка", message) def closeEvent(self, event): """При закрытии окна""" if self.worker and self.worker.isRunning(): reply = QMessageBox.question( self, "Процесс выполняется", "Создание таймлапса еще не завершено.\n\nЗакрыть окно?", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: self.worker.cancel() event.accept() else: event.ignore() else: event.accept()