from flask import Flask, render_template, request, jsonify, send_file, send_from_directory from datetime import datetime from pathlib import Path import subprocess import re import os from database import init_db, add_project, get_all_projects, get_project, update_last_capture, delete_project from scheduler import scheduler, start_job, stop_job, start_all_jobs app = Flask(__name__) CAPTURES_DIR = Path('captures') TIMELAPSES_DIR = Path('timelapses') # Ensure directories exist CAPTURES_DIR.mkdir(exist_ok=True) TIMELAPSES_DIR.mkdir(exist_ok=True) @app.route('/') def index(): return render_template('index.html') @app.route('/api/projects', methods=['GET']) def list_projects(): projects = get_all_projects() result = [] for p in projects: project_id, name, ip, port, period, username, password, created, last_capture, active = p # Count images and get last image safe_name = re.sub(r'[^a-zA-Z0-9_-]', '_', name) capture_folder = CAPTURES_DIR / safe_name image_count = 0 last_image = None if capture_folder.exists(): images = sorted(capture_folder.glob('*.jpg')) image_count = len(images) if images: last_image = f"/api/projects/{project_id}/image/latest" # Calculate runtime created_dt = datetime.strptime(created, '%Y-%m-%d %H:%M:%S') runtime = datetime.now() - created_dt result.append({ 'id': project_id, 'name': name, 'ip_address': ip, 'port': port, 'period_minutes': period, 'created': created, 'runtime_hours': round(runtime.total_seconds() / 3600, 2), 'image_count': image_count, 'last_capture': last_capture, 'has_auth': bool(username and password), 'last_image': last_image }) return jsonify(result) @app.route('/api/projects', methods=['POST']) def create_project(): data = request.json name = data.get('name', '').strip() ip_address = data.get('ip_address', '').strip() port = data.get('port') period_minutes = data.get('period_minutes') username = data.get('username', '').strip() or None password = data.get('password', '').strip() or None # Basic validation if not name or not ip_address or not port or not period_minutes: return jsonify({'error': 'All required fields must be filled'}), 400 # Validate IP address format ip_pattern = r'^(\d{1,3}\.){3}\d{1,3}$' if not re.match(ip_pattern, ip_address): return jsonify({'error': 'Invalid IP address format'}), 400 # Validate port if not (1 <= port <= 65535): return jsonify({'error': 'Port must be between 1 and 65535'}), 400 # Create project in database project_id = add_project(name, ip_address, port, period_minutes, username, password) if not project_id: return jsonify({'error': 'Project name already exists'}), 400 # Start scheduler job start_job(project_id, ip_address, port, period_minutes, username, password, name) return jsonify({'success': True, 'project_id': project_id}) @app.route('/api/projects/', methods=['DELETE']) def remove_project(project_id): project = get_project(project_id) if not project: return jsonify({'error': 'Project not found'}), 404 stop_job(project_id) delete_project(project_id) return jsonify({'success': True}) @app.route('/api/projects//image/latest') def get_latest_image(project_id): """Serve the latest captured image""" project = get_project(project_id) if not project: return jsonify({'error': 'Project not found'}), 404 _, name, _, _, _, _, _, _, _, _ = project safe_name = re.sub(r'[^a-zA-Z0-9_-]', '_', name) capture_folder = CAPTURES_DIR / safe_name if not capture_folder.exists(): return jsonify({'error': 'No images found'}), 404 images = sorted(capture_folder.glob('*.jpg')) if not images: return jsonify({'error': 'No images found'}), 404 return send_file(images[-1], mimetype='image/jpeg') @app.route('/api/projects//gallery') def get_gallery(project_id): """Get list of images for gallery (last 50)""" project = get_project(project_id) if not project: return jsonify({'error': 'Project not found'}), 404 _, name, _, _, _, _, _, _, _, _ = project safe_name = re.sub(r'[^a-zA-Z0-9_-]', '_', name) capture_folder = CAPTURES_DIR / safe_name if not capture_folder.exists(): return jsonify({'error': 'No images found'}), 404 images = sorted(capture_folder.glob('*.jpg'), reverse=True)[:50] result = [] for img in images: stat = img.stat() result.append({ 'filename': img.name, 'path': f"/api/projects/{project_id}/image/{img.name}", 'size': stat.st_size, 'created': datetime.fromtimestamp(stat.st_ctime).isoformat() }) return jsonify(result) @app.route('/api/projects//image/') def get_image(project_id, filename): """Serve a specific image""" project = get_project(project_id) if not project: return jsonify({'error': 'Project not found'}), 404 _, name, _, _, _, _, _, _, _, _ = project safe_name = re.sub(r'[^a-zA-Z0-9_-]', '_', name) capture_folder = CAPTURES_DIR / safe_name return send_from_directory(str(capture_folder), filename, mimetype='image/jpeg') @app.route('/api/projects//timelapse', methods=['POST']) def create_timelapse(project_id): project = get_project(project_id) if not project: return jsonify({'error': 'Project not found'}), 404 _, name, _, _, _, _, _, _, _, _ = project safe_name = re.sub(r'[^a-zA-Z0-9_-]', '_', name) capture_folder = CAPTURES_DIR / safe_name if not capture_folder.exists(): return jsonify({'error': 'No captures found'}), 404 # Get all images sorted by name images = sorted(capture_folder.glob('*.jpg')) if len(images) < 2: return jsonify({'error': 'Need at least 2 images to create timelapse'}), 400 # Create video filename timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') video_filename = f"{safe_name}_{timestamp}.mp4" video_path = TIMELAPSES_DIR / video_filename # Create image list file for ffmpeg list_file = capture_folder / 'images.txt' with open(list_file, 'w') as f: for img in images: f.write(f"file '{img.absolute()}'\n") # Run ffmpeg to create timelapse ffmpeg_cmd = [ 'ffmpeg', '-y', '-f', 'concat', '-safe', '0', '-i', str(list_file), '-vf', 'fps=30', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', str(video_path) ] result = subprocess.run(ffmpeg_cmd, capture_output=True, text=True) if result.returncode != 0: return jsonify({'error': f'ffmpeg failed: {result.stderr}'}), 500 # Send file for download return send_file( video_path, mimetype='video/mp4', as_attachment=True, download_name=video_filename ) if __name__ == '__main__': init_db() # Start scheduler scheduler.start() # Load existing projects and restart their jobs projects = get_all_projects() start_all_jobs(projects) app.run(host='0.0.0.0', port=5800, debug=True, use_reloader=False)