HelioParser/views/timelapse_dialog.py
2026-06-10 17:33:12 +03:00

370 lines
No EOL
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Диалог для создания таймлапс-анимации
"""
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()