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