Files
2026-08-30 05:16:30 +00:00

233 lines
7.5 KiB
Python

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/<int:project_id>', 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/<int:project_id>/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/<int:project_id>/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/<int:project_id>/image/<filename>')
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/<int:project_id>/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)