296 lines
9.7 KiB
Python
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)
|