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