поламалаь с новой логикой, чиним
All checks were successful
Deploy timelaps / deploy (push) Successful in 6s
All checks were successful
Deploy timelaps / deploy (push) Successful in 6s
This commit is contained in:
@@ -7,10 +7,14 @@ from .models import TimelapseJob
|
||||
|
||||
|
||||
class TimelapseJobCreateForm(forms.ModelForm):
|
||||
"""Форма создания задания таймлапса из UI."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Подставляет дефолтный период: последние 7 дней до текущей даты."""
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.is_bound:
|
||||
today = timezone.localdate()
|
||||
# Период по умолчанию: неделя до текущего дня включительно.
|
||||
self.initial.setdefault('date_to', today)
|
||||
self.initial.setdefault('date_from', today - timedelta(days=7))
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ logger = logging.getLogger('camlaps')
|
||||
|
||||
|
||||
def _iter_dates(date_from: date, date_to: date):
|
||||
"""Итерируется по датам включительно в указанном диапазоне."""
|
||||
current = date_from
|
||||
while current <= date_to:
|
||||
yield current
|
||||
@@ -20,6 +21,7 @@ def _iter_dates(date_from: date, date_to: date):
|
||||
|
||||
|
||||
def _safe_camera_root(job: TimelapseJob) -> Path:
|
||||
"""Возвращает безопасный путь к каталогу камеры внутри STORAGE_PATH."""
|
||||
storage_root = Path(settings.STORAGE_PATH).resolve()
|
||||
camera_root = (storage_root / job.camera.storage_path).resolve()
|
||||
|
||||
@@ -38,6 +40,7 @@ def _time_to_minutes(t: time) -> int:
|
||||
|
||||
|
||||
def _pick_nearest_frame_for_day(job: TimelapseJob, day_files: list[Path]) -> Path | None:
|
||||
"""Выбирает кадр дня, ближайший к anchor_time с учетом фильтра дня/ночи."""
|
||||
candidates: list[tuple[int, Path]] = []
|
||||
anchor_minutes = _time_to_minutes(job.anchor_time)
|
||||
|
||||
@@ -64,16 +67,46 @@ def _pick_nearest_frame_for_day(job: TimelapseJob, day_files: list[Path]) -> Pat
|
||||
|
||||
|
||||
def _select_frames(job: TimelapseJob, camera_root: Path) -> tuple[list[Path], int, int]:
|
||||
"""Подбирает кадры и возвращает (кадры, всего_дней, дней_с_кадрами)."""
|
||||
logger.info('worker:select_frames:start job_id=%s', job.id)
|
||||
selected: list[Path] = []
|
||||
interval_seconds = int(job.sampling_interval_minutes) * 60
|
||||
last_selected_dt: datetime | None = None
|
||||
days_total = 0
|
||||
days_with_frames = 0
|
||||
|
||||
# Для 1 кадра/сутки берем ближайший кадр к якорному времени в каждом дне.
|
||||
if int(job.sampling_interval_minutes) == 1440:
|
||||
for day in _iter_dates(job.date_from, job.date_to):
|
||||
days_total += 1
|
||||
day_dir = camera_root / day.isoformat()
|
||||
if not day_dir.exists() or not day_dir.is_dir():
|
||||
continue
|
||||
|
||||
day_files = sorted(day_dir.glob('*.jpg'))
|
||||
picked = _pick_nearest_frame_for_day(job, day_files)
|
||||
if picked is not None:
|
||||
selected.append(picked)
|
||||
days_with_frames += 1
|
||||
|
||||
logger.info(
|
||||
'worker:select_frames:done job_id=%s selected=%s days_total=%s days_with_frames=%s mode=daily_anchor',
|
||||
job.id,
|
||||
len(selected),
|
||||
days_total,
|
||||
days_with_frames,
|
||||
)
|
||||
return selected, days_total, days_with_frames
|
||||
|
||||
# Для остальных интервалов применяем шаг по времени по общей временной оси.
|
||||
interval_seconds = int(job.sampling_interval_minutes) * 60
|
||||
last_selected_dt: datetime | None = None
|
||||
|
||||
for day in _iter_dates(job.date_from, job.date_to):
|
||||
days_total += 1
|
||||
day_dir = camera_root / day.isoformat()
|
||||
if not day_dir.exists() or not day_dir.is_dir():
|
||||
continue
|
||||
|
||||
day_has_selected = False
|
||||
day_files = sorted(day_dir.glob('*.jpg'))
|
||||
for img_path in day_files:
|
||||
if img_path.name.lower() == 'lastsnap.jpg':
|
||||
@@ -91,9 +124,19 @@ def _select_frames(job: TimelapseJob, camera_root: Path) -> tuple[list[Path], in
|
||||
if last_selected_dt is None or (current_dt - last_selected_dt).total_seconds() >= interval_seconds:
|
||||
selected.append(img_path)
|
||||
last_selected_dt = current_dt
|
||||
day_has_selected = True
|
||||
|
||||
logger.info('worker:select_frames:done job_id=%s selected=%s', job.id, len(selected))
|
||||
return selected
|
||||
if day_has_selected:
|
||||
days_with_frames += 1
|
||||
|
||||
logger.info(
|
||||
'worker:select_frames:done job_id=%s selected=%s days_total=%s days_with_frames=%s mode=interval',
|
||||
job.id,
|
||||
len(selected),
|
||||
days_total,
|
||||
days_with_frames,
|
||||
)
|
||||
return selected, days_total, days_with_frames
|
||||
|
||||
|
||||
def _copy_frames_to_temp(job: TimelapseJob, frame_paths: list[Path], temp_dir: Path):
|
||||
|
||||
@@ -59,6 +59,7 @@ def camera_preview(request, camera_id: int):
|
||||
|
||||
|
||||
def job_list(request):
|
||||
"""Отображает таблицу задач и признак активности очереди для автообновления."""
|
||||
qs = TimelapseJob.objects.select_related('camera').all()
|
||||
camera_id = request.GET.get('camera')
|
||||
if camera_id:
|
||||
@@ -78,6 +79,7 @@ def job_list(request):
|
||||
|
||||
|
||||
def worker_logs(request):
|
||||
"""Показывает последние строки лога воркера в интерфейсе."""
|
||||
logger.info('logs:view:start')
|
||||
log_file = settings.LOG_DIR / 'camlaps.log'
|
||||
lines = []
|
||||
@@ -106,6 +108,7 @@ def clear_worker_logs(request):
|
||||
|
||||
@require_POST
|
||||
def start_queue(request):
|
||||
"""Запускает воркер одним проходом для следующей задачи из очереди."""
|
||||
logger.info('queue:start_request:start')
|
||||
|
||||
has_planned = TimelapseJob.objects.filter(status=TimelapseJob.Status.PLANNED).exists()
|
||||
@@ -135,6 +138,7 @@ def start_queue(request):
|
||||
|
||||
@require_POST
|
||||
def retry_job(request, job_id: int):
|
||||
"""Сбрасывает задачу в planned для повторного запуска после ошибки."""
|
||||
logger.info('job:retry:start job_id=%s', job_id)
|
||||
job = get_object_or_404(TimelapseJob, pk=job_id)
|
||||
|
||||
@@ -194,6 +198,7 @@ def job_detail(request, job_id: int):
|
||||
|
||||
@require_POST
|
||||
def run_job_now(request, job_id: int):
|
||||
"""Немедленно запускает конкретную задачу через management command."""
|
||||
logger.info('job:run_now:start job_id=%s', job_id)
|
||||
job = get_object_or_404(TimelapseJob, pk=job_id)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user