HelioParser/models/timelapse_model.py

227 lines
8.4 KiB
Python
Raw Normal View History

2026-06-10 17:33:12 +03:00
"""
Модель для управления процессом создания таймлапса
Single Responsibility: только данные и состояние процесса таймлапса
"""
from PySide6.QtCore import QObject, Signal, QThread
from datetime import datetime, timedelta
from pathlib import Path
from typing import List, Optional, Callable
from dataclasses import dataclass, field
from enum import Enum
class TimelapseStatus(Enum):
"""Статус процесса создания таймлапса"""
IDLE = "idle"
PREPARING = "preparing"
DOWNLOADING = "downloading"
PROCESSING = "processing"
CREATING_VIDEO = "creating_video"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
@dataclass
class TimelapseConfig:
"""Конфигурация таймлапса"""
source_id: int
start_date: datetime
end_date: datetime
output_path: Path
fps: int = 10
quality: int = 90 # качество видео (0-100)
include_metadata: bool = True
output_format: str = "mp4" # mp4, gif, webm
@dataclass
class TimelapseProgress:
"""Прогресс создания таймлапса"""
status: TimelapseStatus
current: int = 0
total: int = 0
message: str = ""
current_date: Optional[datetime] = None
downloaded_files: List[Path] = field(default_factory=list)
class TimelapseModel(QObject):
"""
Модель для управления созданием таймлапса
Хранит состояние и предоставляет интерфейс для контроллера
"""
# Сигналы для оповещения о изменениях
progress_updated = Signal(TimelapseProgress)
status_changed = Signal(TimelapseStatus)
log_message = Signal(str)
def __init__(self):
super().__init__()
self._current_progress = TimelapseProgress(status=TimelapseStatus.IDLE)
self._config: Optional[TimelapseConfig] = None
self._is_cancelled = False
def configure(self, config: TimelapseConfig) -> bool:
"""
Настраивает таймлапс
Args:
config: Конфигурация таймлапса
Returns:
True если конфигурация валидна
"""
# Валидация параметров
if config.start_date >= config.end_date:
self.log_message.emit("Ошибка: Дата начала должна быть раньше даты окончания")
return False
if config.fps < 1 or config.fps > 60:
self.log_message.emit("Ошибка: FPS должен быть в диапазоне 1-60")
return False
if not config.output_path.parent.exists():
self.log_message.emit(f"Ошибка: Папка {config.output_path.parent} не существует")
return False
self._config = config
self._is_cancelled = False
# Рассчитываем общее количество кадров
delta = config.end_date - config.start_date
total_frames = delta.days + 1
self._current_progress = TimelapseProgress(
status=TimelapseStatus.PREPARING,
total=total_frames,
message=f"Подготовка к созданию таймлапса из {total_frames} кадров"
)
self.log_message.emit(f"Настроен таймлапс: {total_frames} кадров, {config.fps} FPS")
return True
def get_total_frames(self) -> int:
"""Возвращает общее количество кадров"""
if self._config:
delta = self._config.end_date - self._config.start_date
return delta.days + 1
return 0
def update_progress(self, current_frame: int, total_frames: int,
message: str = "", current_date: datetime = None):
"""
Обновляет прогресс создания таймлапса
Args:
current_frame: Текущий кадр
total_frames: Всего кадров
message: Сообщение о прогрессе
current_date: Текущая обрабатываемая дата
"""
self._current_progress.current = current_frame
self._current_progress.total = total_frames
self._current_progress.message = message or self._current_progress.message
self._current_progress.current_date = current_date or self._current_progress.current_date
# Вычисляем процент
if total_frames > 0:
percent = (current_frame / total_frames) * 100
status_text = f"{percent:.1f}%"
self.progress_updated.emit(self._current_progress)
def set_status(self, status: TimelapseStatus, message: str = ""):
"""
Устанавливает статус процесса
Args:
status: Новый статус
message: Сообщение (опционально)
"""
self._current_progress.status = status
if message:
self._current_progress.message = message
self.status_changed.emit(status)
self.progress_updated.emit(self._current_progress)
self.log_message.emit(message or f"Статус: {status.value}")
def add_downloaded_file(self, filepath: Path):
"""Добавляет скачанный файл в список"""
self._current_progress.downloaded_files.append(filepath)
def get_downloaded_files(self) -> List[Path]:
"""Возвращает список скачанных файлов"""
return self._current_progress.downloaded_files.copy()
def cancel(self):
"""Отменяет создание таймлапса"""
self._is_cancelled = True
self.set_status(TimelapseStatus.CANCELLED, "Создание таймлапса отменено пользователем")
def is_cancelled(self) -> bool:
"""Проверяет, отменен ли процесс"""
return self._is_cancelled
def get_config(self) -> Optional[TimelapseConfig]:
"""Возвращает конфигурацию таймлапса"""
return self._config
def reset(self):
"""Сбрасывает состояние модели"""
self._config = None
self._is_cancelled = False
self._current_progress = TimelapseProgress(status=TimelapseStatus.IDLE)
self.progress_updated.emit(self._current_progress)
def generate_date_sequence(self) -> List[datetime]:
"""
Генерирует последовательность дат для таймлапса
Returns:
Список дат для каждого кадра
"""
if not self._config:
return []
dates = []
current = self._config.start_date.replace(hour=12, minute=0, second=0)
while current <= self._config.end_date:
dates.append(current)
current += timedelta(days=1)
return dates
def get_estimated_size(self) -> int:
"""
Оценивает примерный размер видео в байтах
Returns:
Оценочный размер или 0 если невозможно оценить
"""
if not self._config:
return 0
# Приблизительная оценка: ~500KB на кадр для HD видео
total_frames = self.get_total_frames()
estimated_bytes = total_frames * 500 * 1024
# Корректируем в зависимости от FPS и качества
estimated_bytes = estimated_bytes * (self._config.fps / 30) * (self._config.quality / 100)
return int(estimated_bytes)
def get_formatted_estimated_size(self) -> str:
"""Возвращает отформатированную оценку размера"""
size_bytes = self.get_estimated_size()
if size_bytes < 1024:
return f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
return f"{size_bytes / 1024:.1f} KB"
elif size_bytes < 1024 * 1024 * 1024:
return f"{size_bytes / (1024 * 1024):.1f} MB"
else:
return f"{size_bytes / (1024 * 1024 * 1024):.2f} GB"