ripped off from EE-status-v3, changed it to pull list of videos and play recorded mp4s
commit
78d61c122f
|
@ -0,0 +1,14 @@
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
.DS_Store
|
||||||
|
my_settings.py
|
||||||
|
*.orig
|
||||||
|
.lazy_login
|
||||||
|
.lazy_login*
|
||||||
|
.ipynb_checkpoints/
|
||||||
|
.ipynb_checkpoints/*
|
||||||
|
flask_session/
|
||||||
|
git-version.txt
|
||||||
|
settings.py
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
COPY ./requirements.txt /app/requirements.txt
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt update
|
||||||
|
|
||||||
|
RUN pip install --upgrade pip
|
||||||
|
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
|
||||||
|
CMD [ "./startup.sh" ]
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Mark Cotton
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,37 @@
|
||||||
|
# EE-downloader-v3 #
|
||||||
|
|
||||||
|
## Summary ##
|
||||||
|
This is a python webapp for downloading recorder videos from your cameras.
|
||||||
|
|
||||||
|
## Settings File ##
|
||||||
|
There is file `settings.py` that is needed to run. It should look similar to this:
|
||||||
|
|
||||||
|
```
|
||||||
|
config = {
|
||||||
|
|
||||||
|
# Set up your application and get client id/secrete first
|
||||||
|
# https://developerv3.eagleeyenetworks.com/page/my-application
|
||||||
|
"client_id": "",
|
||||||
|
"client_secret": "",
|
||||||
|
|
||||||
|
# you will need to add approved redirect_uris in your application
|
||||||
|
# this examples assumes you've added http://127.0.0.1:3333/login_callback
|
||||||
|
# change the following variables if you did something different
|
||||||
|
# Note: do not use localhost for server_host, use 127.0.0.1 instead
|
||||||
|
"server_protocol": "http",
|
||||||
|
"server_host": "127.0.0.1",
|
||||||
|
"server_port": "3333",
|
||||||
|
"server_path": "login_callback",
|
||||||
|
|
||||||
|
# preferences
|
||||||
|
"log_level": "INFO"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can create your application and setup credentials at: [https://developerv3.eagleeyenetworks.com/page/my-application-html](my applications). You can also reach out to api_support@een.com for help.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Ideas on how to use ##
|
||||||
|
|
||||||
|
I encluded an example Flask server in `app.py`. You can run it locally with `python flask run -p 3333 --debug`
|
|
@ -0,0 +1,339 @@
|
||||||
|
|
||||||
|
import json, requests
|
||||||
|
from flask import Flask, request, session, render_template, redirect, url_for, Response, send_file
|
||||||
|
from flask_session import Session
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
from matplotlib.figure import Figure
|
||||||
|
import matplotlib.dates as mdates
|
||||||
|
import base64
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from EagleEyev3 import *
|
||||||
|
from settings import config
|
||||||
|
|
||||||
|
|
||||||
|
SECRET_KEY = "this needs to be changed to something unique and not checked into git"
|
||||||
|
|
||||||
|
|
||||||
|
# check if it could pull in a config object from settings.py
|
||||||
|
if config:
|
||||||
|
# start checking for keys in config object and set sensible defaults
|
||||||
|
if 'days_of_history' in config:
|
||||||
|
DAYS_OF_HISTORY = config['days_of_history']
|
||||||
|
else:
|
||||||
|
# fallback to a sane default
|
||||||
|
DAYS_OF_HISTORY = 1
|
||||||
|
|
||||||
|
if 'log_level' in config:
|
||||||
|
logger.setLevel(config['log_level'])
|
||||||
|
|
||||||
|
if 'client_secret' in config:
|
||||||
|
SECRET_KEY = config['client_secret']
|
||||||
|
|
||||||
|
else:
|
||||||
|
logging.setLevel(config['INFO'])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
logging.info(f"Using EagleEyev3 version {EagleEyev3.__version__}")
|
||||||
|
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
app.secret_key = SECRET_KEY
|
||||||
|
SESSION_TYPE = 'filesystem'
|
||||||
|
SESSION_PERMANENT = False
|
||||||
|
PERMANENT_SESSION_LIFETIME = 60 * 60 * 24 * 7 # one week in seconds
|
||||||
|
app.config.from_object(__name__)
|
||||||
|
Session(app)
|
||||||
|
|
||||||
|
|
||||||
|
# $ flask routes -s rule
|
||||||
|
# INFO:root:Using EagleEyev3 version 0.0.15
|
||||||
|
# Endpoint Methods Rule
|
||||||
|
# --------------------- ------- ---------------------------
|
||||||
|
# index GET /
|
||||||
|
# accounts GET /accounts
|
||||||
|
# camera_live_preivew GET /camera/<esn>/preview
|
||||||
|
# camera__preivew_image GET /camera/<esn>/preview_image
|
||||||
|
# cameras GET /cameras
|
||||||
|
# landing GET /landing
|
||||||
|
# login_callback GET /login_callback
|
||||||
|
# logout GET /logout
|
||||||
|
# static GET /static/<path:filename>
|
||||||
|
# switch_account GET /switch_account
|
||||||
|
|
||||||
|
|
||||||
|
def login_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
logging.debug(session)
|
||||||
|
if 'een' in session and session['een'].access_token:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
# failed to find a valid session so redirecting to landing page
|
||||||
|
logging.warn('@login_requried failed to find a valid session')
|
||||||
|
logging.info(session)
|
||||||
|
return redirect(url_for('landing'))
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/landing')
|
||||||
|
def landing():
|
||||||
|
|
||||||
|
# sometimes you just want to get to the landing page and want to avoid the index redirect logic
|
||||||
|
if 'een' in session:
|
||||||
|
een = session['een']
|
||||||
|
else:
|
||||||
|
een = EagleEyev3(config)
|
||||||
|
session['een'] = een
|
||||||
|
|
||||||
|
base_url = "https://auth.eagleeyenetworks.com/oauth2/authorize"
|
||||||
|
path_url = f"?client_id={een.client_id}&response_type=code&scope=vms.all&redirect_uri={een.redirect_uri}"
|
||||||
|
values = { "login_link": f"{base_url}{path_url}" }
|
||||||
|
return render_template('cover.html', template_values=values)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
@login_required
|
||||||
|
def index():
|
||||||
|
|
||||||
|
# get een instance from session, @login_required already checked for it
|
||||||
|
een = session['een']
|
||||||
|
|
||||||
|
# using current_user as a proxy for an established valid session
|
||||||
|
if een.access_token == None:
|
||||||
|
base_url = "https://auth.eagleeyenetworks.com/oauth2/authorize"
|
||||||
|
path_url = f"?client_id={een.client_id}&response_type=code&scope=vms.all&redirect_uri={een.redirect_uri}"
|
||||||
|
values = { "login_link": f"{base_url}{path_url}" }
|
||||||
|
return render_template('cover.html', template_values=values)
|
||||||
|
|
||||||
|
# call this to check if session is actually valid
|
||||||
|
check = een.get_current_user()
|
||||||
|
if 'success' not in check or check['success'] == False:
|
||||||
|
base_url = "https://auth.eagleeyenetworks.com/oauth2/authorize"
|
||||||
|
path_url = f"?client_id={een.client_id}&response_type=code&scope=vms.all&redirect_uri={een.redirect_uri}"
|
||||||
|
|
||||||
|
values = { "login_link": f"{base_url}{path_url}" }
|
||||||
|
return render_template('cover.html', template_values=values)
|
||||||
|
else:
|
||||||
|
logging.info(f"{check['success']} - check get_current_user {een.current_user['email']} {een.access_token}")
|
||||||
|
|
||||||
|
|
||||||
|
een.get_list_of_cameras()
|
||||||
|
een.get_list_of_accounts()
|
||||||
|
|
||||||
|
if len(een.accounts) > 0 and een.active_account == None:
|
||||||
|
# they need to pick and account
|
||||||
|
|
||||||
|
logging.info(f"redirecting to accounts.html because they don't have an active account { een.active_account }")
|
||||||
|
|
||||||
|
values = {
|
||||||
|
"current_user": een.current_user,
|
||||||
|
"accounts": een.accounts,
|
||||||
|
"active_account": een.active_account,
|
||||||
|
"user_base_url": een.user_base_url,
|
||||||
|
"access_token": een.access_token
|
||||||
|
}
|
||||||
|
redirect(url_for('accounts'))
|
||||||
|
|
||||||
|
values = {
|
||||||
|
"current_user": een.current_user,
|
||||||
|
"cameras": een.cameras,
|
||||||
|
"camera_count": len(een.cameras),
|
||||||
|
"camera_count_online": len([i for i in een.cameras if i.is_online()]),
|
||||||
|
"camera_count_offline": len([i for i in een.cameras if i.is_offline()]),
|
||||||
|
"accounts": een.accounts,
|
||||||
|
"active_account": een.active_account,
|
||||||
|
"user_base_url": een.user_base_url,
|
||||||
|
"access_token": een.access_token
|
||||||
|
}
|
||||||
|
|
||||||
|
return render_template('index.html', template_values=values)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/login_callback')
|
||||||
|
def login_callback():
|
||||||
|
|
||||||
|
# This is getting the ?code= querystring value from the HTTP request.
|
||||||
|
code = request.args.get('code')
|
||||||
|
|
||||||
|
# create a new een object and store it in their session
|
||||||
|
een = EagleEyev3(config)
|
||||||
|
session['een'] = een
|
||||||
|
|
||||||
|
|
||||||
|
if (code):
|
||||||
|
# use the include code parameter to complete login process
|
||||||
|
oauth_object = een.login_tokens(code)
|
||||||
|
|
||||||
|
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/logout')
|
||||||
|
def logout():
|
||||||
|
|
||||||
|
# logout isn't working APIv3 right now so don't wait for the call to fail before logging out
|
||||||
|
# if 'een' in session:
|
||||||
|
# een = session['een']
|
||||||
|
# een.logout()
|
||||||
|
|
||||||
|
session.pop('een')
|
||||||
|
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/cameras')
|
||||||
|
@login_required
|
||||||
|
def cameras():
|
||||||
|
|
||||||
|
# get een instance from session, @login_required already checked for it
|
||||||
|
een = session['een']
|
||||||
|
|
||||||
|
logging.debug(een.get_list_of_cameras())
|
||||||
|
|
||||||
|
values = {
|
||||||
|
"current_user": een.current_user,
|
||||||
|
"cameras": een.cameras,
|
||||||
|
"camera_count": len(een.cameras),
|
||||||
|
"camera_count_online": len([i for i in een.cameras if i.is_online()]),
|
||||||
|
"camera_count_offline": len([i for i in een.cameras if i.is_offline()])
|
||||||
|
}
|
||||||
|
|
||||||
|
return render_template('cameras_partial.html', template_values=values)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/accounts')
|
||||||
|
@login_required
|
||||||
|
def accounts():
|
||||||
|
|
||||||
|
# get een instance from session, @login_required already checked for it
|
||||||
|
een = session['een']
|
||||||
|
|
||||||
|
logging.debug(een.get_list_of_accounts())
|
||||||
|
|
||||||
|
values = {
|
||||||
|
"current_user": een.current_user,
|
||||||
|
"accounts": een.accounts,
|
||||||
|
"active_account": een.active_account
|
||||||
|
}
|
||||||
|
|
||||||
|
return render_template('accounts_partial.html', template_values=values)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/switch_account')
|
||||||
|
@login_required
|
||||||
|
def switch_account():
|
||||||
|
|
||||||
|
# This is getting the ?account= querystring value from the HTTP request.
|
||||||
|
account = request.args.get('account')
|
||||||
|
|
||||||
|
# get een instance from session, @login_required already checked for it
|
||||||
|
een = session['een']
|
||||||
|
|
||||||
|
if (account):
|
||||||
|
# switch into account
|
||||||
|
logging.info(f"attempting to switch into account {account}")
|
||||||
|
logging.debug(een.login_from_reseller(target_account_id=account))
|
||||||
|
|
||||||
|
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/camera/<esn>/preview_image')
|
||||||
|
@login_required
|
||||||
|
def camera__preivew_image(esn=None):
|
||||||
|
|
||||||
|
# get een instance from session, @login_required already checked for it
|
||||||
|
een = session['een']
|
||||||
|
|
||||||
|
camera = een.get_camera_by_id(esn)
|
||||||
|
res = camera.get_live_preview()
|
||||||
|
|
||||||
|
if res:
|
||||||
|
return send_file(BytesIO(res.content), mimetype='image/jpeg')
|
||||||
|
else:
|
||||||
|
return send_file('static/placeholder.png')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/camera/<esn>/preview')
|
||||||
|
@login_required
|
||||||
|
def camera_live_preivew(esn=None):
|
||||||
|
|
||||||
|
# get een instance from session, @login_required already checked for it
|
||||||
|
een = session['een']
|
||||||
|
|
||||||
|
camera = een.get_camera_by_id(esn)
|
||||||
|
|
||||||
|
values = {
|
||||||
|
"current_user": een.current_user,
|
||||||
|
"camera": camera,
|
||||||
|
"events": camera.events['status']
|
||||||
|
}
|
||||||
|
|
||||||
|
return render_template('camera_preview.html', template_values=values)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/camera/<esn>/video_player/<start_timestamp>')
|
||||||
|
@login_required
|
||||||
|
def camera_video_player(esn=None, start_timestamp=None):
|
||||||
|
|
||||||
|
# get een instance from session, @login_required already checked for it
|
||||||
|
een = session['een']
|
||||||
|
|
||||||
|
camera = een.get_camera_by_id(esn)
|
||||||
|
|
||||||
|
video_url = next(x['mp4Url'] for x in camera.videos if x['startTimestamp'] == start_timestamp)
|
||||||
|
|
||||||
|
values = {
|
||||||
|
"current_user": een.current_user,
|
||||||
|
"camera_name": camera.name,
|
||||||
|
"start_timestamp": start_timestamp,
|
||||||
|
"video_url": video_url
|
||||||
|
}
|
||||||
|
|
||||||
|
return render_template('camera_video_player.html', template_values=values)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/camera/<esn>/videos')
|
||||||
|
@login_required
|
||||||
|
def camera_list_of_videos(esn=None):
|
||||||
|
|
||||||
|
# get een instance from session, @login_required already checked for it
|
||||||
|
een = session['een']
|
||||||
|
|
||||||
|
camera = een.get_camera_by_id(esn)
|
||||||
|
logging.debug(camera.get_list_of_videos(start_timestamp=een.time_before(hours=12), end_timestamp=een.time_now()))
|
||||||
|
|
||||||
|
values = {
|
||||||
|
"current_user": een.current_user,
|
||||||
|
"camera": { 'id': camera.id, 'name': camera.name },
|
||||||
|
"videos": camera.videos
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if 'HX-Request' in request.headers:
|
||||||
|
return render_template('camera_videos_partial.html', template_values=values)
|
||||||
|
else:
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(host=config.server_host, port=config.server_port)
|
|
@ -0,0 +1,13 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
ports:
|
||||||
|
- "9400:3000"
|
||||||
|
restart: always
|
||||||
|
tty: true
|
||||||
|
user: 1000:1000
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
pytz==2023.3
|
||||||
|
requests==2.29.0
|
||||||
|
blinker==1.6.2
|
||||||
|
certifi==2023.5.7
|
||||||
|
charset-normalizer==3.1.0
|
||||||
|
click==8.1.3
|
||||||
|
Flask==2.3.2
|
||||||
|
idna==3.4
|
||||||
|
itsdangerous==2.1.2
|
||||||
|
Jinja2==3.1.2
|
||||||
|
MarkupSafe==2.1.3
|
||||||
|
urllib3==1.26.16
|
||||||
|
Werkzeug==2.3.6
|
||||||
|
gunicorn==20.1.0
|
||||||
|
cachelib==0.10.2
|
||||||
|
Flask-Session==0.5.0
|
||||||
|
EagleEyev3>=0.0.16
|
||||||
|
tqdm
|
||||||
|
pandas
|
||||||
|
numpy
|
||||||
|
matplotlib
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#python3 -u server.py
|
||||||
|
gunicorn --bind 0.0.0.0:3000 --limit-request-line 0 --worker-class=gthread --threads=4 wsgi:app
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
* Globals
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Links */
|
||||||
|
a,
|
||||||
|
a:focus,
|
||||||
|
a:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom default button */
|
||||||
|
.btn-secondary,
|
||||||
|
.btn-secondary:hover,
|
||||||
|
.btn-secondary:focus {
|
||||||
|
color: #333;
|
||||||
|
text-shadow: none; /* Prevent inheritance from `body` */
|
||||||
|
background-color: #fff;
|
||||||
|
border: .05rem solid #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Base structure
|
||||||
|
*/
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5);
|
||||||
|
box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-container {
|
||||||
|
max-width: 42em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Header
|
||||||
|
*/
|
||||||
|
.masthead {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.masthead-brand {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-masthead .nav-link {
|
||||||
|
padding: .25rem 0;
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgba(255, 255, 255, .5);
|
||||||
|
background-color: transparent;
|
||||||
|
border-bottom: .25rem solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-masthead .nav-link:hover,
|
||||||
|
.nav-masthead .nav-link:focus {
|
||||||
|
border-bottom-color: rgba(255, 255, 255, .25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-masthead .nav-link + .nav-link {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-masthead .active {
|
||||||
|
color: #fff;
|
||||||
|
border-bottom-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 48em) {
|
||||||
|
.masthead-brand {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.nav-masthead {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Cover
|
||||||
|
*/
|
||||||
|
.cover {
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
}
|
||||||
|
.cover .btn-lg {
|
||||||
|
padding: .75rem 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Footer
|
||||||
|
*/
|
||||||
|
.mastfoot {
|
||||||
|
color: rgba(255, 255, 255, .5);
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
After Width: | Height: | Size: 1.2 MiB |
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,27 @@
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4 offset-5">
|
||||||
|
<h2>
|
||||||
|
{% for account in template_values['accounts'] %}
|
||||||
|
{% if account.id == template_values['active_account'] %}
|
||||||
|
{{ account.name }}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="col-3">
|
||||||
|
<label >Choose Account:</label>
|
||||||
|
<select onchange="this.options[this.selectedIndex].value && (window.location = this.options[this.selectedIndex].value);">
|
||||||
|
{% for account in template_values['accounts'] %}
|
||||||
|
<option value="/switch_account?account={{ account.id }}"
|
||||||
|
{% if account.id == template_values['active_account'] %}
|
||||||
|
selected
|
||||||
|
{% endif %}
|
||||||
|
title="{{ account.id }}">
|
||||||
|
{{ account.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,121 @@
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="author" content="">
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||||
|
{% block title %}
|
||||||
|
<title>downloader.mcotton.space</title>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<!-- Bootstrap core CSS -->
|
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="//cdn.datatables.net/1.10.19/css/jquery.dataTables.min.css">
|
||||||
|
<link href="/static/dataTables.bootstrap4.min.css">
|
||||||
|
|
||||||
|
<!-- Custom styles for this template -->
|
||||||
|
<!-- <link href="album.css" rel="stylesheet">
|
||||||
|
<link href="custom.css" rel="stylesheet"> -->
|
||||||
|
<link href="/static/style.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- self-hosted fontawesome -->
|
||||||
|
<link href="/static/css/all.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
|
||||||
|
|
||||||
|
<link href="/static/theme.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.row {
|
||||||
|
border: dashed lightgray 1px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.left_text {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{% block style %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse">
|
||||||
|
<ul class="navbar-nav mr-auto mt-2 mt-lg-0">
|
||||||
|
<li class="nav-item active">
|
||||||
|
<a href="/" class="navbar-brand d-flex align-items-center">
|
||||||
|
<small>downloader</small>.mcotton.space
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
<span class="navbar-text">
|
||||||
|
{% block current_user %}
|
||||||
|
{% endblock %}
|
||||||
|
</span>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
|
||||||
|
|
||||||
|
<main role="main">
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="text-muted">
|
||||||
|
<div class="container">
|
||||||
|
<p class="float-right">
|
||||||
|
</p>
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Bootstrap core JavaScript
|
||||||
|
================================================== -->
|
||||||
|
<!-- Placed at the end of the document so the pages load faster -->
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>
|
||||||
|
|
||||||
|
<script src="//cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js"></script>
|
||||||
|
<script src="https://cdn.datatables.net/1.10.19/js/dataTables.bootstrap4.min.js"></script>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/htmx.org@1.9.2"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
|
||||||
|
{% endblock%}
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<h3>Preview Image <i class="bi bi-card-image"></i></h3>
|
||||||
|
<h5>{{ template_values['camera'].name }}</h5>
|
||||||
|
<img src="/camera/{{ template_values['camera'].id }}/live_preview" style="max-height:360px;">
|
|
@ -0,0 +1,46 @@
|
||||||
|
<h3>List of Events <i class="bi bi-calendar-event"></i></h3>
|
||||||
|
<h5>{{ template_values['camera'].name }}</h5>
|
||||||
|
<a href="/camera/{{ template_values['camera'].id }}/events/3" hx-get="/camera/{{ template_values['camera'].id }}/events/3" hx-trigger="click" hx-target="#camera_status_events" hx-indicator=".progress">
|
||||||
|
<button class="btn btn-outline-success">
|
||||||
|
3x<i class="bi bi-calendar-event" title="click to load events list for 3 days"></i>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
<a href="/camera/{{ template_values['camera'].id }}/events/7" hx-get="/camera/{{ template_values['camera'].id }}/events/7" hx-trigger="click" hx-target="#camera_status_events" hx-indicator=".progress">
|
||||||
|
<button class="btn btn-outline-success">
|
||||||
|
7x<i class="bi bi-calendar-event" title="click to load events list for 7 days"></i>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
<a href="/camera/{{ template_values['camera'].id }}/events/14" hx-get="/camera/{{ template_values['camera'].id }}/events/14" hx-trigger="click" hx-target="#camera_status_events" hx-indicator=".progress">
|
||||||
|
<button class="btn btn-outline-success">
|
||||||
|
14x<i class="bi bi-calendar-event" title="click to load events list for 14 days"></i>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
<a href="/camera/{{ template_values['camera'].id }}/status_plot" hx-get="/camera/{{ template_values['camera'].id }}/status_plot" hx-trigger="click" hx-target="#camera_status_plot" hx-indicator=".progress">
|
||||||
|
<button class="btn btn-outline-success">
|
||||||
|
Graph Events <i class="bi bi-bar-chart" title="click to generate graph of events"></i>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
{% if template_values['events'] %}
|
||||||
|
{% for event in template_values['events'] %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-2 left_text">
|
||||||
|
<small>Status:</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-9 offset-1 left_text">
|
||||||
|
<b>{{ event['data'][0]['newStatus']['connectionStatus'] }}</b>
|
||||||
|
</div>
|
||||||
|
<div class="col-2 left_text">
|
||||||
|
<small>End:</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-9 offset-1 left_text">
|
||||||
|
{{ event['endTimestamp'] }}
|
||||||
|
</div>
|
||||||
|
<div class="col-2 left_text">
|
||||||
|
<small>Start:</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-9 offset-1 left_text">
|
||||||
|
{{ event['startTimestamp'] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
|
@ -0,0 +1,3 @@
|
||||||
|
<h3>Preview Image <i class="bi bi-card-image"></i></h3>
|
||||||
|
<h5>{{ template_values['camera'].name }}</h5>
|
||||||
|
<img src="/camera/{{ template_values['camera'].id }}/preview_image" style="max-height:360px; max-width:100%">
|
|
@ -0,0 +1,4 @@
|
||||||
|
<h3>Video Player <i class="bi bi-card-image"></i></h3>
|
||||||
|
<h5>{{ template_values['camera_name'] }}</h5>
|
||||||
|
<video src="{{ template_values['video_url'] }}" autoplay muted preload="auto" controls type="video/mp4" style="max-height:360px; max-width:100%"></video>
|
||||||
|
<br><small>{{ template_values['start_timestamp'] }}</small>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<h3>List of Videos <i class="bi bi-calendar-event"></i></h3>
|
||||||
|
<h5>{{ template_values['camera'].name }}</h5>
|
||||||
|
|
||||||
|
{% if template_values['videos'] %}
|
||||||
|
{% for video in template_values['videos'] %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4 left_text">
|
||||||
|
{{ video['endTimestamp'] }}
|
||||||
|
</div>
|
||||||
|
<div class="col-4 left_text">
|
||||||
|
{{ video['startTimestamp'] }}
|
||||||
|
</div>
|
||||||
|
<div class="col-4 left_text">
|
||||||
|
<a href="/camera/{{ video['deviceId'] }}/video_player/{{ video['startTimestamp'] }}" hx-get="/camera/{{ video['deviceId'] }}/video_player/{{ video['startTimestamp'] }}" hx-trigger="click" hx-target="#camera_detail" hx-indicator=".progress">
|
||||||
|
<button class="btn btn-outline-success">
|
||||||
|
<i class="bi bi-play" title="click to play video"></i>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
document.getElementById('')
|
||||||
|
|
||||||
|
</script>
|
|
@ -0,0 +1,51 @@
|
||||||
|
|
||||||
|
<h3>Cameras <i class="bi bi-camera-video"></i></h3>
|
||||||
|
<h5>Online <small>[{{ template_values['camera_count_online'] }} of {{ template_values['camera_count'] }}]</small></h5>
|
||||||
|
{% for camera in template_values['cameras'] %}
|
||||||
|
{% if camera.is_online() %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-5 col-md-6 offset-1 left_text" style="overflow: hidden;">
|
||||||
|
<span title="ESN: {{ camera.id }}">{{ camera.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6 col-md-5">
|
||||||
|
<a href="/camera/{{ camera.id }}/preview" hx-get="/camera/{{ camera.id }}/preview" hx-trigger="click" hx-target="#camera_detail" hx-indicator=".progress">
|
||||||
|
<button class="btn btn-outline-success">
|
||||||
|
<i class="bi bi-card-image" title="click to load preview image"></i>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
<a href="/camera/{{ camera.id }}/events" hx-get="/camera/{{ camera.id }}/videos" hx-trigger="click" hx-target="#camera_status_events" hx-indicator=".progress">
|
||||||
|
<button class="btn btn-outline-success">
|
||||||
|
<i class="bi bi-calendar-event" title="click to load video list"></i>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<h5>Offline <small>[{{ template_values['camera_count_offline'] }} of {{ template_values['camera_count'] }}]</small></h5>
|
||||||
|
{% for camera in template_values['cameras'] %}
|
||||||
|
{% if camera.is_offline() %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-5 col-md-6 offset-1 left_text" style="overflow: hidden;">
|
||||||
|
<span title="ESN: {{ camera.id }} Status: {{ camera.status['connectionStatus'] }}">{{ camera.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6 col-md-5">
|
||||||
|
<a href="/camera/{{ camera.id }}/preview" hx-get="/camera/{{ camera.id }}/preview" hx-trigger="click" hx-target="#camera_detail" hx-indicator=".progress">
|
||||||
|
<button class="btn btn-outline-success">
|
||||||
|
<i class="bi bi-card-image" title="click to load preview image"></i>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
<a href="/camera/{{ camera.id }}/events" hx-get="/camera/{{ camera.id }}/videos" hx-trigger="click" hx-target="#camera_status_events" hx-indicator=".progress">
|
||||||
|
<button class="btn btn-outline-success">
|
||||||
|
<i class="bi bi-calendar-event" title="click to load video list"></i>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
|
||||||
|
<button hx-get="/cameras" hx-trigger="click" hx-target="#camera_list" class="btn btn-md btn-outline-success" hx-indicator=".progress">refresh <i class="bi bi-arrow-clockwise"></i></button>
|
||||||
|
<br>
|
|
@ -0,0 +1,61 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="author" content="">
|
||||||
|
<link rel="icon" href="/static/favicon.ico">
|
||||||
|
|
||||||
|
<title>downloader.mcotton.space</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap core CSS -->
|
||||||
|
<link href="/static/bootstrap.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom styles for this template -->
|
||||||
|
<link href="/static/cover.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="text-center">
|
||||||
|
|
||||||
|
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
|
||||||
|
<header class="masthead mb-auto">
|
||||||
|
<div class="inner">
|
||||||
|
<h3 class="masthead-brand"><a href="/"><small>downloader</small>.mcotton.space</a></h3>
|
||||||
|
<!-- <nav class="nav nav-masthead justify-content-center">
|
||||||
|
<a class="nav-link active" href="#">Home</a>
|
||||||
|
<a class="nav-link" href="#">Features</a>
|
||||||
|
<a class="nav-link" href="#">Contact</a>
|
||||||
|
</nav>
|
||||||
|
</div> -->
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main role="main" class="inner cover">
|
||||||
|
<h1 class="cover-heading">Camera Video Downloader</h1>
|
||||||
|
<p class="lead">Want to download all your videos? Think that cold storage is really cool? Do you enjoy sorting filenames by ISO date format? Today is your lucky day.</p>
|
||||||
|
<p class="lead">
|
||||||
|
<a href="{{ template_values['login_link'] }}" class="btn btn-lg btn-secondary">Get Started</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- <video src="/static/cover_movie_2.mp4" style="max-width:100%;" autoplay controls loop muted preload > -->
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="mastfoot mt-auto">
|
||||||
|
<div class="inner">
|
||||||
|
<p>This project is open-source and can be found at <a href="https://git.mcotton.space/mcotton/EE-downloader-v3">EE-downloader-v3</a> and uses <a href="https://pypi.org/project/EagleEyev3/">EagleEyev3</a>.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Bootstrap core JavaScript
|
||||||
|
================================================== -->
|
||||||
|
<!-- Placed at the end of the document so the pages load faster -->
|
||||||
|
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
||||||
|
<script>window.jQuery || document.write('<script src="../../assets/js/vendor/jquery-slim.min.js"><\/script>')</script>
|
||||||
|
<script src="../../assets/js/vendor/popper.min.js"></script>
|
||||||
|
<script src="/static/bootstrap.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,177 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block style %}
|
||||||
|
<style>
|
||||||
|
#camera_list_items {
|
||||||
|
max-height: 500px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#events_list {
|
||||||
|
max-height: 500px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
height: 4px;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-clip: padding-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.progress .indeterminate:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
background-color: inherit;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
will-change: left, right;
|
||||||
|
-webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395)
|
||||||
|
infinite;
|
||||||
|
animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
|
||||||
|
}
|
||||||
|
.progress .indeterminate:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
background-color: inherit;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
will-change: left, right;
|
||||||
|
-webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1)
|
||||||
|
infinite;
|
||||||
|
animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1)
|
||||||
|
infinite;
|
||||||
|
-webkit-animation-delay: 1.15s;
|
||||||
|
animation-delay: 1.15s;
|
||||||
|
}
|
||||||
|
.progress {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.htmx-request .progress {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.htmx-request.progress {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
@-webkit-keyframes indeterminate {
|
||||||
|
0% {
|
||||||
|
left: -35%;
|
||||||
|
right: 100%;
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
left: 100%;
|
||||||
|
right: -90%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 100%;
|
||||||
|
right: -90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes indeterminate {
|
||||||
|
0% {
|
||||||
|
left: -35%;
|
||||||
|
right: 100%;
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
left: 100%;
|
||||||
|
right: -90%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 100%;
|
||||||
|
right: -90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-webkit-keyframes indeterminate-short {
|
||||||
|
0% {
|
||||||
|
left: -200%;
|
||||||
|
right: 100%;
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
left: 107%;
|
||||||
|
right: -8%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 107%;
|
||||||
|
right: -8%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes indeterminate-short {
|
||||||
|
0% {
|
||||||
|
left: -200%;
|
||||||
|
right: 100%;
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
left: 107%;
|
||||||
|
right: -8%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 107%;
|
||||||
|
right: -8%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block current_user %}
|
||||||
|
{{ template_values['current_user']['email'] }}
|
||||||
|
<a href="/logout" tabindex="-1">Logout</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
{% if template_values['accounts']|count > 1 %}
|
||||||
|
{% include 'accounts_partial.html' %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="progress" style="height: 3px; background-color: white;">
|
||||||
|
<div class="indeterminate" style="background-color: red;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container text-center">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-5" id="camera_list" style="max-height:400px; overflow:auto;">
|
||||||
|
{% include 'cameras_partial.html' %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-7" id="camera_detail">
|
||||||
|
<h2>Preview Image <i class="bi bi-card-image"></i></h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12" id="camera_status_events" style="max-height:800px; overflow:auto;">
|
||||||
|
<h2>List of Videos <i class="bi bi-calendar-event"></i></h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var requestOptions = {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
"Authorization" : "Bearer {{ template_values['access_token'] }}"
|
||||||
|
},
|
||||||
|
credentials: 'include'
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch("https://{{ template_values['user_base_url'] }}/api/v3.0/media/session", requestOptions)
|
||||||
|
.then(response => response.json() )
|
||||||
|
.then( body => fetch(body.url, requestOptions) )
|
||||||
|
.then( response => console.log("response status", response.status ) )
|
||||||
|
.catch(error => console.log('error', error));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,115 @@
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
from EagleEyev3 import EagleEyev3
|
||||||
|
|
||||||
|
class TestEagleEyev3(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# Create an instance of EagleEyev3 for testing
|
||||||
|
self.eagle_eye = EagleEyev3()
|
||||||
|
|
||||||
|
@patch('EagleEyev3.requests')
|
||||||
|
def test_login_tokens_success(self, mock_requests):
|
||||||
|
# Mock the requests.post method to return a successful response
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = {'access_token': 'token', 'refresh_token': 'refresh_token'}
|
||||||
|
mock_requests.post.return_value = mock_response
|
||||||
|
|
||||||
|
result = self.eagle_eye.login_tokens(code='authorization_code', cascade=True)
|
||||||
|
|
||||||
|
self.assertTrue(result['success'])
|
||||||
|
self.assertEqual(result['response_http_status'], 200)
|
||||||
|
self.assertEqual(result['data']['access_token'], 'token')
|
||||||
|
self.assertEqual(result['data']['refresh_token'], 'refresh_token')
|
||||||
|
|
||||||
|
@patch('EagleEyev3.requests')
|
||||||
|
def test_login_tokens_failure(self, mock_requests):
|
||||||
|
# Mock the requests.post method to return an unsuccessful response
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 400
|
||||||
|
mock_response.json.return_value = {}
|
||||||
|
mock_requests.post.return_value = mock_response
|
||||||
|
|
||||||
|
result = self.eagle_eye.login_tokens(code='authorization_code', cascade=True)
|
||||||
|
|
||||||
|
self.assertFalse(result['success'])
|
||||||
|
self.assertEqual(result['response_http_status'], 400)
|
||||||
|
self.assertEqual(result['data'], {})
|
||||||
|
|
||||||
|
@patch('EagleEyev3.requests')
|
||||||
|
def test_get_base_url_success(self, mock_requests):
|
||||||
|
# Set access_token for testing
|
||||||
|
self.eagle_eye.access_token = 'access_token'
|
||||||
|
|
||||||
|
# Mock the requests.get method to return a successful response
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = {'httpsBaseUrl': {'hostname': 'base_url'}}
|
||||||
|
mock_requests.get.return_value = mock_response
|
||||||
|
|
||||||
|
result = self.eagle_eye.get_base_url(cascade=True)
|
||||||
|
|
||||||
|
self.assertTrue(result['success'])
|
||||||
|
self.assertEqual(result['response_http_status'], 200)
|
||||||
|
self.assertEqual(self.eagle_eye.user_base_url, 'base_url')
|
||||||
|
self.assertIsNotNone(result['data'])
|
||||||
|
|
||||||
|
@patch('EagleEyev3.requests')
|
||||||
|
def test_get_base_url_failure(self, mock_requests):
|
||||||
|
# Set access_token for testing
|
||||||
|
self.eagle_eye.access_token = 'access_token'
|
||||||
|
|
||||||
|
# Mock the requests.get method to return an unsuccessful response
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 400
|
||||||
|
mock_response.json.return_value = {}
|
||||||
|
mock_requests.get.return_value = mock_response
|
||||||
|
|
||||||
|
result = self.eagle_eye.get_base_url(cascade=True)
|
||||||
|
|
||||||
|
self.assertFalse(result['success'])
|
||||||
|
self.assertEqual(result['response_http_status'], 400)
|
||||||
|
self.assertEqual(self.eagle_eye.user_base_url, None)
|
||||||
|
self.assertEqual(result['data'], {})
|
||||||
|
|
||||||
|
@patch('EagleEyev3.requests')
|
||||||
|
def test_get_current_user_success(self, mock_requests):
|
||||||
|
# Set access_token and user_base_url for testing
|
||||||
|
self.eagle_eye.access_token = 'access_token'
|
||||||
|
self.eagle_eye.user_base_url = 'base_url'
|
||||||
|
|
||||||
|
# Mock the requests.get method to return a successful response
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 200
|
||||||
|
mock_response.json.return_value = {'user_id': 'user123'}
|
||||||
|
mock_requests.get.return_value = mock_response
|
||||||
|
|
||||||
|
result = self.eagle_eye.get_current_user()
|
||||||
|
|
||||||
|
self.assertTrue(result['success'])
|
||||||
|
self.assertEqual(result['response_http_status'], 200)
|
||||||
|
self.assertEqual(result['data']['user_id'], 'user123')
|
||||||
|
|
||||||
|
@patch('EagleEyev3.requests')
|
||||||
|
def test_get_current_user_failure(self, mock_requests):
|
||||||
|
# Set access_token and user_base_url for testing
|
||||||
|
self.eagle_eye.access_token = 'access_token'
|
||||||
|
self.eagle_eye.user_base_url = 'base_url'
|
||||||
|
|
||||||
|
# Mock the requests.get method to return an unsuccessful response
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 400
|
||||||
|
mock_response.json.return_value = {}
|
||||||
|
mock_requests.get.return_value = mock_response
|
||||||
|
|
||||||
|
result = self.eagle_eye.get_current_user()
|
||||||
|
|
||||||
|
self.assertFalse(result['success'])
|
||||||
|
self.assertEqual(result['response_http_status'], 400)
|
||||||
|
self.assertEqual(result['data'], {})
|
||||||
|
|
||||||
|
# Write similar tests for the remaining methods
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
Loading…
Reference in New Issue