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/', 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/') 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/', 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/', 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/') 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/', 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)