initial commit
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user