Files
2026-08-30 04:34:16 +00:00

296 lines
9.7 KiB
Python

from flask import Flask, render_template, request, jsonify, send_file, flash, redirect, url_for
from database import init_db, add_camera, get_cameras, delete_camera, add_recording, get_recordings
from apscheduler.schedulers.background import BackgroundScheduler
from datetime import datetime, timedelta
import requests
from requests.auth import HTTPBasicAuth
import os
import logging
app = Flask(__name__)
app.secret_key = 'your-secret-key-here' # Change this!
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Scheduler for auto-stopping recordings
scheduler = BackgroundScheduler()
scheduler.start()
# Track active recordings: {recording_id: {'camera_id': id, 'stop_time': datetime}}
active_recordings = {}
RECORDINGS_DIR = 'recordings'
os.makedirs(RECORDINGS_DIR, exist_ok=True)
def make_camera_request(camera, endpoint, params=None, stream=False):
"""Make authenticated request to Axis camera API"""
url = f"http://{camera['ip']}:{camera['port']}{endpoint}"
try:
response = requests.get(
url,
auth=HTTPBasicAuth(camera['username'], camera['password']),
params=params,
timeout=30,
stream=stream
)
response.raise_for_status()
return response
except requests.RequestException as e:
logger.error(f"Camera request failed: {e}")
raise
def start_recording_on_camera(camera):
"""Start recording on camera and return recording ID"""
response = make_camera_request(camera, '/axis-cgi/record/record.cgi',
{'diskid': 'SD_DISK'})
# The response typically contains the recording ID
# Parse it from the response text
return response.text
def stop_recording_on_camera(camera, recording_id):
"""Stop recording on camera"""
response = make_camera_request(camera, '/axis-cgi/record/stop.cgi',
{'recordingid': recording_id})
return response.text
def export_recording(camera, recording_id, output_path):
"""Download recording from camera"""
response = make_camera_request(
camera,
'/axis-cgi/record/export/exportrecording.cgi',
{'recordingid': recording_id, 'format': 'mp4'},
stream=True
)
with open(output_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
return output_path
def auto_stop_recording(recording_db_id, camera_id, recording_id):
"""Scheduled job to stop recording and export"""
from database import get_camera_by_id, update_recording_status
camera = get_camera_by_id(camera_id)
if not camera:
logger.error(f"Camera {camera_id} not found for stopping recording")
return
try:
# Stop the recording
stop_recording_on_camera(camera, recording_id)
logger.info(f"Stopped recording {recording_id} on camera {camera['ip']}")
# Export the video
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f"camera_{camera_id}_{timestamp}.mp4"
filepath = os.path.join(RECORDINGS_DIR, filename)
export_recording(camera, recording_id, filepath)
logger.info(f"Exported recording to {filepath}")
# Update database
update_recording_status(recording_db_id, 'completed', filepath)
except Exception as e:
logger.error(f"Failed to stop/export recording: {e}")
update_recording_status(recording_db_id, 'failed', None)
@app.route('/')
def index():
cameras = get_cameras()
recordings = get_recordings()
return render_template('index.html', cameras=cameras, recordings=recordings)
@app.route('/cameras/add', methods=['POST'])
def add_camera_route():
ip = request.form.get('ip')
port = request.form.get('port', 80)
username = request.form.get('username')
password = request.form.get('password')
name = request.form.get('name', f'Camera {ip}')
if not all([ip, username, password]):
flash('IP, username, and password are required', 'error')
return redirect(url_for('index'))
try:
add_camera(name, ip, port, username, password)
flash('Camera added successfully', 'success')
except Exception as e:
flash(f'Error adding camera: {e}', 'error')
return redirect(url_for('index'))
@app.route('/cameras/delete/<int:camera_id>', methods=['POST'])
def delete_camera_route(camera_id):
try:
delete_camera(camera_id)
flash('Camera deleted successfully', 'success')
except Exception as e:
flash(f'Error deleting camera: {e}', 'error')
return redirect(url_for('index'))
@app.route('/cameras/test/<int:camera_id>')
def test_camera_route(camera_id):
"""Test connection to camera"""
from database import get_camera_by_id
camera = get_camera_by_id(camera_id)
if not camera:
return jsonify({'success': False, 'message': 'Camera not found'})
try:
response = make_camera_request(camera, '/axis-cgi/record/list.cgi')
return jsonify({'success': True, 'message': 'Connection successful', 'response': response.text[:500]})
except Exception as e:
return jsonify({'success': False, 'message': str(e)})
@app.route('/recordings/start/<int:camera_id>', methods=['POST'])
def start_recording_route(camera_id):
from database import get_camera_by_id
camera = get_camera_by_id(camera_id)
if not camera:
flash('Camera not found', 'error')
return redirect(url_for('index'))
duration_minutes = int(request.form.get('duration', 5))
try:
# Start recording
response = start_recording_on_camera(camera)
logger.info(f"Start recording response: {response}")
# Try to extract recording ID from response
# Axis typically returns something like "recordingid=12345"
recording_id = None
if 'recordingid=' in response:
recording_id = response.split('recordingid=')[1].split()[0].strip()
if not recording_id:
# If we can't get ID immediately, we'll need to poll list.cgi
flash('Recording started (ID pending)', 'warning')
return redirect(url_for('index'))
# Schedule auto-stop
stop_time = datetime.now() + timedelta(minutes=duration_minutes)
# Add to database
recording_db_id = add_recording(
camera_id=camera_id,
recording_id=recording_id,
duration_minutes=duration_minutes,
scheduled_stop=stop_time
)
# Schedule the stop job
scheduler.add_job(
auto_stop_recording,
'date',
run_date=stop_time,
args=[recording_db_id, camera_id, recording_id],
id=f'stop_{recording_db_id}'
)
flash(f'Recording started! Will stop in {duration_minutes} minutes', 'success')
except Exception as e:
flash(f'Error starting recording: {e}', 'error')
logger.error(f"Start recording error: {e}")
return redirect(url_for('index'))
@app.route('/recordings/stop/<int:recording_id>', methods=['POST'])
def stop_recording_route(recording_id):
from database import get_recording_by_id, get_camera_by_id
recording = get_recording_by_id(recording_id)
if not recording or not recording['recording_id']:
flash('Recording not found', 'error')
return redirect(url_for('index'))
camera = get_camera_by_id(recording['camera_id'])
try:
stop_recording_on_camera(camera, recording['recording_id'])
# Cancel scheduled job if exists
try:
scheduler.remove_job(f'stop_{recording_id}')
except:
pass
# Export immediately
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f"camera_{recording['camera_id']}_{timestamp}.mp4"
filepath = os.path.join(RECORDINGS_DIR, filename)
export_recording(camera, recording['recording_id'], filepath)
from database import update_recording_status
update_recording_status(recording_id, 'completed', filepath)
flash('Recording stopped and exported', 'success')
except Exception as e:
flash(f'Error stopping recording: {e}', 'error')
logger.error(f"Stop recording error: {e}")
return redirect(url_for('index'))
@app.route('/recordings/download/<int:recording_id>')
def download_recording_route(recording_id):
from database import get_recording_by_id
recording = get_recording_by_id(recording_id)
if not recording or not recording['file_path']:
flash('Recording file not found', 'error')
return redirect(url_for('index'))
if not os.path.exists(recording['file_path']):
flash('Recording file not found on disk', 'error')
return redirect(url_for('index'))
return send_file(
recording['file_path'],
as_attachment=True,
download_name=os.path.basename(recording['file_path'])
)
@app.route('/recordings/delete/<int:recording_id>', methods=['POST'])
def delete_recording_route(recording_id):
from database import delete_recording
recording = get_recordings()
rec = next((r for r in recording if r['id'] == recording_id), None)
if rec and rec.get('file_path') and os.path.exists(rec['file_path']):
os.remove(rec['file_path'])
delete_recording(recording_id)
flash('Recording deleted', 'success')
return redirect(url_for('index'))
if __name__ == '__main__':
init_db()
app.run(debug=True, host='0.0.0.0', port=5800)