233 lines
7.5 KiB
Python
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)
|