поламалаь с новой логикой, чиним
All checks were successful
Deploy timelaps / deploy (push) Successful in 6s

This commit is contained in:
ack
2026-04-19 22:59:40 +03:00
parent 035cbac430
commit 587cb0bcb4
3 changed files with 54 additions and 2 deletions

View File

@@ -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))

View File

@@ -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] = []
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):

View File

@@ -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)