import logging import subprocess import sys from django.conf import settings from django.http import FileResponse, Http404 from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.views.decorators.http import require_POST from .models import Camera, TimelapseJob from .services.cameras import ( create_cameras_from_candidates, discover_camera_candidates, get_camera_lastsnap_path, is_storage_available, list_active_cameras, ) logger = logging.getLogger('camlaps') def index(request): cameras = list_active_cameras() context = { 'cameras': cameras, 'storage_available': is_storage_available(), 'added': request.GET.get('added'), } return render(request, 'camlaps/index.html', context) @require_POST def discover_cameras(request): cameras = list_active_cameras() candidates = discover_camera_candidates() context = { 'cameras': cameras, 'storage_available': is_storage_available(), 'discovered_candidates': candidates, 'discovered_count': len(candidates), } return render(request, 'camlaps/index.html', context) @require_POST def apply_discovered_cameras(request): selected_paths = request.POST.getlist('selected_storage_paths') created = create_cameras_from_candidates(selected_paths) return redirect(f"{reverse('camlaps:index')}?added={created}") def camera_preview(request, camera_id: int): camera = get_object_or_404(Camera, pk=camera_id, is_active=True) path = get_camera_lastsnap_path(camera) if not path: raise Http404 return FileResponse(path.open('rb'), content_type='image/jpeg') def job_list(request): qs = TimelapseJob.objects.select_related('camera').all() camera_id = request.GET.get('camera') if camera_id: qs = qs.filter(camera_id=camera_id) jobs = qs.order_by('-created_at')[:200] return render(request, 'camlaps/job_list.html', {'jobs': jobs, 'queue_started': request.GET.get('started')}) @require_POST def start_queue(request): logger.info('queue:start_request:start') has_planned = TimelapseJob.objects.filter(status=TimelapseJob.Status.PLANNED).exists() if not has_planned: logger.info('queue:start_request:done no_planned_jobs=true') return redirect(f"{reverse('camlaps:job_list')}?started=none") cmd = [sys.executable, 'manage.py', 'run_timelapse_worker', '--once'] kwargs = { 'cwd': settings.BASE_DIR, 'stdout': subprocess.DEVNULL, 'stderr': subprocess.DEVNULL, } if sys.platform.startswith('win'): kwargs['creationflags'] = subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP try: subprocess.Popen(cmd, **kwargs) logger.info('queue:start_request:done worker_started=true') return redirect(f"{reverse('camlaps:job_list')}?started=worker") except Exception: logger.exception('queue:start_request:error') return redirect(f"{reverse('camlaps:job_list')}?started=error") def job_detail(request, job_id: int): job = get_object_or_404(TimelapseJob.objects.select_related('camera'), pk=job_id) video_url = None if job.output_rel_path: rel = job.output_rel_path.lstrip('/') if rel.startswith('timelapses/'): video_url = '/' + rel else: video_url = '/timelapses/' + rel.split('/')[-1] return render(request, 'camlaps/job_detail.html', {'job': job, 'video_url': video_url}) def job_create(request, camera_id: int): from .forms import TimelapseJobCreateForm camera = get_object_or_404(Camera, pk=camera_id, is_active=True) if request.method == 'POST': form = TimelapseJobCreateForm(request.POST) if form.is_valid(): job = form.save(commit=False) job.camera = camera job.save() return redirect(reverse('camlaps:job_detail', kwargs={'job_id': job.id})) else: form = TimelapseJobCreateForm() sampling_choices = TimelapseJob.SamplingPreset.choices sampling_max = len(TimelapseJob.SAMPLING_PRESET_MINUTES) - 1 return render( request, 'camlaps/job_create.html', { 'camera': camera, 'form': form, 'sampling_choices': sampling_choices, 'sampling_max': sampling_max, }, )