fuck yeah!
This commit is contained in:
parent
ccb53d9091
commit
da10f5e132
44 changed files with 3260 additions and 448 deletions
370
views/timelapse_dialog.py
Normal file
370
views/timelapse_dialog.py
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
"""
|
||||
Диалог для создания таймлапс-анимации
|
||||
"""
|
||||
|
||||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue