""" Модель для управления процессом создания таймлапса 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"