diff --git a/.gitignore b/.gitignore index 74b6b654..cd1cef74 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,7 @@ conf.json super.json dbdata npm-debug.log -shinobi.sqlite \ No newline at end of file +shinobi.sqlite +package-lock.json +dist +._* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..03c3a634 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +## Contributing license +By contributing to this repository, you agree your contributions to be bound by the [SHINOBI OPEN SOURCE SOFTWARE LICENSE AGREEMENT +](LICENSE.md) + +## Suggestions +For suggestions to this project, please refer to the [README](README.md) + +## Development +For contributing to the repository, please refer to [DEVELOPMENT.md](DEVELOPMENT.md) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 274ae7c9..c82b4ef5 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -15,8 +15,6 @@ any sort of super user access. Prerequisites ============= -> To get all of Shinobi at once you can use the Ninja Way. Learn more about that here -https://shinobi.video/docs/start#content-the-ninja-way - *Node.js* : You'll need Node and NPM, and for this guide we recommend you set up a user-local install of @@ -34,6 +32,11 @@ instance you're working with by copying it. It is recommended to use at least ve You'll also need FFmpeg. This is the video processing engine at the core of Shinobi. You will need at least version 3.3.3. +### Installing prerequisites automatically +To get all of Shinobi at once you can use the Ninja Way. Learn more about that here +https://shinobi.video/docs/start#content-the-ninja-way +However this will download the repository to /home/Shinobi and start Shinobi. Once you have finished the installation using the ninja way, ensure that you stop Shinobi othewise it will confilict with your dev instance. To stop Shinobi run `sudo pm2 stop camera.js` and `sudo pm2 stop cron.js` + Development =========== First off you need to clone the Shinobi repository. Either the regular Shinobi repository or the @@ -56,6 +59,8 @@ git clone https://gitlab.com/Shinobi-Systems/ShinobiCE.git ``` Then cd into either the "Shinobi" or "ShinobiCE" directory. +Make sure to add your fork as a remote so you can send Merge requests + Grabbing packages ----------------- To install the required Node packages you need to install them with NPM: @@ -73,27 +78,7 @@ instances of Shinobi whenever someone updated. ```sh cp conf.sample.json conf.json ``` -Generally for development SQLite is going to be easier with SQLite, as we don't have to maintain -a MariaDB/MySQL installation and we may need to reset the database often. To set the DB to SQLite: -```sh -node tools/modifyConfiguration.js databaseType=sqlite -``` -You can confirm this worked by checking conf.json for the following line -\[near the end of the file\]: -```json -"databaseType": "sqlite" -``` -Currently Shinobi doesn't use a model framework or seeding system, so setting up the database -basically involves copying the pre-set instance bundled with the repository: -```sh -cp sql/shinobi.sample.sqlite shinobi.sqlite -``` - -### Resetting the DB -If you need to reset the database, you can now do so by deleting the shiobi.sqlite file and -copying the sql/shinobi.sample.sqlite file again. Just be sure to stop any running Shinobi -instances before you do so. Enabling the Super User Interface --------------------------------- @@ -113,4 +98,8 @@ and "cron" processes directy. To monitor output, we recommend you use a terminal byobu, tmux, or screen. In one terminal window, run ```node cron.js``` and in another run ```node camera.js```. Shinobi should now be running on port 8080 on your local machine (you can change the port in conf.json) and accessable at http://localhost:8080 in your browser. Any source -code changes you make will require restarting either the camera or cron process [or both]. +code changes you make will require restarting either the camera or cron process [or both]. To avoid manually restarting, use the npm package `nodemon`. Run these commands in two separate terminals. +```sh +npx nodemon server.js +npx nodemon cron.js +``` diff --git a/Docker/README.md b/Docker/README.md new file mode 100644 index 00000000..e46c1df8 --- /dev/null +++ b/Docker/README.md @@ -0,0 +1,138 @@ +# Install Shinobi with Docker + +### There are three ways! + +## Docker Ninja Way + +> This method uses `docker-compose` and has the ability to quick install the TensorFlow Object Detection plugin. + +``` +bash <(curl -s https://gitlab.com/Shinobi-Systems/Shinobi-Installer/raw/master/shinobi-docker.sh) +``` + +## Docker Ninja Way - Version 2 + +#### Installing Shinobi + +> Please remember to check out the Environment Variables table further down this README. + +``` +docker run -d --name='Shinobi' -p '8080:8080/tcp' -v "/dev/shm/Shinobi/streams":'/dev/shm/streams':'rw' -v "$HOME/Shinobi/config":'/config':'rw' -v "$HOME/Shinobi/customAutoLoad":'/home/Shinobi/libs/customAutoLoad':'rw' -v "$HOME/Shinobi/database":'/var/lib/mysql':'rw' -v "$HOME/Shinobi/videos":'/home/Shinobi/videos':'rw' -v "$HOME/Shinobi/plugins":'/home/Shinobi/plugins':'rw' -v '/etc/localtime':'/etc/localtime':'ro' shinobisystems/shinobi:dev +``` + +#### Installing Object Detection (TensorFlow.js) + +> This requires that you add the plugin key to the Shinobi container. This key is generated and displayed in the startup logs of the Object Detection docker container. + +- `-p '8082:8082/tcp'` is an optional flag if you decide to run the plugin in host mode. +- `-e PLUGIN_HOST='10.1.103.113'` Set this as your Shinobi IP Address. +- `-e PLUGIN_PORT='8080'` Set this as your Shinobi Web Port number. + +``` +docker run -d --name='shinobi-tensorflow' -e PLUGIN_HOST='10.1.103.113' -e PLUGIN_PORT='8080' -v "$HOME/Shinobi/docker-plugins/tensorflow":'/config':'rw' shinobisystems/shinobi-tensorflow:latest +``` + +More Information about this plugin : +- CPU : https://gitlab.com/Shinobi-Systems/docker-plugin-tensorflow.js +- GPU (NVIDIA CUDA) : https://gitlab.com/Shinobi-Systems/docker-plugin-tensorflow.js/-/tree/gpu + + +## From Source +> Image is based on Ubuntu Bionic (20.04). Node.js 12 is used. MariaDB and FFmpeg are included. + +1. Download Repo + +``` +git clone -b dev https://gitlab.com/Shinobi-Systems/Shinobi.git ShinobiSource +``` + +2. Enter Repo and Build Image. + +``` +cd ShinobiSource +docker build --tag shinobi-image:1.0 . +``` + +3. Create a container with the image. + +> This command only works on Linux because of the temporary directory used. This location must exist in RAM. `-v "/dev/shm/shinobiStreams":'/dev/shm/streams':'rw'`. The timezone is also acquired from the host by the volume declaration of `-v '/etc/localtime':'/etc/localtime':'ro'`. + +``` +docker run -d --name='Shinobi' -p '8080:8080/tcp' -v "/dev/shm/Shinobi/streams":'/dev/shm/streams':'rw' -v "$HOME/Shinobi/config":'/config':'rw' -v "$HOME/Shinobi/customAutoLoad":'/home/Shinobi/libs/customAutoLoad':'rw' -v "$HOME/Shinobi/database":'/var/lib/mysql':'rw' -v "$HOME/Shinobi/videos":'/home/Shinobi/videos':'rw' -v "$HOME/Shinobi/plugins":'/home/Shinobi/plugins':'rw' -v '/etc/localtime':'/etc/localtime':'ro' shinobi-image:1.0 +``` + + > Host mount paths have been updated in this document. + + ### Volumes + + | Volumes | Description | + |-----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| + | /dev/shm/Shinobi/streams | **IMPORTANT!** This must be mapped to somewhere in the host's RAM. When running this image on Windows you will need to select a different location. | + | $HOME/Shinobi/config | Put `conf.json` or `super.json` files in here to override the default. values. | + | $HOME/Shinobi/customAutoLoad | Maps to the `/home/Shinobi/libs/customAutoLoad` folder for loading your own modules into Shinobi. | + | $HOME/Shinobi/database | A map to `/var/lib/mysql` in the container. This is the database's core files. | + | $HOME/Shinobi/videos | A map to `/home/Shinobi/videos`. The storage location of your recorded videos. | + | $HOME/Shinobi/plugins | A map to `/home/Shinobi/plugins`. Mapped so that plugins can easily be modified or swapped. | + +### Environment Variables + + | Environment Variable | Description | Default | + |----------------------|----------------------------------------------------------------------|--------------------| + | SUBSCRIPTION_ID | **THIS IS NOT REQUIRED**. If you are a subscriber to any of the Shinobi services you may use that key as the value for this parameter. If you have donated by PayPal you may use your Transaction ID to activate the license as well. | *None* | + | DB_USER | Username that the Shinobi process will connect to the database with. | majesticflame | + | DB_PASSWORD | Password that the Shinobi process will connect to the database with. | *None* | + | DB_HOST | Address that the Shinobi process will connect to the database with. | localhost | + | DB_DATABASE | Database that the Shinobi process will interact with. | ccio | + | DB_DISABLE_INCLUDED | Disable included database to use your own. Set to `true` to disable.| false | + | PLUGIN_KEYS | The object containing connection keys for plugins running in client mode (non-host, default). | {} | + | SSL_ENABLED | Enable or Disable SSL. | false | + | SSL_COUNTRY | Country Code for SSL. | CA | + | SSL_STATE | Province/State Code for SSL. | BC | + | SSL_LOCATION | Location of where SSL key is being used. | Vancouver | + | SSL_ORGANIZATION | Company Name associated to key. | Shinobi Systems | + | SSL_ORGANIZATION_UNIT | Department associated to key. | IT Department | + | SSL_COMMON_NAME | Common Name associated to key. | nvr.ninja | + + > You must add (to the docker container) `/config/ssl/server.key` and `/config/ssl/server.cert`. The `/config` folder is mapped to `$HOME/Shinobi/config` on the host by default with the quick run methods. Place `key` and `cert` in `$HOME/Shinobi/config/ssl`. If `SSL_ENABLED=true` and these files don't exist they will be generated with `openssl`. + +> For those using `DB_DISABLE_INCLUDED=true` please remember to create a user in your databse first. The Docker image will create the `DB_DATABASE` under the specified connection information. + +### Tips + +Modifying `conf.json` or Superuser credentials. +> Please read **Volumes** table in this README. conf.json is for general configuration. super.json is for Superuser credential management. + +Get Docker Containers +``` +docker ps -a +``` + +Get Images +``` +docker images +``` + +Container Logs +``` +docker logs /Shinobi +``` + +Enter the Command Line of the Container +``` +docker exec -it /Shinobi /bin/bash +``` + +Stop and Remove +``` +docker stop /Shinobi +docker rm /Shinobi +``` + +**WARNING - DEVELOPMENT ONLY!!!** Kill all Containers and Images +> These commands will completely erase all of your docker containers and images. **You have been warned!** + +``` +docker stop /Shinobi +docker rm $(docker ps -a -f status=exited -q) +docker rmi $(docker images -a -q) +``` diff --git a/Docker/init.sh b/Docker/init.sh new file mode 100644 index 00000000..2e235f6d --- /dev/null +++ b/Docker/init.sh @@ -0,0 +1,105 @@ +#!/bin/sh +set -e + +cp sql/framework.sql sql/framework1.sql +OLD_SQL_USER_TAG="ccio" +NEW_SQL_USER_TAG="$DB_DATABASE" +sed -i "s/$OLD_SQL_USER_TAG/$NEW_SQL_USER_TAG/g" sql/framework1.sql +if [ "$SSL_ENABLED" = "true" ]; then + if [ -d /config/ssl ]; then + echo "Using provided SSL Key" + cp -R /config/ssl ssl + SSL_CONFIG='{"key":"./ssl/server.key","cert":"./ssl/server.cert"}' + else + echo "Making new SSL Key" + mkdir -p ssl + openssl req -nodes -new -x509 -keyout ssl/server.key -out ssl/server.cert -subj "/C=$SSL_COUNTRY/ST=$SSL_STATE/L=$SSL_LOCATION/O=$SSL_ORGANIZATION/OU=$SSL_ORGANIZATION_UNIT/CN=$SSL_COMMON_NAME" + cp -R ssl /config/ssl + SSL_CONFIG='{"key":"./ssl/server.key","cert":"./ssl/server.cert"}' + fi +else + SSL_CONFIG='{}' +fi +if [ "$DB_DISABLE_INCLUDED" = "false" ]; then + echo "MariaDB Directory ..." + ls /var/lib/mysql + + if [ ! -f /var/lib/mysql/ibdata1 ]; then + echo "Installing MariaDB ..." + mysql_install_db --user=mysql --datadir=/var/lib/mysql --silent + fi + echo "Starting MariaDB ..." + /usr/bin/mysqld_safe --user=mysql & + sleep 5s + + chown -R mysql /var/lib/mysql + + if [ ! -f /var/lib/mysql/ibdata1 ]; then + mysql -u root --password="" -e "SET @@SESSION.SQL_LOG_BIN=0; + USE mysql; + DELETE FROM mysql.user ; + DROP USER IF EXISTS 'root'@'%','root'@'localhost','${DB_USER}'@'localhost','${DB_USER}'@'%'; + CREATE USER 'root'@'%' IDENTIFIED BY '${DB_PASS}' ; + CREATE USER 'root'@'localhost' IDENTIFIED BY '${DB_PASS}' ; + CREATE USER '${DB_USER}'@'%' IDENTIFIED BY '${DB_PASS}' ; + CREATE USER '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}' ; + GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION ; + GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION ; + GRANT ALL PRIVILEGES ON *.* TO '${DB_USER}'@'%' WITH GRANT OPTION ; + GRANT ALL PRIVILEGES ON *.* TO '${DB_USER}'@'localhost' WITH GRANT OPTION ; + DROP DATABASE IF EXISTS test ; + FLUSH PRIVILEGES ;" + fi + + # Create MySQL database if it does not exists + if [ -n "${DB_HOST}" ]; then + echo "Wait for MySQL server" ... + while ! mysqladmin ping -h"$DB_HOST"; do + sleep 1 + done + fi + + echo "Setting up MySQL database if it does not exists ..." + + echo "Create database schema if it does not exists ..." + mysql -e "source /home/Shinobi/sql/framework.sql" || true + + echo "Create database user if it does not exists ..." + mysql -e "source /home/Shinobi/sql/user.sql" || true + +else + echo "Create database schema if it does not exists ..." + mysql -u "$DB_USER" -h "$DB_HOST" -p"$DB_PASSWORD" --port="$DB_PORT" -e "source /home/Shinobi/sql/framework.sql" || true +fi + +DATABASE_CONFIG='{"host": "'$DB_HOST'","user": "'$DB_USER'","password": "'$DB_PASSWORD'","database": "'$DB_DATABASE'","port":'$DB_PORT'}' + +cronKey="$(head -c 1024 < /dev/urandom | sha256sum | awk '{print substr($1,1,29)}')" + +cd /home/Shinobi +mkdir -p libs/customAutoLoad +if [ -e "/config/conf.json" ]; then + cp /config/conf.json conf.json +fi +if [ ! -e "./conf.json" ]; then + sudo cp conf.sample.json conf.json +fi +sudo sed -i -e 's/change_this_to_something_very_random__just_anything_other_than_this/'"$cronKey"'/g' conf.json +node tools/modifyConfiguration.js cpuUsageMarker=CPU subscriptionId=$SUBSCRIPTION_ID thisIsDocker=true pluginKeys="$PLUGIN_KEYS" db="$DATABASE_CONFIG" ssl="$SSL_CONFIG" +sudo cp conf.json /config/conf.json + + +echo "=============" +echo "Default Superuser : admin@shinobi.video" +echo "Default Password : admin" +echo "Log in at http://HOST_IP:SHINOBI_PORT/super" +if [ -e "/config/super.json" ]; then + cp /config/super.json super.json +fi +if [ ! -e "./super.json" ]; then + sudo cp super.sample.json super.json + sudo cp super.sample.json /config/super.json +fi +# Execute Command +echo "Starting Shinobi ..." +exec "$@" diff --git a/Docker/pm2.yml b/Docker/pm2.yml new file mode 100644 index 00000000..1b6613d8 --- /dev/null +++ b/Docker/pm2.yml @@ -0,0 +1,7 @@ +apps: + - script : '/home/Shinobi/camera.js' + name : 'camera' + kill_timeout : 5000 + - script : '/home/Shinobi/cron.js' + name : 'cron' + kill_timeout : 5000 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..1f9e379e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,108 @@ +FROM node:12.18.3-buster-slim + +ENV DB_USER=majesticflame \ + DB_PASSWORD='' \ + DB_HOST='localhost' \ + DB_DATABASE=ccio \ + DB_PORT=3306 \ + SUBSCRIPTION_ID=sub_XXXXXXXXXXXX \ + PLUGIN_KEYS='{}' \ + SSL_ENABLED='false' \ + SSL_COUNTRY='CA' \ + SSL_STATE='BC' \ + SSL_LOCATION='Vancouver' \ + SSL_ORGANIZATION='Shinobi Systems' \ + SSL_ORGANIZATION_UNIT='IT Department' \ + SSL_COMMON_NAME='nvr.ninja' \ + DB_DISABLE_INCLUDED=false +ARG DEBIAN_FRONTEND=noninteractive + +RUN mkdir -p /home/Shinobi /config /var/lib/mysql + +RUN apt update -y +RUN apt install wget curl net-tools -y + +# Install MariaDB server... the debian way +RUN if [ "$DB_DISABLE_INCLUDED" = "false" ] ; then set -ex; \ + { \ + echo "mariadb-server" mysql-server/root_password password '${DB_ROOT_PASSWORD}'; \ + echo "mariadb-server" mysql-server/root_password_again password '${DB_ROOT_PASSWORD}'; \ + } | debconf-set-selections; \ + apt-get update; \ + apt-get install -y \ + "mariadb-server" \ + socat \ + ; \ + find /etc/mysql/ -name '*.cnf' -print0 \ + | xargs -0 grep -lZE '^(bind-address|log)' \ + | xargs -rt -0 sed -Ei 's/^(bind-address|log)/#&/'; fi + +RUN if [ "$DB_DISABLE_INCLUDED" = "false" ] ; then sed -ie "s/^bind-address\s*=\s*127\.0\.0\.1$/#bind-address = 0.0.0.0/" /etc/mysql/my.cnf; fi + +# Install FFmpeg + +RUN apt install -y software-properties-common \ + libfreetype6-dev \ + libgnutls28-dev \ + libmp3lame-dev \ + libass-dev \ + libogg-dev \ + libtheora-dev \ + libvorbis-dev \ + libvpx-dev \ + libwebp-dev \ + libssh2-1-dev \ + libopus-dev \ + librtmp-dev \ + libx264-dev \ + libx265-dev \ + yasm && \ + apt install -y \ + build-essential \ + bzip2 \ + coreutils \ + gnutls-bin \ + nasm \ + tar \ + x264 + +RUN apt install -y \ + ffmpeg \ + git \ + make \ + g++ \ + gcc \ + pkg-config \ + python3 \ + wget \ + tar \ + sudo \ + xz-utils + + +WORKDIR /home/Shinobi +COPY . . +RUN rm -rf /home/Shinobiplugins +COPY ./plugins /home/Shinobi/plugins +RUN chmod -R 777 /home/Shinobi/plugins +RUN npm i npm@latest -g && \ + npm install pm2 -g && \ + npm install --unsafe-perm && \ + npm audit fix --force +COPY ./Docker/pm2.yml ./ + +# Copy default configuration files +# COPY ./config/conf.json ./config/super.json /home/Shinobi/ +RUN chmod -f +x /home/Shinobi/Docker/init.sh + +VOLUME ["/home/Shinobi/videos"] +VOLUME ["/home/Shinobi/plugins"] +VOLUME ["/config"] +VOLUME ["/customAutoLoad"] +VOLUME ["/var/lib/mysql"] + +EXPOSE 8080 + +ENTRYPOINT ["/home/Shinobi/Docker/init.sh"] + +CMD [ "pm2-docker", "pm2.yml" ] diff --git a/INSTALL/autoinstall-ubuntu-latest.sh b/INSTALL/autoinstall-ubuntu-latest.sh index 0ae4853b..d4ace473 100644 --- a/INSTALL/autoinstall-ubuntu-latest.sh +++ b/INSTALL/autoinstall-ubuntu-latest.sh @@ -1,5 +1,7 @@ +#!/bin/sh + apt install git -y git clone https://github.com/ShinobiCCTV/Shinobi.git -b dev Shinobi-dev -cd Shinobi-dev +cd Shinobi-dev || exit chmod +x INSTALL/ubuntu-easyinstall.sh && INSTALL/ubuntu-easyinstall.sh bash INSTALL/ubuntu-easyinstall.sh \ No newline at end of file diff --git a/INSTALL/autoinstall-ubuntu-stable.sh b/INSTALL/autoinstall-ubuntu-stable.sh index 6dc12bdd..b7f10245 100644 --- a/INSTALL/autoinstall-ubuntu-stable.sh +++ b/INSTALL/autoinstall-ubuntu-stable.sh @@ -1,5 +1,7 @@ +#!/bin/sh + apt install git -y git clone https://github.com/ShinobiCCTV/Shinobi.git Shinobi -cd Shinobi +cd Shinobi || exit chmod +x INSTALL/ubuntu-easyinstall.sh && INSTALL/ubuntu-easyinstall.sh bash INSTALL/ubuntu-easyinstall.sh \ No newline at end of file diff --git a/INSTALL/centos.sh b/INSTALL/centos.sh index 45884cf1..7a28b6c1 100644 --- a/INSTALL/centos.sh +++ b/INSTALL/centos.sh @@ -5,16 +5,9 @@ echo "=========================================================" echo "To answer yes type the letter (y) in lowercase and press ENTER." echo "Default is no (N). Skip any components you already have or don't need." echo "=============" - -#Create default configuration file if [ ! -e "./conf.json" ]; then cp conf.sample.json conf.json - - #Generate a random Cron key for the config file - cronKey=$(< /dev/urandom tr -dc A-Za-z0-9 | head -c${1:-30}) - sed -i -e 's/change_this_to_something_very_random__just_anything_other_than_this/'"$cronKey"'/g' conf.json fi - if [ ! -e "./super.json" ]; then echo "Default Superuser : admin@shinobi.video" echo "Default Password : admin" @@ -34,12 +27,13 @@ if [ ! -e "./super.json" ]; then fi echo "Shinobi - Run yum update" sudo yum update -y +sudo yum install gcc gcc-c++ -y sudo yum install make zip dos2unix -y if ! [ -x "$(command -v node)" ]; then echo "=============" echo "Shinobi - Installing Node.js" #Installs Node.js 10 - sudo curl --silent --location https://rpm.nodesource.com/setup_8.x | bash - + sudo curl --silent --location https://rpm.nodesource.com/setup_12.x | bash - sudo yum install nodejs -y else echo "Node.js Found..." @@ -69,50 +63,31 @@ if [ "$ffmpeginstall" = "y" ] || [ "$ffmpeginstall" = "Y" ]; then fi fi echo "=============" -echo "Shinobi - Do you want to use MariaDB or SQLite3?" -echo "SQLite3 is better for small installs" -echo "MariaDB (MySQL) is better for large installs" -echo "(S)QLite3 or (M)ariaDB?" -echo "Press [ENTER] for default (MariaDB)" -read sqliteormariadb -if [ "$sqliteormariadb" = "S" ] || [ "$sqliteormariadb" = "s" ]; then - sudo npm install jsonfile - sudo yum install -y sqlite sqlite-devel -y - sudo npm install sqlite3 - node ./tools/modifyConfiguration.js databaseType=sqlite3 - if [ ! -e "./shinobi.sqlite" ]; then - echo "Creating shinobi.sqlite for SQLite3..." - sudo cp sql/shinobi.sample.sqlite shinobi.sqlite - else - echo "shinobi.sqlite already exists. Continuing..." - fi -else - echo "=============" - echo "Shinobi - Do you want to Install MariaDB?" - echo "(y)es or (N)o" - read mysqlagree - if [ "$mysqlagree" = "y" ] || [ "$mysqlagree" = "Y" ]; then - #Add the MariaDB repository to yum, this allows for a more current version of MariaDB to be installed - sudo curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | sudo bash -s -- --skip-maxscale - sudo yum install mariadb mariadb-server -y - #Start mysql and enable on boot - sudo systemctl start mariadb - sudo systemctl enable mariadb - #Run mysql install - sudo mysql_secure_installation - fi - echo "=============" - echo "Shinobi - Database Installation" - echo "(y)es or (N)o" - read mysqlagreeData - if [ "$mysqlagreeData" = "y" ] || [ "$mysqlagreeData" = "Y" ]; then - echo "What is your SQL Username?" - read sqluser - echo "What is your SQL Password?" - read sqlpass - sudo mysql -u $sqluser -p$sqlpass -e "source sql/user.sql" || true - sudo mysql -u $sqluser -p$sqlpass -e "source sql/framework.sql" || true - fi +echo "=============" +echo "Shinobi - Do you want to Install MariaDB?" +echo "(y)es or (N)o" +read mysqlagree +if [ "$mysqlagree" = "y" ] || [ "$mysqlagree" = "Y" ]; then + #Add the MariaDB repository to yum, this allows for a more current version of MariaDB to be installed + sudo curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | sudo bash -s -- --skip-maxscale + sudo yum install mariadb mariadb-server -y + #Start mysql and enable on boot + sudo systemctl start mariadb + sudo systemctl enable mariadb + #Run mysql install + sudo mysql_secure_installation +fi +echo "=============" +echo "Shinobi - Database Installation" +echo "(y)es or (N)o" +read mysqlagreeData +if [ "$mysqlagreeData" = "y" ] || [ "$mysqlagreeData" = "Y" ]; then + echo "What is your SQL Username?" + read sqluser + echo "What is your SQL Password?" + read sqlpass + sudo mysql -u $sqluser -p$sqlpass -e "source sql/user.sql" || true + sudo mysql -u $sqluser -p$sqlpass -e "source sql/framework.sql" || true fi echo "=============" echo "Shinobi - Install NPM Libraries" diff --git a/INSTALL/cuda-10-2.sh b/INSTALL/cuda-10-2.sh new file mode 100644 index 00000000..4f9e7c34 --- /dev/null +++ b/INSTALL/cuda-10-2.sh @@ -0,0 +1,49 @@ +#!/bin/sh +echo "------------------------------------------" +echo "-- Installing CUDA Toolkit and CUDA DNN --" +echo "------------------------------------------" +# Install CUDA Drivers and Toolkit +if [ -x "$(command -v apt)" ]; then + wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/cuda-ubuntu1804.pin + sudo mv cuda-ubuntu1804.pin /etc/apt/preferences.d/cuda-repository-pin-600 + sudo apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/7fa2af80.pub + sudo add-apt-repository "deb http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/ /" + sudo apt-get update + + sudo apt-get update -y + + sudo apt-get -o Dpkg::Options::="--force-overwrite" install cuda-toolkit-10-2 -y --no-install-recommends + sudo apt-get -o Dpkg::Options::="--force-overwrite" install --fix-broken -y + + # Install CUDA DNN + wget https://cdn.shinobi.video/installers/libcudnn7_7.6.5.32-1+cuda10.2_amd64.deb -O cuda-dnn.deb + sudo dpkg -i cuda-dnn.deb + wget https://cdn.shinobi.video/installers/libcudnn7-dev_7.6.5.32-1+cuda10.2_amd64.deb -O cuda-dnn-dev.deb + sudo dpkg -i cuda-dnn-dev.deb + echo "-- Cleaning Up --" + # Cleanup + sudo rm cuda-dnn.deb + sudo rm cuda-dnn-dev.deb +fi +if [ -x "$(command -v yum)" ]; then + sudo yum-config-manager --add-repo http://developer.download.nvidia.com/compute/cuda/repos/rhel7/x86_64/cuda-rhel7.repo + sudo yum clean all + sudo yum -y install nvidia-driver-latest-dkms cuda + sudo yum -y install cuda-drivers + wget https://cdn.shinobi.video/installers/libcudnn7-7.6.5.33-1.cuda10.2.x86_64.rpm -O cuda-dnn.rpm + sudo yum -y localinstall cuda-dnn.rpm + wget https://cdn.shinobi.video/installers/libcudnn7-devel-7.6.5.33-1.cuda10.2.x86_64.rpm -O cuda-dnn-dev.rpm + sudo yum -y localinstall cuda-dnn-dev.rpm + echo "-- Cleaning Up --" + sudo rm cuda-dnn.rpm + sudo rm cuda-dnn-dev.rpm +fi + +echo "------------------------------" +echo "Reboot is required. Do it now?" +echo "------------------------------" +echo "(y)es or (N)o. Default is No." +read rebootTheMachineHomie +if [ "$rebootTheMachineHomie" = "y" ] || [ "$rebootTheMachineHomie" = "Y" ]; then + sudo reboot +fi diff --git a/INSTALL/cuda-10.sh b/INSTALL/cuda-10.sh new file mode 100644 index 00000000..d032cd1b --- /dev/null +++ b/INSTALL/cuda-10.sh @@ -0,0 +1,47 @@ +#!/bin/sh +echo "------------------------------------------" +echo "-- Installing CUDA Toolkit and CUDA DNN --" +echo "------------------------------------------" +# Install CUDA Drivers and Toolkit +if [ -x "$(command -v apt)" ]; then + wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/cuda-repo-ubuntu1804_10.0.130-1_amd64.deb + sudo dpkg -i --force-overwrite cuda-repo-ubuntu1804_10.0.130-1_amd64.deb + sudo apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/7fa2af80.pub + + sudo apt-get update -y + + sudo apt-get -o Dpkg::Options::="--force-overwrite" install cuda-toolkit-10-0 -y --no-install-recommends + sudo apt-get -o Dpkg::Options::="--force-overwrite" install --fix-broken -y + + # Install CUDA DNN + wget https://cdn.shinobi.video/installers/libcudnn7_7.6.5.32-1+cuda10.0_amd64.deb -O cuda-dnn.deb + sudo dpkg -i cuda-dnn.deb + wget https://cdn.shinobi.video/installers/libcudnn7-dev_7.6.5.32-1+cuda10.0_amd64.deb -O cuda-dnn-dev.deb + sudo dpkg -i cuda-dnn-dev.deb + echo "-- Cleaning Up --" + # Cleanup + sudo rm cuda-dnn.deb + sudo rm cuda-dnn-dev.deb +fi +if [ -x "$(command -v yum)" ]; then + wget https://developer.download.nvidia.com/compute/cuda/repos/rhel7/x86_64/cuda-repo-rhel7-10.0.130-1.x86_64.rpm + sudo rpm -i cuda-repo-rhel7-10.0.130-1.x86_64.rpm + sudo yum clean all + sudo yum install cuda + wget https://cdn.shinobi.video/installers/libcudnn7-7.6.5.32-1.cuda10.0.x86_64.rpm -O cuda-dnn.rpm + sudo yum -y localinstall cuda-dnn.rpm + wget https://cdn.shinobi.video/installers/libcudnn7-devel-7.6.5.32-1.cuda10.0.x86_64.rpm -O cuda-dnn-dev.rpm + sudo yum -y localinstall cuda-dnn-dev.rpm + echo "-- Cleaning Up --" + sudo rm cuda-dnn.rpm + sudo rm cuda-dnn-dev.rpm +fi + +echo "------------------------------" +echo "Reboot is required. Do it now?" +echo "------------------------------" +echo "(y)es or (N)o. Default is No." +read rebootTheMachineHomie +if [ "$rebootTheMachineHomie" = "y" ] || [ "$rebootTheMachineHomie" = "Y" ]; then + sudo reboot +fi diff --git a/INSTALL/cuda-9-0.sh b/INSTALL/cuda-9-0.sh new file mode 100644 index 00000000..e63d1688 --- /dev/null +++ b/INSTALL/cuda-9-0.sh @@ -0,0 +1,36 @@ +#!/bin/sh +echo "------------------------------------------" +echo "-- Installing CUDA Toolkit and CUDA DNN --" +echo "------------------------------------------" +# Install CUDA Drivers and Toolkit +echo "=============" +echo " Detecting Ubuntu Version" +echo "=============" +getubuntuversion=$(lsb_release -r | awk '{print $2}' | cut -d . -f1) +echo "=============" +echo " Ubuntu Version: $getubuntuversion" +echo "=============" +wget http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1704/x86_64/cuda-repo-ubuntu1704_9.0.176-1_amd64.deb -O cuda.deb +sudo apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1704/x86_64/7fa2af80.pub +sudo dpkg -i --force-overwrite cuda.deb +sudo apt-get update -y +sudo apt-get -o Dpkg::Options::="--force-overwrite" install cuda-toolkit-9-0 -y --no-install-recommends +sudo apt-get -o Dpkg::Options::="--force-overwrite" install --fix-broken -y +# Install CUDA DNN +wget https://cdn.shinobi.video/installers/libcudnn7_7.6.3.30-1+cuda9.0_amd64.deb -O cuda-dnn.deb +sudo dpkg -i cuda-dnn.deb +wget https://cdn.shinobi.video/installers/libcudnn7-dev_7.6.3.30-1+cuda9.0_amd64.deb -O cuda-dnn-dev.deb +sudo dpkg -i cuda-dnn-dev.deb +echo "-- Cleaning Up --" +# Cleanup +sudo rm cuda.deb +sudo rm cuda-dnn.deb +sudo rm cuda-dnn-dev.deb +echo "------------------------------" +echo "Reboot is required. Do it now?" +echo "------------------------------" +echo "(y)es or (N)o. Default is No." +read rebootTheMachineHomie +if [ "$rebootTheMachineHomie" = "y" ] || [ "$rebootTheMachineHomie" = "Y" ]; then + sudo reboot +fi diff --git a/INSTALL/cuda-9-2.sh b/INSTALL/cuda-9-2.sh new file mode 100644 index 00000000..3bffd0b5 --- /dev/null +++ b/INSTALL/cuda-9-2.sh @@ -0,0 +1,49 @@ +#!/bin/sh +echo "------------------------------------------" +echo "-- Installing CUDA Toolkit and CUDA DNN --" +echo "------------------------------------------" +# Install CUDA Drivers and Toolkit +echo "=============" +echo " Detecting Ubuntu Version" +echo "=============" +getubuntuversion=$(lsb_release -r | awk '{print $2}' | cut -d . -f1) +echo "=============" +echo " Ubuntu Version: $getubuntuversion" +echo "=============" +if [ "$getubuntuversion" = "17" ] || [ "$getubuntuversion" > "17" ]; then + wget https://cdn.shinobi.video/installers/cuda-repo-ubuntu1710_9.2.148-1_amd64.deb -O cuda.deb + sudo apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1710/x86_64/7fa2af80.pub + sudo dpkg -i --force-overwrite cuda.deb +fi +if [ "$getubuntuversion" = "16" ]; then + wget https://cdn.shinobi.video/installers/cuda-repo-ubuntu1604_9.2.148-1_amd64.deb -O cuda.deb + sudo apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1604/x86_64/7fa2af80.pub + sudo dpkg -i --force-overwrite cuda.deb +fi +sudo apt-get update -y +if [ "$getubuntuversion" = "17" ] || [ "$getubuntuversion" > "17" ]; then + sudo apt-get -o Dpkg::Options::="--force-overwrite" install cuda -y --no-install-recommends + sudo apt-get -o Dpkg::Options::="--force-overwrite" install --fix-broken -y +fi +if [ "$getubuntuversion" = "16" ]; then + sudo apt-get install libcuda1-384 -y --no-install-recommends + sudo apt-get install nvidia-cuda-toolkit -y +fi +# Install CUDA DNN +wget https://cdn.shinobi.video/installers/libcudnn7_7.2.1.38-1+cuda9.2_amd64.deb -O cuda-dnn.deb +sudo dpkg -i cuda-dnn.deb +wget https://cdn.shinobi.video/installers/libcudnn7-dev_7.2.1.38-1+cuda9.2_amd64.deb -O cuda-dnn-dev.deb +sudo dpkg -i cuda-dnn-dev.deb +echo "-- Cleaning Up --" +# Cleanup +sudo rm cuda.deb +sudo rm cuda-dnn.deb +sudo rm cuda-dnn-dev.deb +echo "------------------------------" +echo "Reboot is required. Do it now?" +echo "------------------------------" +echo "(y)es or (N)o. Default is No." +read rebootTheMachineHomie +if [ "$rebootTheMachineHomie" = "y" ] || [ "$rebootTheMachineHomie" = "Y" ]; then + sudo reboot +fi diff --git a/INSTALL/cuda.sh b/INSTALL/cuda.sh index 7d67aef8..4f9e7c34 100644 --- a/INSTALL/cuda.sh +++ b/INSTALL/cuda.sh @@ -3,42 +3,42 @@ echo "------------------------------------------" echo "-- Installing CUDA Toolkit and CUDA DNN --" echo "------------------------------------------" # Install CUDA Drivers and Toolkit -echo "=============" -echo " Detecting Ubuntu Version" -echo "=============" -getubuntuversion=$(lsb_release -r | awk '{print $2}' | cut -d . -f1) -echo "=============" -echo " Ubuntu Version: $getubuntuversion" -echo "=============" -if [ "$getubuntuversion" = "17" ] || [ "$getubuntuversion" > "17" ]; then - wget https://cdn.shinobi.video/installers/cuda-repo-ubuntu1710_9.2.148-1_amd64.deb -O cuda.deb - sudo apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1710/x86_64/7fa2af80.pub - sudo dpkg -i cuda.deb -fi -if [ "$getubuntuversion" = "16" ]; then - wget https://cdn.shinobi.video/installers/cuda-repo-ubuntu1604_9.2.148-1_amd64.deb -O cuda.deb - sudo apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1604/x86_64/7fa2af80.pub - sudo dpkg -i cuda.deb -fi -sudo apt-get update -y -if [ "$getubuntuversion" = "17" ] || [ "$getubuntuversion" > "17" ]; then - sudo apt-get -o Dpkg::Options::="--force-overwrite" install cuda -y --no-install-recommends +if [ -x "$(command -v apt)" ]; then + wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/cuda-ubuntu1804.pin + sudo mv cuda-ubuntu1804.pin /etc/apt/preferences.d/cuda-repository-pin-600 + sudo apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/7fa2af80.pub + sudo add-apt-repository "deb http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/ /" + sudo apt-get update + + sudo apt-get update -y + + sudo apt-get -o Dpkg::Options::="--force-overwrite" install cuda-toolkit-10-2 -y --no-install-recommends sudo apt-get -o Dpkg::Options::="--force-overwrite" install --fix-broken -y + + # Install CUDA DNN + wget https://cdn.shinobi.video/installers/libcudnn7_7.6.5.32-1+cuda10.2_amd64.deb -O cuda-dnn.deb + sudo dpkg -i cuda-dnn.deb + wget https://cdn.shinobi.video/installers/libcudnn7-dev_7.6.5.32-1+cuda10.2_amd64.deb -O cuda-dnn-dev.deb + sudo dpkg -i cuda-dnn-dev.deb + echo "-- Cleaning Up --" + # Cleanup + sudo rm cuda-dnn.deb + sudo rm cuda-dnn-dev.deb fi -if [ "$getubuntuversion" = "16" ]; then - sudo apt-get install libcuda1-384 -y --no-install-recommends - sudo apt-get install nvidia-cuda-toolkit -y +if [ -x "$(command -v yum)" ]; then + sudo yum-config-manager --add-repo http://developer.download.nvidia.com/compute/cuda/repos/rhel7/x86_64/cuda-rhel7.repo + sudo yum clean all + sudo yum -y install nvidia-driver-latest-dkms cuda + sudo yum -y install cuda-drivers + wget https://cdn.shinobi.video/installers/libcudnn7-7.6.5.33-1.cuda10.2.x86_64.rpm -O cuda-dnn.rpm + sudo yum -y localinstall cuda-dnn.rpm + wget https://cdn.shinobi.video/installers/libcudnn7-devel-7.6.5.33-1.cuda10.2.x86_64.rpm -O cuda-dnn-dev.rpm + sudo yum -y localinstall cuda-dnn-dev.rpm + echo "-- Cleaning Up --" + sudo rm cuda-dnn.rpm + sudo rm cuda-dnn-dev.rpm fi -# Install CUDA DNN -wget https://cdn.shinobi.video/installers/libcudnn7_7.2.1.38-1+cuda9.2_amd64.deb -O cuda-dnn.deb -sudo dpkg -i cuda-dnn.deb -wget https://cdn.shinobi.video/installers/libcudnn7-dev_7.2.1.38-1+cuda9.2_amd64.deb -O cuda-dnn-dev.deb -sudo dpkg -i cuda-dnn-dev.deb -echo "-- Cleaning Up --" -# Cleanup -sudo rm cuda.deb -sudo rm cuda-dnn.deb -sudo rm cuda-dnn-dev.deb + echo "------------------------------" echo "Reboot is required. Do it now?" echo "------------------------------" diff --git a/INSTALL/cuda9-part2-after-reboot.sh b/INSTALL/cuda9-part2-after-reboot.sh index 03351615..21b82617 100644 --- a/INSTALL/cuda9-part2-after-reboot.sh +++ b/INSTALL/cuda9-part2-after-reboot.sh @@ -1,2 +1,4 @@ +#!/bin/sh + sudo apt-get -y install cuda-toolkit-9-1 nvidia-smi \ No newline at end of file diff --git a/INSTALL/freenas.csh b/INSTALL/freenas.csh index b6e99be3..5d66a34c 100644 --- a/INSTALL/freenas.csh +++ b/INSTALL/freenas.csh @@ -7,12 +7,9 @@ pkg install -y nano ffmpeg libav x264 x265 mysql56-server node npm echo "Enabling mysql..." sysrc mysql_enable=yes service mysql-server start -echo "Cloning the official Shinobi Community Edition gitlab repo..." -git clone "https://gitlab.com/Shinobi-Systems/ShinobiCE" -cd ./ShinobiCE echo "Adding Shinobi user to database..." mysql -h localhost -u root -e "source sql/user.sql" -ehco "Shinobi database framework setup..." +echo "Shinobi database framework setup..." mysql -h localhost -u root -e "source sql/framework.sql" echo "Securing mysql..." #/usr/local/bin/mysql_secure_installation diff --git a/INSTALL/jetson-nano-convert-to-headless.sh b/INSTALL/jetson-nano-convert-to-headless.sh index 2247efdd..05e4a4cc 100644 --- a/INSTALL/jetson-nano-convert-to-headless.sh +++ b/INSTALL/jetson-nano-convert-to-headless.sh @@ -1,14 +1,16 @@ +#!/bin/bash + # Moe was here echo "=============" echo "Do you want to purge Desktop components from your Ubuntu 18.04 installation?" echo "You cannot undo this. Choose wisely." echo "Do NOT run this as root, instead run it with 'sudo'; if you want a complete wipe." echo "(y)es or (N)o" -read purgeDesktop +read -r purgeDesktop if [ "$purgeDesktop" = "Y" ] || [ "$purgeDesktop" = "y" ]; then echo "Really really sure?" echo "(y)es or (N)o" - read purgeDesktopSecond + read -r purgeDesktopSecond if [ "$purgeDesktopSecond" = "Y" ] || [ "$purgeDesktopSecond" = "y" ]; then echo "!----------------------------!" echo "Reset network interface to DHCP? (Automatically assign IP Address from network)" @@ -16,9 +18,10 @@ if [ "$purgeDesktop" = "Y" ] || [ "$purgeDesktop" = "y" ]; then echo "You can edit it after in /etc/network/interfaces" echo "!----------------------------!" echo "(y)es or (N)o" - read resetNetworkInterface + read -r resetNetworkInterface if [ "$resetNetworkInterface" = "Y" ] || [ "$resetNetworkInterface" = "y" ]; then - echo "source-directory /etc/network/interfaces.d" > "/etc/network/interfaces" + echo "auto lo" > "/etc/network/interfaces" + echo "iface lo inet loopback" >> "/etc/network/interfaces" echo "auto eth0" >> "/etc/network/interfaces" echo "iface eth0 inet dhcp" >> "/etc/network/interfaces" fi diff --git a/INSTALL/jeston-opencv3-4.sh b/INSTALL/jetson-opencv3-4.sh similarity index 100% rename from INSTALL/jeston-opencv3-4.sh rename to INSTALL/jetson-opencv3-4.sh diff --git a/INSTALL/macos-part2.sh b/INSTALL/macos-part2.sh index 37222e50..d159b1ca 100644 --- a/INSTALL/macos-part2.sh +++ b/INSTALL/macos-part2.sh @@ -1,4 +1,3 @@ - #!/bin/bash echo "=========================================================" echo "==!! Shinobi : The Open Source CCTV and NVR Solution !!==" @@ -6,7 +5,7 @@ echo "=================== Mac OS Install Part 2 ===============" echo "=========================================================" echo "Shinobi - Database Installation" echo "(y)es or (N)o" -read mysqlagreeData +read -r mysqlagreeData if [ "$mysqlagreeData" = "y" ]; then echo "Shinobi will now use root for database installation..." sudo mysql -e "source sql/user.sql" || true @@ -42,7 +41,7 @@ echo "=====================================" >> INSTALL/installed.txt echo "=====================================" >> INSTALL/installed.txt echo "Shinobi - Start Shinobi and set to start on boot?" echo "(y)es or (N)o" -read startShinobi +read -r startShinobi if [ "$startShinobi" = "y" ]; then sudo pm2 start camera.js sudo pm2 startup diff --git a/INSTALL/macos.sh b/INSTALL/macos.sh index 503c7149..ee62ebb4 100644 --- a/INSTALL/macos.sh +++ b/INSTALL/macos.sh @@ -8,17 +8,17 @@ echo "Default is no (N). Skip any components you already have or don't need." echo "=============" echo "Shinobi - Do you want to Install Node.js?" echo "(y)es or (N)o" -read nodejsinstall +read -r nodejsinstall if [ "$nodejsinstall" = "y" ]; then - curl -o node-v8.9.3.pkg https://nodejs.org/dist/v8.9.3/node-v8.9.3.pkg - sudo installer -pkg node-v8.9.3.pkg -target / - rm node-v8.9.3.pkg + curl -o node-installer.pkg https://nodejs.org/dist/v11.9.0/node-v11.9.0.pkg + sudo installer -pkg node-installer.pkg -target / + rm node-installer.pkg sudo ln -s /usr/local/bin/node /usr/bin/nodejs fi echo "=============" echo "Shinobi - Do you want to Install FFmpeg?" echo "(y)es or (N)o" -read ffmpeginstall +read -r ffmpeginstall if [ "$ffmpeginstall" = "y" ]; then echo "Shinobi - Installing FFmpeg" curl -o ffmpeg.zip https://cdn.shinobi.video/installers/ffmpeg-3.4.1-macos.zip @@ -65,7 +65,7 @@ echo "=====================================" >> INSTALL/installed.txt echo "=====================================" >> INSTALL/installed.txt echo "Shinobi - Start Shinobi and set to start on boot?" echo "(y)es or (N)o" -read startShinobi +read -r startShinobi if [ "$startShinobi" = "y" ]; then pm2 start camera.js pm2 startup diff --git a/INSTALL/now.sh b/INSTALL/now.sh index 5ffb2e8f..675c25fe 100644 --- a/INSTALL/now.sh +++ b/INSTALL/now.sh @@ -4,41 +4,46 @@ echo "========" echo "Select your OS" echo "If your OS is not on the list please refer to the docs." echo "========" -echo "1. Ubuntu" -echo "2. CentOS" -echo "3. MacOS" -echo "4. FreeBSD" -echo "5. OpenSUSE" -echo "6. CentOS - Quick Install" +echo "1. Ubuntu - Fast and Touchless" +echo "2. Ubuntu - Advanced" +echo "3. CentOS" +echo "4. CentOS - Quick Install" +echo "5. MacOS" +echo "6. FreeBSD" +echo "7. OpenSUSE" echo "========" read oschoicee case $oschoicee in "1") +chmod +x INSTALL/ubuntu-touchless.sh +sh INSTALL/ubuntu-touchless.sh + ;; +"2") chmod +x INSTALL/ubuntu.sh sh INSTALL/ubuntu.sh ;; -"2") +"3") chmod +x INSTALL/centos.sh sh INSTALL/centos.sh ;; -"3") +"4") +chmod +x "INSTALL/CentOS - Quick Install.sh" +sh "INSTALL/CentOS - Quick Install.sh" 1 + ;; +"5") chmod +x INSTALL/macos.sh sh INSTALL/macos.sh ;; -"4") +"6") chmod +x INSTALL/freebsd.sh sh INSTALL/freebsd.sh ;; -"5") +"7") chmod +x INSTALL/opensuse.sh sh INSTALL/opensuse.sh ;; -"6") -chmod +x "INSTALL/CentOS - Quick Install.sh" -sh "INSTALL/CentOS - Quick Install.sh" 1 - ;; *) echo "Choice not found." ;; -esac \ No newline at end of file +esac diff --git a/INSTALL/openbsd.sh b/INSTALL/openbsd.sh new file mode 100644 index 00000000..ba137fa8 --- /dev/null +++ b/INSTALL/openbsd.sh @@ -0,0 +1,212 @@ +#!/bin/sh + +# Copyright (c) 2020 Jordan Geoghegan + +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. + +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +# Functions + +doas_perms_abort () { + echo "\n!!! doas is not enabled! Please check /etc/doas.conf configuration. Exiting..." ; exit 1 +} + +package_install_abort () { + echo "\n!!! Package Install Failed! Exiting..." ; exit 1 +} + +mariadb_setup_abort () { + echo "\n!!! MariaDB configuration failed!. Exiting..." ; exit 1 +} + +user_create_abort () { + echo '\n!!! Creation of system user "_shinobi" failed. Exiting...' ; exit 1 +} + +source_dl_abort () { + echo "\n!!! Failed to download Shinobi source code from GitLab. Please check your internet connection. Exiting..." ; exit 1 +} + +source_extract_abort () { + echo "\n!!! Failed to extract Shinobi source code. Exiting..." ; exit 1 +} + +schema_install_abort () { + echo "\n!!! Failed to install Shinobi Database Schema. Exiting..." ; exit 1 +} + +npm_abort () { + echo "\n!!! Failed to install Shinobi Node.js dependencies. Exiting..." ; exit 1 +} + +package_install () { + while true ; do + echo "\nWould you like to install the required packages?" + printf "(Yes/no) : " + read -r pkg_perm + case $pkg_perm in + [Yy]* ) doas pkg_add node mariadb-server ffmpeg || package_install_abort ; break;; + [Nn]* ) echo "Packages are required to install Shinobi. Exiting..."; exit 0;; + * ) echo "Please answer yes or no.";; + esac + done +} + +mariadb_enable () { + echo "\n### Setting up MariaDB... Please follow the on screen instructions\n" + echo '\n### Running "/usr/local/bin/mysql_install_db"' + doas /usr/local/bin/mysql_install_db >/dev/null 2>&1 || mariadb_setup_abort + echo "\n### Configuring MariaDB to start at boot" + doas rcctl enable mysqld || mariadb_setup_abort + echo "\n### Starting MariaDB" + doas rcctl start mysqld || mariadb_setup_abort + echo '\n### Running "mysql_secure_installation"' + doas mysql_secure_installation || mariadb_setup_abort +} + +mariadb_setup () { + while true ; do + echo "\nWould you like to setup MariaDB now?" + printf "(Yes/no) : " + read -r mariadb_setup + case $mariadb_setup in + [Yy]* ) mariadb_enable || mariadb_setup_abort ; break;; + [Nn]* ) echo "MariaDB is required to install Shinobi. Exiting..."; exit 0;; + * ) echo "Please answer yes or no.";; + esac + done +} + +schema_install () { + echo "\nWhat is the MariaDB root password?" + printf "Password? : " + read -r sqlpass + cd /home/_shinobi/shinobi || schema_install_abort + doas -u _shinobi mysql -u root -p"$sqlpass" -e "source /home/_shinobi/shinobi/sql/user.sql" || schema_install_abort + doas -u _shinobi mysql -u root -p"$sqlpass" -e "source /home/_shinobi/shinobi/sql/framework.sql" || schema_install_abort +} + +pro_download () { + echo "\n### Grabbing Shinobi Pro from master branch\n" + doas -u _shinobi ftp -o /home/_shinobi/shinobi.tar.gz https://gitlab.com/Shinobi-Systems/Shinobi/-/archive/master/Shinobi-master.tar.gz +} + +gpl_download () { + echo "\n### Grabbing Shinobi CE from master branch\n" + doas -u _shinobi ftp -o /home/_shinobi/shinobi.tar.gz https://gitlab.com/Shinobi-Systems/ShinobiCE/-/archive/master/ShinobiCE-master.tar.gz +} + +dev_download () { + echo "\n### Grabbing latest Shinobi from development branch\n" + doas -u _shinobi ftp -o /home/_shinobi/shinobi.tar.gz https://gitlab.com/Shinobi-Systems/Shinobi/-/archive/dev/Shinobi-dev.tar.gz +} + +# Script Start + +while true ; do +echo "Does $(whoami) have doas permissions?" +printf "(Yes/no) : " + read -r doas_perm + case $doas_perm in + [Yy]* ) doas ls >/dev/null 2>&1 || doas_perms_abort; break;; + [Nn]* ) echo "Please run this script as user with doas permissions"; exit 0;; + * ) echo "Please answer yes or no.";; + esac +done + +while true ; do +echo "\nAre the following packages already installed?\n + Node.js + MariaDB + FFmpeg\n" +printf "(Yes/no) : " + read -r package_deps + case $package_deps in + [Yy]* ) echo "Proceeding..." ; break;; + [Nn]* ) package_install || package_install_abort ; break;; + * ) echo "Please answer yes or no.";; + esac +done + +while true ; do +echo "\nIs MariaDB already installed and configured on this machine?" +printf "(Yes/no) : " + read -r mariadb_conf + case $mariadb_conf in + [Yy]* ) echo "Proceeding..." ; break;; + [Nn]* ) mariadb_setup || mariadb_setup_abort ; break;; + * ) echo "Please answer yes or no.";; + esac +done + +# Shinobi unpriv user creation +echo '\n### Creating "_shinobi" System User\n' +doas useradd -s /sbin/nologin -m -d /home/_shinobi _shinobi || user_create_abort + +# Pro vs Community choice +while true ; do +echo "\nWhich version of Shinobi would you like to install?" +echo "[D]evelopment, [P]ro or [C]ommunity Edition" +printf "(Dev/Pro/Community) : " + read -r pro_ce + case $pro_ce in + [Dd]* ) dev_download || source_dl_abort ; break;; + [Pp]* ) pro_download || source_dl_abort ; break;; + [Cc]* ) gpl_download || source_dl_abort ; break;; + * ) echo 'Enter "P" for Pro or "C" for Community Version.';; + esac +done + +echo "\n### Extracting to install directory\n" +doas -u _shinobi tar -xzf /home/_shinobi/shinobi.tar.gz -C /home/_shinobi/ || source_extract_abort +doas -u _shinobi find /home/_shinobi/ -type d -name "Shinobi*" -exec mv {} /home/_shinobi/shinobi \; >/dev/null 2>&1 + + +# MariaDB DB schema install +while true ; do +echo '\nInstall Shinobi Database schema? (Answer "No" only if you have already installed it manually)' +printf "(Yes/no) : " + read -r schema_yn + case $schema_yn in + [Yy]* ) schema_install || schema_install_abort ; break;; + [Nn]* ) echo "Proceeding..." ; break;; + * ) echo "Please answer yes or no.";; + esac +done + +# NPM Node Module Installation +echo "\n### Installing required Node modules\n" +cd /home/_shinobi/shinobi || npm_abort +doas -u _shinobi npm install --unsafe-perm +doas npm audit fix --force +doas -u _shinobi cp /home/_shinobi/shinobi/conf.sample.json /home/_shinobi/shinobi/conf.json +doas -u _shinobi cp /home/_shinobi/shinobi/super.sample.json /home/_shinobi/shinobi/super.json +doas npm install -g pm2 + +# Post-Install Info +echo "\nCongratulations, Shinobi is now installed!\n" + +echo 'To start Shinobi at boot, add a crontab entry for the user "_shinobi" with something like this:\n' + +echo '$ doas crontab -u _shinobi -e + +@reboot /bin/sh -c "cd /home/_shinobi/Shinobi && pm2 start camera.js cron.js" + +echo "\nYou can access Shinobi at http://$(ifconfig | grep 'inet ' | awk '!/127.0.0.1/ {print $2}'):8080" + +echo "\nPlease create a user by logging in to the admin panel at http://$(ifconfig | grep 'inet ' | awk '!/127.0.0.1/ {print $2}'):8080/super" + +echo "\nThe default login credentials are: + username: admin@shinobi.video + password: admin" + +echo "\nThe official Shinobi Documentation can be found at: https://shinobi.video/docs/" \ No newline at end of file diff --git a/INSTALL/opencv-cuda.sh b/INSTALL/opencv-cuda.sh index 1a1ee504..52bbfac6 100644 --- a/INSTALL/opencv-cuda.sh +++ b/INSTALL/opencv-cuda.sh @@ -9,7 +9,7 @@ if [ ! -e "./opencv" ]; then echo "Downloading OpenCV..." git clone https://github.com/opencv/opencv.git cd opencv - git checkout 3.4.0 + git checkout 3.4.10 cd .. fi if [ ! -e "./opencv_contrib" ]; then @@ -34,16 +34,19 @@ echo "*****************" echo "Adding Additional Repository" echo "http://security.ubuntu.com/ubuntu" if [ "$flavor" = *"Artful"* ]; then - sudo add-apt-repository "deb http://security.ubuntu.com/ubuntu artful-security main" + sudo add-apt-repository "deb http://security.ubuntu.com/ubuntu artful-security main" -y fi if [ "$flavor" = *"Zesty"* ]; then - sudo add-apt-repository "deb http://security.ubuntu.com/ubuntu zesty-security main" + sudo add-apt-repository "deb http://security.ubuntu.com/ubuntu zesty-security main" -y fi if [ "$flavor" = *"Xenial"* ]; then - sudo add-apt-repository "deb http://security.ubuntu.com/ubuntu xenial-security main" + sudo add-apt-repository "deb http://security.ubuntu.com/ubuntu xenial-security main" -y fi if [ "$flavor" = *"Trusty"* ]; then - sudo add-apt-repository "deb http://security.ubuntu.com/ubuntu trusty-security main" + sudo add-apt-repository "deb http://security.ubuntu.com/ubuntu trusty-security main" -y +fi +if [ "$flavor" = *"Eoan"* ]; then + sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu/ bionic main restricted universe multiverse" -y fi echo "Downloading Libraries" sudo apt-get install libjpeg-dev libpango1.0-dev libgif-dev build-essential gcc-6 g++-6 -y; @@ -77,4 +80,4 @@ read opencvuninstall if [ "$opencvuninstall" = "y" ] || [ "$opencvuninstall" = "Y" ]; then rm -rf opencv rm -rf opencv_contrib -fi \ No newline at end of file +fi diff --git a/INSTALL/opensuse.sh b/INSTALL/opensuse.sh index 0b2d34e0..19dae61d 100644 --- a/INSTALL/opensuse.sh +++ b/INSTALL/opensuse.sh @@ -17,7 +17,7 @@ if [ ! -e "./super.json" ]; then echo "if you would like to limit accessibility of an" echo "account for business scenarios." echo "(y)es or (N)o" - read createSuperJson + read -r createSuperJson if [ "$createSuperJson" = "y" ] || [ "$createSuperJson" = "Y" ]; then echo "Default Superuser : admin@shinobi.video" echo "Default Password : admin" @@ -32,22 +32,22 @@ echo "=============" echo "Shinobi - Do you want to Install Node.js?" echo "(y)es or (N)o" NODEJSINSTALL=0 -read nodejsinstall +read -r nodejsinstall if [ "$nodejsinstall" = "y" ] || [ "$nodejsinstall" = "Y" ]; then - sudo zypper install -y nodejs8 + sudo zypper install -y nodejs11 NODEJSINSTALL=1 fi echo "=============" echo "Shinobi - Do you want to Install FFMPEG?" echo "(y)es or (N)o" -read ffmpeginstall +read -r ffmpeginstall if [ "$ffmpeginstall" = "y" ] || [ "$ffmpeginstall" = "Y" ]; then # Without nodejs8 package we can't use npm command if [ "$NODEJSINSTALL" -eq "1" ]; then echo "Shinobi - Do you want to Install FFMPEG with 'zypper --version' or download a static version provided with npm 'npm --version'?" echo "(z)ypper or (N)pm" echo "Press [ENTER] for default (npm)" - read ffmpegstaticinstall + read -r ffmpegstaticinstall if [ "$ffmpegstaticinstall" = "z" ] || [ "$ffmpegstaticinstall" = "Z" ]; then # Install ffmpeg and ffmpeg-devel sudo zypper install -y ffmpeg ffmpeg-devel @@ -59,48 +59,28 @@ if [ "$ffmpeginstall" = "y" ] || [ "$ffmpeginstall" = "Y" ]; then fi fi echo "=============" -echo "Shinobi - Do you want to use MariaDB or SQLite3?" -echo "SQLite3 is better for small installs" -echo "MariaDB (MySQL) is better for large installs" -echo "(S)QLite3 or (M)ariaDB?" -echo "Press [ENTER] for default (MariaDB)" -read sqliteormariadb -if [ "$sqliteormariadb" = "S" ] || [ "$sqliteormariadb" = "s" ]; then - sudo npm install jsonfile - sudo zypper install -y sqlite3 sqlite3-devel - sudo npm install sqlite3 - node ./tools/modifyConfiguration.js databaseType=sqlite3 - if [ ! -e "./shinobi.sqlite" ]; then - echo "Creating shinobi.sqlite for SQLite3..." - sudo cp sql/shinobi.sample.sqlite shinobi.sqlite - else - echo "shinobi.sqlite already exists. Continuing..." - fi -else - echo "=============" - echo "Shinobi - Do you want to Install MariaDB?" - echo "(y)es or (N)o" - read mysqlagree - if [ "$mysqlagree" = "y" ] || [ "$mysqlagree" = "Y" ]; then - sudo zypper install -y mariadb - #Start mysql and enable on boot - sudo systemctl start mariadb - sudo systemctl enable mariadb - #Run mysql install - sudo mysql_secure_installation - fi - echo "=============" - echo "Shinobi - Database Installation" - echo "(y)es or (N)o" - read mysqlagreeData - if [ "$mysqlagreeData" = "y" ] || [ "$mysqlagreeData" = "Y" ]; then - echo "What is your SQL Username?" - read sqluser - echo "What is your SQL Password?" - read sqlpass - sudo mysql -u $sqluser -p$sqlpass -e "source sql/user.sql" || true - sudo mysql -u $sqluser -p$sqlpass -e "source sql/framework.sql" || true - fi +echo "Shinobi - Do you want to Install MariaDB?" +echo "(y)es or (N)o" +read -r mysqlagree +if [ "$mysqlagree" = "y" ] || [ "$mysqlagree" = "Y" ]; then + sudo zypper install -y mariadb + #Start mysql and enable on boot + sudo systemctl start mariadb + sudo systemctl enable mariadb + #Run mysql install + sudo mysql_secure_installation +fi +echo "=============" +echo "Shinobi - Database Installation" +echo "(y)es or (N)o" +read -r mysqlagreeData +if [ "$mysqlagreeData" = "y" ] || [ "$mysqlagreeData" = "Y" ]; then + echo "What is your SQL Username?" + read -r sqluser + echo "What is your SQL Password?" + read -r sqlpass + sudo mysql -u "$sqluser" -p"$sqlpass" -e "source sql/user.sql" || true + sudo mysql -u "$sqluser" -p"$sqlpass" -e "source sql/framework.sql" || true fi echo "=============" echo "Shinobi - Install NPM Libraries" @@ -117,7 +97,7 @@ dos2unix /home/Shinobi/INSTALL/shinobi ln -s /home/Shinobi/INSTALL/shinobi /usr/bin/shinobi echo "Shinobi - Start Shinobi and set to start on boot?" echo "(y)es or (N)o" -read startShinobi +read -r startShinobi if [ "$startShinobi" = "y" ] || [ "$startShinobi" = "Y" ]; then sudo pm2 start camera.js sudo pm2 start cron.js diff --git a/INSTALL/shinobi b/INSTALL/shinobi index ed172942..6f3d94f9 100644 --- a/INSTALL/shinobi +++ b/INSTALL/shinobi @@ -4,7 +4,7 @@ if [ ! -e "/etc/shinobisystems/path.txt" ]; then else installationDirectory=$(cat /etc/shinobisystems/cctv.txt) fi -cd $installationDirectory +cd "$installationDirectory" || exit currentBuild=$(git show --oneline -s) gitOrigin=$(git remote show origin) splitBuildString=($currentBuild) @@ -72,8 +72,8 @@ fi if [[ $@ == *'restart'* ]]; then proccessAlive=$(pm2 list | grep camera) if [ "$proccessAlive" ]; then - pm2 restart $installationDirectory/camera.js - pm2 restart $installationDirectory/cron.js + pm2 restart "$installationDirectory"/camera.js + pm2 restart "$installationDirectory"/cron.js else echo "Shinobi process is not running." fi @@ -85,11 +85,11 @@ else else if [ -e "$installationDirectory/INSTALL/installed.txt" ]; then echo "Starting Shinobi" - pm2 start $installationDirectory/camera.js - pm2 start $installationDirectory/cron.js + pm2 start "$installationDirectory"/camera.js + pm2 start "$installationDirectory"/cron.js fi if [ ! -e "$installationDirectory/INSTALL/installed.txt" ]; then - chmod +x $installationDirectory/INSTALL/now.sh&&INSTALL/now.sh + chmod +x "$installationDirectory"/INSTALL/now.sh&&INSTALL/now.sh fi fi fi @@ -97,8 +97,8 @@ fi if [[ $@ == *'stop'* ]] || [[ $@ == *'exit'* ]]; then proccessAlive=$(pm2 list | grep camera) if [ "$proccessAlive" ]; then - pm2 stop $installationDirectory/camera.js - pm2 stop $installationDirectory/cron.js + pm2 stop "$installationDirectory"/camera.js + pm2 stop "$installationDirectory"/cron.js else echo "Shinobi process is not running." fi @@ -110,7 +110,7 @@ if [[ $@ == *'version'* ]]; then else echo "Repository : Shinobi CE" fi - echo $currentBuild + echo "$currentBuild" fi if [[ $@ == *'bootupEnable'* ]] || [[ $@ == *'bootupenable'* ]]; then pm2 startup @@ -140,17 +140,17 @@ if [[ $@ == *'update'* ]]; then echo "=============" echo "Shinobi - Are you sure you want to update? This will restart Shinobi." echo "(y)es or (N)o" - read updateshinobi + read -r updateshinobi if [ "$updateshinobi" = "y" ] || [ "$updateshinobi" = "Y" ]; then echo "Beginning Update Process..." - pm2 stop $installationDirectory/camera.js - pm2 stop $installationDirectory/cron.js + pm2 stop "$installationDirectory"/camera.js + pm2 stop "$installationDirectory"/cron.js npm install --unsafe-perm npm audit fix --force git reset --hard git pull - pm2 start $installationDirectory/camera.js - pm2 start $installationDirectory/cron.js + pm2 start "$installationDirectory"/camera.js + pm2 start "$installationDirectory"/cron.js else echo "Cancelled Update Process." fi diff --git a/INSTALL/ubuntu-easyinstall.sh b/INSTALL/ubuntu-easyinstall.sh index 4db4e37b..fe740287 100644 --- a/INSTALL/ubuntu-easyinstall.sh +++ b/INSTALL/ubuntu-easyinstall.sh @@ -1,13 +1,13 @@ #!/bin/bash echo "Shinobi - Do you want to Install Node.js?" echo "(y)es or (N)o" -read nodejsinstall +read -r nodejsinstall if [ "$nodejsinstall" = "y" ]; then - wget https://deb.nodesource.com/setup_8.x - chmod +x setup_8.x - ./setup_8.x + wget https://deb.nodesource.com/setup_12.x + chmod +x setup_12.x + ./setup_12.x sudo apt install nodejs -y - rm setup_8.x + rm setup_12.x fi #Detect Ubuntu Version @@ -34,28 +34,28 @@ fi # Install MariaDB echo "Shinobi - Do you want to Install MariaDB? Choose No if you have MySQL." echo "(y)es or (N)o" -read mysqlagree +read -r mysqlagree if [ "$mysqlagree" = "y" ]; then echo "Shinobi - Installing MariaDB" echo "Password for root SQL user, If you are installing SQL now then you may put anything:" - read sqlpass + read -r sqlpass echo "mariadb-server mariadb-server/root_password password $sqlpass" | debconf-set-selections echo "mariadb-server mariadb-server/root_password_again password $sqlpass" | debconf-set-selections apt install mariadb-server -y service mysql start fi -# Make sure files have correct perms +# Make sure files have correct perms chmod -R 755 . # Database Installation -#Check If Mysql-Server is already installed +#Check If Mysql-Server is already installed echo "=============" echo "Checking for mysql-server" echo "=============" dpkg -s mysql-server &> /dev/null -if [ $? -eq 0 ]; then +if [ $? -eq 0 ]; then echo "+====================================+" echo "| Warning MYSQL SERVER IS INSTALLED! |" echo "+====================================+" @@ -64,75 +64,75 @@ if [ $? -eq 0 ]; then echo "+====================================+" echo "Shinobi - Do you want to Install MariaDB?" echo "(y)es or (N)o" - read installmariadb + read -r installmariadb if [ "$installmariadb" = "y" ]; then echo "+=============================================+" echo "| This will DESTORY ALL DATA ON MYSQL SERVER! |" echo "+=============================================+" echo "Please type the following to continue" echo "DESTORY!" - read mysqlagree + read -r mysqlagree if [ "$mysqlagree" = "DESTORY!" ]; then echo "Shinobi - Installing MariaDB" echo "Password for root SQL user, If you are installing SQL now then you may put anything:" - read sqlpass + read -r sqlpass echo "mariadb-server mariadb-server/root_password password $sqlpass" | debconf-set-selections echo "mariadb-server mariadb-server/root_password_again password $sqlpass" | debconf-set-selections - #Create my.cnf file + #Create my.cnf file echo "[client]" >> ~/.my.cnf echo "user=root" >> ~/.my.cnf echo "password=$sqlpass" >> ~/.my.cnf - chmod 755 ~/.my.cnf - apt install mariadb-server + chmod 755 ~/.my.cnf + apt install mariadb-server service mysql start fi fi -else +else echo "Shinobi - Do you want to Install MariaDB?" echo "(y)es or (N)o" - read mysqlagree + read -r mysqlagree if [ "$mysqlagree" = "y" ]; then echo "Shinobi - Installing MariaDB" echo "Password for root SQL user, If you are installing SQL now then you may put anything:" - read sqlpass + read -r sqlpass echo "mariadb-server mariadb-server/root_password password $sqlpass" | debconf-set-selections echo "mariadb-server mariadb-server/root_password_again password $sqlpass" | debconf-set-selections echo "[client]" >> ~/.my.cnf echo "user=root" >> ~/.my.cnf echo "password=$sqlpass" >> ~/.my.cnf - chmod 755 ~/.my.cnf + chmod 755 ~/.my.cnf apt install mariadb-server -y service mysql start fi -fi +fi chmod -R 755 . echo "Shinobi - Database Installation" echo "(y)es or (N)o" -read mysqlagreeData +read -r mysqlagreeData if [ "$mysqlagreeData" = "y" ]; then mysql -e "source sql/user.sql" || true mysql -e "source sql/framework.sql" || true echo "Shinobi - Do you want to Install Default Data (default_data.sql)?" echo "(y)es or (N)o" - read mysqlDefaultData + read -r mysqlDefaultData if [ "$mysqlDefaultData" = "y" ]; then escapeReplaceQuote='\\"' - groupKey=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 7 | head -n 1) - userID=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 6 | head -n 1) - userEmail=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 6 | head -n 1)"@"$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 6 | head -n 1)".com" - userPasswordPlain=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 6 | head -n 1) - userPasswordMD5=$(echo -n "$userPasswordPlain" | md5sum | awk '{print $1}') + groupKey=$(head -c 64 < /dev/urandom | sha256sum | awk '{print substr($1,1,7)}') + userID=$(head -c 64 < /dev/urandom | sha256sum | awk '{print substr($1,1,6)}') + userEmail=$(head -c 64 < /dev/urandom | sha256sum | awk '{print substr($1,1,6)}')"@"$(head -c 64 < /dev/urandom | sha256sum | awk '{print substr($1,1,6)}')".com" + userPasswordPlain=$(head -c 64 < /dev/urandom | sha256sum | awk '{print substr($1,1,7)}') + userPasswordMD5=$(echo -n "$userPasswordPlain" | md5sum | awk '{print $1}') userDetails='{"days":"10"}' userDetails=$(echo "$userDetails" | sed -e 's/"/'$escapeReplaceQuote'/g') - echo $userDetailsNew + echo "$userDetailsNew" apiIP='0.0.0.0' - apiKey=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) + apiKey=$(head -c 64 < /dev/urandom | sha256sum | awk '{print substr($1,1,32)}') apiDetails='{"auth_socket":"1","get_monitors":"1","control_monitors":"1","get_logs":"1","watch_stream":"1","watch_snapshot":"1","watch_videos":"1","delete_videos":"1"}' apiDetails=$(echo "$apiDetails" | sed -e 's/"/'$escapeReplaceQuote'/g') rm sql/default_user.sql || true echo "USE ccio;INSERT INTO Users (\`ke\`,\`uid\`,\`auth\`,\`mail\`,\`pass\`,\`details\`) VALUES (\"$groupKey\",\"$userID\",\"$apiKey\",\"$userEmail\",\"$userPasswordMD5\",\"$userDetails\");INSERT INTO API (\`code\`,\`ke\`,\`uid\`,\`ip\`,\`details\`) VALUES (\"$apiKey\",\"$groupKey\",\"$userID\",\"$apiIP\",\"$apiDetails\");" > "sql/default_user.sql" - mysql -u $sqluser -p$sqlpass --database ccio -e "source sql/default_user.sql" > "INSTALL/log.txt" + mysql -u "$sqluser" -p"$sqlpass" --database ccio -e "source sql/default_user.sql" > "INSTALL/log.txt" echo "=====================================" echo "=======!! Login Credentials !!=======" echo "|| Username : $userEmail" @@ -169,9 +169,9 @@ echo "Shinobi - Finished" touch INSTALL/installed.txt echo "Shinobi - Start Shinobi?" echo "(y)es or (N)o" -read startShinobi +read -r startShinobi if [ "$startShinobi" = "y" ]; then pm2 start camera.js pm2 start cron.js pm2 list -fi \ No newline at end of file +fi diff --git a/INSTALL/ubuntu-touchless.sh b/INSTALL/ubuntu-touchless.sh new file mode 100644 index 00000000..0349299b --- /dev/null +++ b/INSTALL/ubuntu-touchless.sh @@ -0,0 +1,132 @@ +#!/bin/bash +echo "=========================================================" +echo "==!! Shinobi : The Open Source CCTV and NVR Solution !!==" +echo "=========================================================" +echo "To answer yes type the letter (y) in lowercase and press ENTER." +echo "Default is no (N). Skip any components you already have or don't need." +echo "=============" +#Detect Ubuntu Version +echo "=============" +echo " Detecting Ubuntu Version" +echo "=============" +getubuntuversion=$(lsb_release -r | awk '{print $2}' | cut -d . -f1) +echo "=============" +echo " Ubuntu Version: $getubuntuversion" +echo "=============" +echo "Shinobi - Do you want to temporarily disable IPv6?" +echo "Sometimes IPv6 causes Ubuntu package updates to fail. Only do this if your machine doesn't rely on IPv6." +echo "(y)es or (N)o" +read -r disableIpv6 +if [ "$disableIpv6" = "y" ] || [ "$disableIpv6" = "Y" ]; then + sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1 + sudo sysctl -w net.ipv6.conf.default.disable_ipv6=1 + sudo sysctl -w net.ipv6.conf.lo.disable_ipv6=1 +fi +if [ "$getubuntuversion" = "18" ] || [ "$getubuntuversion" > "18" ]; then + apt install sudo wget -y + sudo apt install -y software-properties-common + sudo add-apt-repository universe -y +fi +if [ "$getubuntuversion" = "16" ]; then + sudo apt install gnupg-curl -y +fi +sudo apt install gcc g++ -y +sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-8 800 --slave /usr/bin/g++ g++ /usr/bin/g++-8 +#create conf.json +if [ ! -e "./conf.json" ]; then + sudo cp conf.sample.json conf.json + #Generate a random Cron key for the config file + cronKey=$(head -c 1024 < /dev/urandom | sha256sum | awk '{print substr($1,1,29)}') + #Insert key into conf.json + sudo sed -i -e 's/change_this_to_something_very_random__just_anything_other_than_this/'"$cronKey"'/g' conf.json +fi +#create super.json +if [ ! -e "./super.json" ]; then + echo "=============" + echo "Default Superuser : admin@shinobi.video" + echo "Default Password : admin" + echo "* You can edit these settings in \"super.json\" located in the Shinobi directory." + sudo cp super.sample.json super.json +fi +if ! [ -x "$(command -v ifconfig)" ]; then + echo "=============" + echo "Shinobi - Installing Net-Tools" + sudo apt install net-tools -y +fi +if ! [ -x "$(command -v node)" ]; then + echo "=============" + echo "Shinobi - Installing Node.js" + wget https://deb.nodesource.com/setup_12.x + chmod +x setup_12.x + ./setup_12.x + sudo apt install nodejs -y + sudo apt install node-pre-gyp -y + rm setup_12.x +else + echo "Node.js Found..." + echo "Version : $(node -v)" +fi +if ! [ -x "$(command -v npm)" ]; then + sudo apt install npm -y +fi +sudo apt install make zip -y +if ! [ -x "$(command -v ffmpeg)" ]; then + if [ "$getubuntuversion" = "16" ] || [ "$getubuntuversion" < "16" ]; then + echo "=============" + echo "Shinobi - Get FFMPEG 3.x from ppa:jonathonf/ffmpeg-3" + sudo add-apt-repository ppa:jonathonf/ffmpeg-3 -y + sudo apt update -y && sudo apt install ffmpeg libav-tools x264 x265 -y + else + echo "=============" + echo "Shinobi - Installing FFMPEG" + sudo apt install ffmpeg -y + fi +else + echo "FFmpeg Found..." + echo "Version : $(ffmpeg -version)" +fi +echo "=============" +echo "Shinobi - Installing MariaDB" +echo "MariaDB will be installed with no password." +sqlpass="" +echo "mariadb-server mariadb-server/root_password password $sqlpass" | debconf-set-selections +echo "mariadb-server mariadb-server/root_password_again password $sqlpass" | debconf-set-selections +sudo apt install mariadb-server -y +sudo service mysql start +echo "=============" +echo "Shinobi - Installing Database..." +sqluser="root" +sudo mysql -e "source sql/user.sql" || true +sudo mysql -e "source sql/framework.sql" || true +echo "=============" +echo "Shinobi - Install NPM Libraries" +sudo npm i npm -g +sudo npm install --unsafe-perm +sudo npm audit fix --force +echo "=============" +echo "Shinobi - Install PM2" +sudo npm install pm2@3.0.0 -g +echo "Shinobi - Finished" +sudo chmod -R 755 . +touch INSTALL/installed.txt +dos2unix /home/Shinobi/INSTALL/shinobi +ln -s /home/Shinobi/INSTALL/shinobi /usr/bin/shinobi +echo "Shinobi - Randomizing cron key" +node /home/Shinobi/tools/modifyConfiguration.js addToConfig="{\"cron\":{\"key\":\"$(head -c 64 < /dev/urandom | sha256sum | awk '{print substr($1,1,60)}')\"}}" +echo "Shinobi - Starting Shinobi and setting to start on boot" +sudo pm2 start camera.js +sudo pm2 start cron.js +sudo pm2 startup +sudo pm2 save +sudo pm2 list +echo "=====================================" +echo "||===== Install Completed =====||" +echo "=====================================" +echo "|| Login with the Superuser and create a new user!!" +echo "||===================================" +echo "|| Open http://$(ifconfig | sed -En 's/127.0.0.1//;s/.*inet (addr:)?(([0-9]*\.){3}[0-9]*).*/\2/p'):8080/super in your web browser." +echo "||===================================" +echo "|| Default Superuser : admin@shinobi.video" +echo "|| Default Password : admin" +echo "=====================================" +echo "=====================================" diff --git a/INSTALL/ubuntu.sh b/INSTALL/ubuntu.sh index 81b77d0b..a4cbd5d2 100644 --- a/INSTALL/ubuntu.sh +++ b/INSTALL/ubuntu.sh @@ -19,11 +19,17 @@ if [ "$getubuntuversion" = "18" ] || [ "$getubuntuversion" > "18" ]; then sudo add-apt-repository universe -y fi if [ "$getubuntuversion" = "16" ]; then - apt install gnupg-curl -y + sudo apt install gnupg-curl -y fi +sudo apt install gcc-8 g++-8 -y +sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-8 800 --slave /usr/bin/g++ g++ /usr/bin/g++-8 #create conf.json if [ ! -e "./conf.json" ]; then sudo cp conf.sample.json conf.json + #Generate a random Cron key for the config file + cronKey=$(head -c 1024 < /dev/urandom | sha256sum | awk '{print substr($1,1,29)}') + #Insert key into conf.json + sudo sed -i -e 's/change_this_to_something_very_random__just_anything_other_than_this/'"$cronKey"'/g' conf.json fi #create super.json if [ ! -e "./super.json" ]; then @@ -35,16 +41,17 @@ if [ ! -e "./super.json" ]; then fi if ! [ -x "$(command -v ifconfig)" ]; then echo "=============" - echo "Shinobi - Installing Net-Tools" - sudo apt install net-tools -y + echo "Shinobi - Installing Net-Tools and Dos2Unix" + sudo apt install net-tools dos2unix -y fi if ! [ -x "$(command -v node)" ]; then echo "=============" echo "Shinobi - Installing Node.js" - wget https://deb.nodesource.com/setup_8.x - chmod +x setup_8.x - ./setup_8.x + wget https://deb.nodesource.com/setup_12.x + chmod +x setup_12.x + ./setup_12.x sudo apt install nodejs -y + rm setup_12.x else echo "Node.js Found..." echo "Version : $(node -v)" @@ -69,53 +76,34 @@ else echo "Version : $(ffmpeg -version)" fi echo "=============" -echo "Shinobi - Do you want to use MariaDB or SQLite3?" -echo "SQLite3 is better for small installs" -echo "MariaDB (MySQL) is better for large installs" -echo "(S)QLite3 or (M)ariaDB?" -echo "Press [ENTER] for default (MariaDB)" -read sqliteormariadb -if [ "$sqliteormariadb" = "S" ] || [ "$sqliteormariadb" = "s" ]; then - sudo npm install jsonfile - sudo apt-get install sqlite3 libsqlite3-dev -y - sudo npm install sqlite3 - node ./tools/modifyConfiguration.js databaseType=sqlite3 - if [ ! -e "./shinobi.sqlite" ]; then - echo "Creating shinobi.sqlite for SQLite3..." - sudo cp sql/shinobi.sample.sqlite shinobi.sqlite - else - echo "shinobi.sqlite already exists. Continuing..." - fi -else - echo "Shinobi - Do you want to Install MariaDB? Choose No if you already have it." - echo "(y)es or (N)o" - read mysqlagree +echo "Shinobi - Do you want to Install MariaDB? Choose No if you already have it." +echo "(y)es or (N)o" +read -r mysqlagree +if [ "$mysqlagree" = "y" ] || [ "$mysqlagree" = "Y" ]; then + echo "Shinobi - Installing MariaDB" + echo "Password for root SQL user, If you are installing SQL now then you may put anything:" + read -r sqlpass + echo "mariadb-server mariadb-server/root_password password $sqlpass" | debconf-set-selections + echo "mariadb-server mariadb-server/root_password_again password $sqlpass" | debconf-set-selections + sudo apt install mariadb-server -y + sudo service mysql start +fi +echo "=============" +echo "Shinobi - Database Installation" +echo "(y)es or (N)o" +read -r mysqlagreeData +if [ "$mysqlagreeData" = "y" ] || [ "$mysqlagreeData" = "Y" ]; then if [ "$mysqlagree" = "y" ] || [ "$mysqlagree" = "Y" ]; then - echo "Shinobi - Installing MariaDB" - echo "Password for root SQL user, If you are installing SQL now then you may put anything:" - read sqlpass - echo "mariadb-server mariadb-server/root_password password $sqlpass" | debconf-set-selections - echo "mariadb-server mariadb-server/root_password_again password $sqlpass" | debconf-set-selections - sudo apt install mariadb-server -y - sudo service mysql start + sqluser="root" fi - echo "=============" - echo "Shinobi - Database Installation" - echo "(y)es or (N)o" - read mysqlagreeData - if [ "$mysqlagreeData" = "y" ] || [ "$mysqlagreeData" = "Y" ]; then - if [ "$mysqlagree" = "y" ] || [ "$mysqlagree" = "Y" ]; then - sqluser="root" - fi - if [ ! "$mysqlagree" = "y" ]; then - echo "What is your SQL Username?" - read sqluser - echo "What is your SQL Password?" - read sqlpass - fi - sudo mysql -u $sqluser -p$sqlpass -e "source sql/user.sql" || true - sudo mysql -u $sqluser -p$sqlpass -e "source sql/framework.sql" || true + if [ ! "$mysqlagree" = "y" ]; then + echo "What is your SQL Username?" + read -r sqluser + echo "What is your SQL Password?" + read -r sqlpass fi + sudo mysql -u "$sqluser" -p"$sqlpass" -e "source sql/user.sql" || true + sudo mysql -u "$sqluser" -p"$sqlpass" -e "source sql/framework.sql" || true fi echo "=============" echo "Shinobi - Install NPM Libraries" @@ -128,11 +116,13 @@ sudo npm install pm2@3.0.0 -g echo "Shinobi - Finished" sudo chmod -R 755 . touch INSTALL/installed.txt -dos2unix /home/Shinobi/INSTALL/shinobi -ln -s /home/Shinobi/INSTALL/shinobi /usr/bin/shinobi +dos2unix INSTALL/shinobi +ln -s `readlink -f INSTALL/shinobi` /usr/bin/shinobi +echo "Shinobi - Randomizing cron key" +node tools/modifyConfiguration.js addToConfig="{\"cron\":{\"key\":\"$(head -c 64 < /dev/urandom | sha256sum | awk '{print substr($1,1,60)}')\"}}" echo "Shinobi - Start Shinobi and set to start on boot?" echo "(y)es or (N)o" -read startShinobi +read -r startShinobi if [ "$startShinobi" = "y" ] || [ "$startShinobi" = "y" ]; then sudo pm2 start camera.js sudo pm2 start cron.js diff --git a/README.md b/README.md index 583ba94b..dc6b388c 100644 --- a/README.md +++ b/README.md @@ -1,146 +1,77 @@ -# Shinobi Pro +# Shinobi Pro ### (Shinobi Open Source Software) -Shinobi is the Open Source CCTV Solution written in Node.JS. Designed with multiple account system, Streams by WebSocket, and Save to WebM. Shinobi can record IP Cameras and Local Cameras. +Shinobi is the Open Source CCTV Solution written in Node.JS. Designed with multiple account system, Streams by WebSocket, and Direct saving to MP4. Shinobi can record IP Cameras and Local Cameras. - +## Install and Use -# Key Aspects +- Installation : http://shinobi.video/docs/start +- Post-Installation Tutorials : http://shinobi.video/docs/configure +- Troubleshooting Guide : https://hub.shinobi.video/articles/view/v0AFPFchfVcFGUS -For an updated list of features visit the official website. http://shinobi.video/features +#### Docker +- Install with **Docker** : https://gitlab.com/Shinobi-Systems/Shinobi/-/tree/dev/Docker -- Time-lapse Viewer (Watch a hours worth of footage in a few minutes) -- 2-Factor Authentication -- Defeats stream limit imposed by browsers - - With Base64 (Stream Type) and JPEG Mode (Option) -- Records IP Cameras and Local Cameras -- Streams by WebSocket, HLS (includes audio), and MJPEG -- Save to WebM and MP4 - - Can save Audio -- Push Events - When a video is finished it will appear in the dashboard without a refresh -- Region Motion Detection (Similar to ZoneMinder Zone Detection) - - Represented by a Motion Guage on each monitor -- "No Motion" Notifications -- 1 Process for Each Camera to do both, Recording and Streaming -- Timeline for viewing Motion Events and Videos -- Sub-Accounts with permissions - - Monitor Viewing - - Monitor Editing - - Video Deleting - - Separate API keys for sub account -- Cron Filters can be set based on master account -- Stream Analyzer built-in (FFprobe GUI) -- Monitor Groups -- Can snapshot images from stream directly -- Lower Bandwith Mode (JPEG Mode) - - Snapshot (cgi-bin) must be enabled in Monitor Settings -- Control Cameras from Interface -- API - - Get videos - - Get monitors - - Change monitor modes : Disabled, Watch, Record - - Embedding streams -- Dashboard Framework made with Google Material Design Lite, jQuery, and Bootstrap +## "is my camera supported?" + +Ask yourself these questions to get a general sense. + +- Does it have ONVIF? + - If yes, then it may have H.264 or H.265 streaming capability. +- Does it have RTSP Protocol for Streaming? + - If yes, then it may have H.264 or H.265 streaming capability. +- Can you stream it in VLC Player? + - If yes, use that same URL in Shinobi. You may need to specify the port number when using `rtsp://` protocol. +- Does it have MJPEG Streaming? + - While this would work in Shinobi, it is far from ideal. Please see if any of the prior questions are applicable. +- Does it have a web interface that you can connect to directly? + - If yes, then you may be able to find model information that can be used to search online for a streaming URL. + +Configuration Guides : http://shinobi.video/docs/configure ## Asking for help -Before asking questions it would nice if you read the docs :) http://shinobi.video +- General Support : https://shinobi.community + - Please be sure to read the `#guidelines` channel after joining. +- Business Inquiries : business@shinobi.video or the Live Chat on https://shinobi.video -After doing so please head on over to the Discord community chat for support. https://discordapp.com/invite/mdhmvuH +## Support the Development -The Issues section is only for bugs with the software. Comments and feature requests may be closed without comment. http://shinobi.video/docs/contribute +It's a proven fact that generosity makes you a happier person :) https://www.nature.com/articles/ncomms15964 -Please be considerate of developer efforts. If you have simple questions, like "what does this button do?", please be sure to have read the docs entirely before asking. If you would like to skip reading the docs and ask away you can order a support package :) http://shinobi.video/support +Get a Mobile License to unlock extended features on the Mobile App as well as support the development! +- Shinobi Mobile App : https://cdn.shinobi.video/installers/ShinobiMobile/ +- Get a Mobile License : https://licenses.shinobi.video/subscribe?planSubscribe=plan_G31AZ9mknNCa6z -## Making Suggestions or Feature Requests - -You can post suggestions on the Forum in the Suggestions category. Please do not treat this channel like a "demands" window. Developer efforts are limited. Much more than many alternatives. - -when you have a suggestion please try and make the changes yourself then post a pull request to the `dev` branch. Then we can decide if it's a good change for Shinobi. If you don't know how to go about it and want to have me put it higher on my priority list you can order a support package :) Pretty Ferengi of me... but until we live in a world without money please support Shinobi :) Cheers! - -http://shinobi.video/support - -## Help make Shinobi the best Open Source CCTV Solution. -Donate - http://shinobi.video/docs/donate - -Ordering a License, Paid Support, or anything from here will allow a lot more time to be spent on Shinobi. - -Order Support - http://shinobi.video/support - -# Why make this? +## Why make this? http://shinobi.video/why -# What others say +## Author -> "After trying zoneminder without success (heavy unstable and slow) I passed to Shinobi that despite being young spins a thousand times better (I have a setup with 16 cameras recording in FHD to ~ 10fps on a pentium of ~ 2009 and I turn with load below 1.5)." +Moe Alam, Shinobi Systems -> *A Reddit user, /r/ItalyInformatica* +Shinobi is developed by many contributors. Please have a look at the commits to see some of who they are :) +https://gitlab.com/Shinobi-Systems/Shinobi/-/commits/dev -  - -> "I would suggest Shinobi as a NVR. It's still in the early days but works a lot better than ZoneMinder for me. I'm able to record 16 cams at 1080p 15fps continously whith no load on server (Pentium E5500 3GB RAM) where zm crashed with 6 cams at 720p. Not to mention the better interface." - -> *A Reddit user, /r/HomeNetworking* - -# How to Install and Run - -> FOR DOCKER USERS : Docker is not officially supported and is not recommended. The kitematic method is provided for those who wish to quickly test Shinobi. The Docker files included in the master and dev branches are maintained by the community. If you would like support with Docker please find a community member who maintains the Docker files or please refer to Docker's forum. - -#### Fast Install (The Ninja Way) - -1. Become `root` to use the installer and run Shinobi. Use one of the following to do so. - - - Ubuntu 17.04, 17.10 - - `sudo su` - - CentOS 7 - - `su` - - MacOS 10.7(+) - - `su` -2. Download and run the installer. - -``` -bash <(curl -s https://gitlab.com/Shinobi-Systems/Shinobi-Installer/raw/master/shinobi-install.sh) -``` - -#### Elaborate Installs - -Installation Tutorials - http://shinobi.video/docs/start - -Troubleshooting Guide - http://shinobi.video/docs/start#trouble-section - -# Author - -Moe Alam - -Follow Shinobi on Twitter https://twitter.com/ShinobiCCTV - -Join the Community Chat - - - -# Support the Development +## Support the Development Ordering a certificate or support package greatly boosts development. Please consider contributing :) http://shinobi.video/support -# Links +## Links -Documentation - http://shinobi.video/docs - -Donate - https://shinobi.video/docs/donate - -Tested Cameras and Systems - http://shinobi.video/docs/supported - -Features - http://shinobi.video/features - -Reddit (Forum) - https://www.reddit.com/r/ShinobiCCTV/ - -YouTube (Tutorials) - https://www.youtube.com/channel/UCbgbBLTK-koTyjOmOxA9msQ - -Discord (Community Chat) - https://discordapp.com/invite/mdhmvuH - -Twitter (News) - https://twitter.com/ShinobiCCTV - -Facebook (News) - https://www.facebook.com/Shinobi-1223193167773738/?ref=bookmarks \ No newline at end of file +- Articles : http://hub.shinobi.video/articles +- Documentation : http://shinobi.video/docs +- Features List : http://shinobi.video/features + - Some features may not be listed. +- Donation : http://shinobi.video/docs/donate +- Buy Shinobi Stuff : https://licenses.shinobi.video +- User Submitted Configurations : http://shinobi.video/docs/cameras +- Features : http://shinobi.video/features +- Reddit (Forum) : https://www.reddit.com/r/ShinobiCCTV/ +- YouTube (Tutorials) : https://www.youtube.com/channel/UCbgbBLTK-koTyjOmOxA9msQ +- Discord (Community Chat) : https://discordapp.com/invite/mdhmvuH +- Twitter (News) : https://twitter.com/ShinobiCCTV +- Facebook (News) : https://www.facebook.com/Shinobi-1223193167773738/?ref=bookmarks diff --git a/UPDATE.sh b/UPDATE.sh new file mode 100644 index 00000000..9ca4e321 --- /dev/null +++ b/UPDATE.sh @@ -0,0 +1,5 @@ +git reset --hard +git pull +npm install --unsafe-perm +# pm2 restart camera +# pm2 restart cron diff --git a/camera.js b/camera.js index dd9ab65f..f200e22b 100644 --- a/camera.js +++ b/camera.js @@ -1,6 +1,6 @@ // // Shinobi -// Copyright (C) 2016 Moe Alam, moeiscool +// Copyright (C) 2020 Moe Alam, moeiscool // // // # Donate @@ -9,84 +9,84 @@ // PayPal : paypal@m03.ca // var io = new (require('socket.io'))() -//library loader -var loadLib = function(lib){ - return require(__dirname+'/libs/'+lib+'.js') -} //process handlers -var s = loadLib('process')(process,__dirname) +var s = require('./libs/process.js')(process,__dirname) //load extender functions -loadLib('extenders')(s) +require('./libs/extenders.js')(s) //configuration loader -var config = loadLib('config')(s) +var config = require('./libs/config.js')(s) //basic functions -loadLib('basic')(s,config) +require('./libs/basic.js')(s,config) //language loader -var lang = loadLib('language')(s,config) +var lang = require('./libs/language.js')(s,config) //working directories : videos, streams, fileBin.. -loadLib('folders')(s,config,lang) +require('./libs/folders.js')(s,config,lang) //code test module -loadLib('codeTester')(s,config,lang) +require('./libs/codeTester.js')(s,config,lang) //get version -loadLib('version')(s,config,lang) +require('./libs/version.js')(s,config,lang) //video processing engine -loadLib('ffmpeg')(s,config,lang,function(ffmpeg){ +require('./libs/ffmpeg.js')(s,config,lang,async function(ffmpeg){ //ffmpeg coProcessor - loadLib('ffmpegCoProcessor')(s,config,lang,ffmpeg) + require('./libs/ffmpegCoProcessor.js')(s,config,lang,ffmpeg) //database connection : mysql, sqlite3.. - loadLib('sql')(s,config) + require('./libs/sql.js')(s,config) //authenticator functions : API, dashboard login.. - loadLib('auth')(s,config,lang) + require('./libs/auth.js')(s,config,lang) //express web server with ejs - var app = loadLib('webServer')(s,config,lang,io) + var app = require('./libs/webServer.js')(s,config,lang,io) //web server routes : page handling.. - loadLib('webServerPaths')(s,config,lang,app,io) + require('./libs/webServerPaths.js')(s,config,lang,app,io) //web server routes for streams : streams.. - loadLib('webServerStreamPaths')(s,config,lang,app,io) + require('./libs/webServerStreamPaths.js')(s,config,lang,app,io) //web server admin routes : create sub accounts, share monitors, share videos - loadLib('webServerAdminPaths')(s,config,lang,app,io) + require('./libs/webServerAdminPaths.js')(s,config,lang,app,io) //web server superuser routes : create admin accounts and manage system functions - loadLib('webServerSuperPaths')(s,config,lang,app,io) + require('./libs/webServerSuperPaths.js')(s,config,lang,app,io) //websocket connection handlers : login and streams.. - loadLib('socketio')(s,config,lang,io) + require('./libs/socketio.js')(s,config,lang,io) //user and group functions - loadLib('user')(s,config,lang) + require('./libs/user.js')(s,config,lang) //timelapse functions - loadLib('timelapse')(s,config,lang,app,io) + require('./libs/timelapse.js')(s,config,lang,app,io) //fileBin functions - loadLib('fileBin')(s,config,lang,app,io) + require('./libs/fileBin.js')(s,config,lang,app,io) //monitor/camera handlers - loadLib('monitor')(s,config,lang) + require('./libs/monitor.js')(s,config,lang) //event functions : motion, object matrix handler - loadLib('events')(s,config,lang) - //built-in detector functions : pam-diff.. - loadLib('detector')(s,config) + require('./libs/events.js')(s,config,lang) //recording functions - loadLib('videos')(s,config,lang) - //branding functions and config defaults - loadLib('videoDropInServer')(s,config,lang,app,io) + require('./libs/videos.js')(s,config,lang) //plugins : websocket connected services.. - loadLib('plugins')(s,config,lang,io) + require('./libs/plugins.js')(s,config,lang,io) //health : cpu and ram trackers.. - loadLib('health')(s,config,lang,io) + require('./libs/health.js')(s,config,lang,io) //cluster module - loadLib('childNode')(s,config,lang,app,io) + require('./libs/childNode.js')(s,config,lang,app,io) //cloud uploaders : amazon s3, webdav, backblaze b2.. - loadLib('uploaders')(s,config,lang) + require('./libs/uploaders.js')(s,config,lang,app,io) //notifiers : discord.. - loadLib('notification')(s,config,lang) + require('./libs/notification.js')(s,config,lang) //notifiers : discord.. - loadLib('rtmpserver')(s,config,lang) + require('./libs/rtmpserver.js')(s,config,lang) //dropInEvents server (file manipulation to create event trigger) - loadLib('dropInEvents')(s,config,lang,app,io) + require('./libs/dropInEvents.js')(s,config,lang,app,io) //form fields to drive the internals - loadLib('definitions')(s,config,lang,app,io) + require('./libs/definitions.js')(s,config,lang,app,io) //branding functions and config defaults - loadLib('branding')(s,config,lang,app,io) + require('./libs/branding.js')(s,config,lang,app,io) //custom module loader - loadLib('customAutoLoad')(s,config,lang,app,io) + require('./libs/customAutoLoad.js')(s,config,lang,app,io) //scheduling engine - loadLib('scheduler')(s,config,lang,app,io) + require('./libs/shinobiHub.js')(s,config,lang,app,io) + //onvif, ptz engine + require('./libs/control.js')(s,config,lang,app,io) + //ffprobe, onvif engine + require('./libs/scanners.js')(s,config,lang,app,io) + //scheduling engine + require('./libs/scheduler.js')(s,config,lang,app,io) //on-start actions, daemon(s) starter - loadLib('startup')(s,config,lang) + await require('./libs/startup.js')(s,config,lang) + //p2p, commander + require('./libs/commander.js')(s,config,lang) }) diff --git a/conf.sample.json b/conf.sample.json index 59310610..1566a561 100644 --- a/conf.sample.json +++ b/conf.sample.json @@ -2,6 +2,7 @@ "port": 8080, "passwordType": "sha256", "detectorMergePamRegionTriggers": true, + "wallClockTimestampAsDefault": true, "addStorage": [ {"name":"second","path":"__DIR__/videos2"} ], diff --git a/cron.js b/cron.js index 883b457a..2ae7e843 100644 --- a/cron.js +++ b/cron.js @@ -135,7 +135,142 @@ s.sqlQuery = function(query,values,onMoveOn){ } }) } - +const cleanSqlWhereObject = (where) => { + const newWhere = {} + Object.keys(where).forEach((key) => { + if(key !== '__separator'){ + const value = where[key] + newWhere[key] = value + } + }) + return newWhere +} +const processSimpleWhereCondition = (dbQuery,where,didOne) => { + var whereIsArray = where instanceof Array; + if(where[0] === 'or' || where.__separator === 'or'){ + if(whereIsArray){ + where.shift() + dbQuery.orWhere(...where) + }else{ + where = cleanSqlWhereObject(where) + dbQuery.orWhere(where) + } + }else if(!didOne){ + didOne = true + whereIsArray ? dbQuery.where(...where) : dbQuery.where(where) + }else{ + whereIsArray ? dbQuery.andWhere(...where) : dbQuery.andWhere(where) + } +} +const processWhereCondition = (dbQuery,where,didOne) => { + var whereIsArray = where instanceof Array; + if(!where[0])return; + if(where[0] && where[0] instanceof Array){ + dbQuery.where(function() { + var _this = this + var didOneInsideGroup = false + where.forEach((whereInsideGroup) => { + processWhereCondition(_this,whereInsideGroup,didOneInsideGroup) + }) + }) + }else if(where[0] && where[0] instanceof Object){ + dbQuery.where(function() { + var _this = this + var didOneInsideGroup = false + where.forEach((whereInsideGroup) => { + processSimpleWhereCondition(_this,whereInsideGroup,didOneInsideGroup) + }) + }) + }else{ + processSimpleWhereCondition(dbQuery,where,didOne) + } +} +const knexError = (dbQuery,options,err) => { + console.error('knexError----------------------------------- START') + if(config.debugLogVerbose && config.debugLog === true){ + s.debugLog('s.knexQuery QUERY',JSON.stringify(options,null,3)) + s.debugLog('STACK TRACE, NOT AN ',new Error()) + } + console.error(err) + console.error(dbQuery.toString()) + console.error('knexError----------------------------------- END') +} +const knexQuery = (options,callback) => { + try{ + if(!s.databaseEngine)return// console.log('Database Not Set'); + // options = { + // action: "", + // columns: "", + // table: "" + // } + var dbQuery + switch(options.action){ + case'select': + options.columns = options.columns.indexOf(',') === -1 ? [options.columns] : options.columns.split(','); + dbQuery = s.databaseEngine.select(...options.columns).from(options.table) + break; + case'count': + options.columns = options.columns.indexOf(',') === -1 ? [options.columns] : options.columns.split(','); + dbQuery = s.databaseEngine(options.table) + dbQuery.count(options.columns) + break; + case'update': + dbQuery = s.databaseEngine(options.table).update(options.update) + break; + case'delete': + dbQuery = s.databaseEngine(options.table) + break; + case'insert': + dbQuery = s.databaseEngine(options.table).insert(options.insert) + break; + } + if(options.where instanceof Array){ + var didOne = false; + options.where.forEach((where) => { + processWhereCondition(dbQuery,where,didOne) + }) + }else if(options.where instanceof Object){ + dbQuery.where(options.where) + } + if(options.action === 'delete'){ + dbQuery.del() + } + if(options.orderBy){ + dbQuery.orderBy(...options.orderBy) + } + if(options.groupBy){ + dbQuery.groupBy(options.groupBy) + } + if(options.limit){ + if(`${options.limit}`.indexOf(',') === -1){ + dbQuery.limit(options.limit) + }else{ + const limitParts = `${options.limit}`.split(',') + dbQuery.limit(limitParts[0]).offset(limitParts[1]) + } + } + if(config.debugLog === true){ + console.log(dbQuery.toString()) + } + if(callback || options.update || options.insert || options.action === 'delete'){ + dbQuery.asCallback(function(err,r) { + if(err){ + knexError(dbQuery,options,err) + } + if(callback)callback(err,r) + if(config.debugLogVerbose && config.debugLog === true){ + s.debugLog('s.knexQuery QUERY',JSON.stringify(options,null,3)) + s.debugLog('s.knexQuery RESPONSE',JSON.stringify(r,null,3)) + s.debugLog('STACK TRACE, NOT AN ',new Error()) + } + }) + } + return dbQuery + }catch(err){ + if(callback)callback(err,[]) + knexError(dbQuery,options,err) + } +} s.debugLog = function(arg1,arg2){ if(config.debugLog === true){ if(!arg2)arg2 = '' @@ -236,29 +371,24 @@ const checkFilterRules = function(v,callback){ var b = v.d.filters[m]; s.debugLog(b) if(b.enabled==="1"){ - b.ar=[v.ke]; - b.sql=[]; - b.where.forEach(function(j,k){ - if(j.p1==='ke'){j.p3=v.ke} - switch(j.p3_type){ - case'function': - b.sql.push(j.p1+' '+j.p2+' '+j.p3) - break; - default: - b.sql.push(j.p1+' '+j.p2+' ?') - b.ar.push(j.p3) - break; - } + const whereQuery = [ + ['ke','=',v.ke], + ['status','!=',"0"], + ['details','NOT LIKE','%"archived":"1"%'], + ] + b.where.forEach(function(condition){ + if(condition.p1 === 'ke'){condition.p3 = v.ke} + whereQuery.push([condition.p1,condition.p2 || '=',condition.p3]) }) - b.sql='WHERE ke=? AND status != 0 AND details NOT LIKE \'%"archived":"1"%\' AND ('+b.sql.join(' AND ')+')'; - if(b.sort_by&&b.sort_by!==''){ - b.sql+=' ORDER BY `'+b.sort_by+'` '+b.sort_by_direction - } - if(b.limit&&b.limit!==''){ - b.sql+=' LIMIT '+b.limit - } - s.sqlQuery('SELECT * FROM Videos '+b.sql,b.ar,function(err,r){ - if(r&&r[0]){ + knexQuery({ + action: "select", + columns: "*", + table: "Videos", + where: whereQuery, + orderBy: [b.sort_by,b.sort_by_direction.toLowerCase()], + limit: b.limit + },(err,r) => { + if(r && r[0]){ if(r.length > 0 || config.debugLog === true){ s.cx({f:'filterMatch',msg:r.length+' SQL rows match "'+m+'"',ke:v.ke,time:moment()}) } @@ -309,10 +439,19 @@ const deleteRowsWithNoVideo = function(v,callback){ ) ){ s.alreadyDeletedRowsWithNoVideosOnStart[v.ke]=true; - es={}; - s.sqlQuery('SELECT * FROM Videos WHERE ke=? AND status!=0 AND details NOT LIKE \'%"archived":"1"%\' AND time < ?',[v.ke,s.sqlDate('10 MINUTE')],function(err,evs){ - if(evs&&evs[0]){ - es.del=[];es.ar=[v.ke]; + knexQuery({ + action: "select", + columns: "*", + table: "Videos", + where: [ + ['ke','=',v.ke], + ['status','!=','0'], + ['details','NOT LIKE','%"archived":"1"%'], + ['time','<',s.sqlDate('10 MINUTE')], + ] + },(err,evs) => { + if(evs && evs[0]){ + const videosToDelete = []; evs.forEach(function(ev){ var filename var details @@ -337,8 +476,8 @@ const deleteRowsWithNoVideo = function(v,callback){ s.tx({f:'video_delete',filename:filename+'.'+ev.ext,mid:ev.mid,ke:ev.ke,time:ev.time,end:s.moment(new Date,'YYYY-MM-DD HH:mm:ss')},'GRP_'+ev.ke); } }); - if(es.del.length>0 || config.debugLog === true){ - s.cx({f:'deleteNoVideo',msg:es.del.length+' SQL rows with no file deleted',ke:v.ke,time:moment()}) + if(videosToDelete.length>0 || config.debugLog === true){ + s.cx({f:'deleteNoVideo',msg:videosToDelete.length+' SQL rows with no file deleted',ke:v.ke,time:moment()}) } } setTimeout(function(){ @@ -353,7 +492,14 @@ const deleteRowsWithNoVideo = function(v,callback){ const deleteOldLogs = function(v,callback){ if(!v.d.log_days||v.d.log_days==''){v.d.log_days=10}else{v.d.log_days=parseFloat(v.d.log_days)}; if(config.cron.deleteLogs===true&&v.d.log_days!==0){ - s.sqlQuery("DELETE FROM Logs WHERE ke=? AND `time` < ?",[v.ke,s.sqlDate(v.d.log_days+' DAY')],function(err,rrr){ + knexQuery({ + action: "delete", + table: "Logs", + where: [ + ['ke','=',v.ke], + ['time','<',s.sqlDate(v.d.log_days+' DAY')], + ] + },(err,rrr) => { callback() if(err)return console.error(err); if(rrr.affectedRows && rrr.affectedRows.length>0 || config.debugLog === true){ @@ -368,10 +514,39 @@ const deleteOldLogs = function(v,callback){ const deleteOldEvents = function(v,callback){ if(!v.d.event_days||v.d.event_days==''){v.d.event_days=10}else{v.d.event_days=parseFloat(v.d.event_days)}; if(config.cron.deleteEvents===true&&v.d.event_days!==0){ - s.sqlQuery("DELETE FROM Events WHERE ke=? AND `time` < ?",[v.ke,s.sqlDate(v.d.event_days+' DAY')],function(err,rrr){ + knexQuery({ + action: "delete", + table: "Events", + where: [ + ['ke','=',v.ke], + ['time','<',s.sqlDate(v.d.event_days+' DAY')], + ] + },(err,rrr) => { callback() if(err)return console.error(err); - if(rrr.affectedRows && rrr.affectedRows.length>0 || config.debugLog === true){ + if(rrr.affectedRows && rrr.affectedRows.length > 0 || config.debugLog === true){ + s.cx({f:'deleteEvents',msg:(rrr.affectedRows || 0)+' SQL rows older than '+v.d.event_days+' days deleted',ke:v.ke,time:moment()}) + } + }) + }else{ + callback() + } +} +//event counts +const deleteOldEventCounts = function(v,callback){ + if(!v.d.event_days||v.d.event_days==''){v.d.event_days=10}else{v.d.event_days=parseFloat(v.d.event_days)}; + if(config.cron.deleteEvents===true&&v.d.event_days!==0){ + knexQuery({ + action: "delete", + table: "Events Counts", + where: [ + ['ke','=',v.ke], + ['time','<',s.sqlDate(v.d.event_days+' DAY')], + ] + },(err,rrr) => { + callback() + if(err && err.code !== 'ER_NO_SUCH_TABLE')return console.error(err); + if(rrr.affectedRows && rrr.affectedRows.length > 0 || config.debugLog === true){ s.cx({f:'deleteEvents',msg:(rrr.affectedRows || 0)+' SQL rows older than '+v.d.event_days+' days deleted',ke:v.ke,time:moment()}) } }) @@ -384,7 +559,15 @@ const deleteOldFileBins = function(v,callback){ if(!v.d.fileBin_days||v.d.fileBin_days==''){v.d.fileBin_days=10}else{v.d.fileBin_days=parseFloat(v.d.fileBin_days)}; if(config.cron.deleteFileBins===true&&v.d.fileBin_days!==0){ var fileBinQuery = " FROM Files WHERE ke=? AND `time` < ?"; - s.sqlQuery("SELECT *"+fileBinQuery,[v.ke,s.sqlDate(v.d.fileBin_days+' DAY')],function(err,files){ + knexQuery({ + action: "select", + columns: "*", + table: "Files", + where: [ + ['ke','=',v.ke], + ['time','<',s.sqlDate(v.d.fileBin_days+' DAY')], + ] + },(err,files) => { if(files&&files[0]){ //delete the files files.forEach(function(file){ @@ -393,7 +576,14 @@ const deleteOldFileBins = function(v,callback){ }) }) //delete the database rows - s.sqlQuery("DELETE"+fileBinQuery,[v.ke,v.d.fileBin_days],function(err,rrr){ + knexQuery({ + action: "delete", + table: "Files", + where: [ + ['ke','=',v.ke], + ['time','<',s.sqlDate(v.d.fileBin_days+' DAY')], + ] + },(err,rrr) => { callback() if(err)return console.error(err); if(rrr.affectedRows && rrr.affectedRows.length>0 || config.debugLog === true){ @@ -436,7 +626,14 @@ const processUser = function(number,rows){ if(!v.d.size||v.d.size==''){v.d.size=10000}else{v.d.size=parseFloat(v.d.size)}; //days to keep videos if(!v.d.days||v.d.days==''){v.d.days=5}else{v.d.days=parseFloat(v.d.days)}; - s.sqlQuery('SELECT * FROM Monitors WHERE ke=?', [v.ke], function(err,rr) { + knexQuery({ + action: "select", + columns: "*", + table: "Monitors", + where: [ + ['ke','=',v.ke], + ] + },(err,rr) => { if(!v.d.filters||v.d.filters==''){ v.d.filters={}; } @@ -474,14 +671,17 @@ const processUser = function(number,rows){ s.debugLog('--- deleteOldFileBins Complete') deleteOldEvents(v,function(){ s.debugLog('--- deleteOldEvents Complete') - checkFilterRules(v,function(){ - s.debugLog('--- checkFilterRules Complete') - deleteRowsWithNoVideo(v,function(){ - s.debugLog('--- deleteRowsWithNoVideo Complete') - checkForOrphanedFiles(v,function(){ - //done user, unlock current, and do next - overlapLocks[v.ke]=false; - processUser(number+1,rows) + deleteOldEventCounts(v,function(){ + s.debugLog('--- deleteOldEventCounts Complete') + checkFilterRules(v,function(){ + s.debugLog('--- checkFilterRules Complete') + deleteRowsWithNoVideo(v,function(){ + s.debugLog('--- deleteRowsWithNoVideo Complete') + checkForOrphanedFiles(v,function(){ + //done user, unlock current, and do next + overlapLocks[v.ke]=false; + processUser(number+1,rows) + }) }) }) }) @@ -504,7 +704,14 @@ const clearCronInterval = function(){ } const doCronJobs = function(){ s.cx({f:'start',time:moment()}) - s.sqlQuery('SELECT ke,uid,details,mail FROM Users WHERE details NOT LIKE \'%"sub"%\'', function(err,rows) { + knexQuery({ + action: "select", + columns: "ke,uid,details,mail", + table: "Users", + where: [ + ['details','NOT LIKE','%"sub"%'], + ] + },(err,rows) => { if(err){ console.error(err) } diff --git a/definitions/en_CA.js b/definitions/en_CA.js index 8bec2844..46e62eff 100644 --- a/definitions/en_CA.js +++ b/definitions/en_CA.js @@ -16,7 +16,7 @@ module.exports = function(s,config,lang){ "field": lang.Mode, "fieldType": "select", "description": "This is the primary task of the monitor.", - "default": "stop", + "default": "start", "example": "", "selector": "h_m", "possible": [ @@ -52,7 +52,7 @@ module.exports = function(s,config,lang){ "name": "name", "field": lang.Name, "description": "This is the human-readable display name for the monitor.", - "example": "Bunny" + "example": "Home-Front" }, { "name": "detail=max_keep_days", @@ -75,6 +75,41 @@ module.exports = function(s,config,lang){ } ] }, + "Presets": { + id: "monSectionPresets", + "name": lang.Presets, + "color": "purple", + isSection: true, + "info": [ + { + "name": lang['Add New'], + "color": "grey", + isFormGroupGroup: true, + "info": [ + { + "id": "monitorPresetsName", + "field": lang['Preset Name'], + }, + { + "fieldType": "btn", + "class": `btn-success add-new`, + "btnContent": `   ${lang['Add']}`, + }, + ] + }, + { + "fieldType": 'ul', + "id": "monitorPresetsSelection", + "class": "mdl-list" + }, + { + "fieldType": "btn", + "attribute": `data-toggle="modal" data-target="#schedules"`, + "class": `btn-info`, + "btnContent": `   ${lang['Schedules']}`, + }, + ], + }, "Connection": { "name": lang.Connection, "color": "orange", @@ -163,8 +198,8 @@ module.exports = function(s,config,lang){ "field": lang.Automatic, "description": "Feed the individual pieces required to build a stream URL or provide the full URL and allow Shinobi to parse it for you.", "selector": "h_auto_host", - "form-group-class": "h_t_input h_t_h264 h_t_hls h_t_mp4 h_t_jpeg h_t_mjpeg", - "form-group-class-pre-layer":"h_t_input h_t_h264 h_t_hls h_t_mp4 h_t_jpeg h_t_mjpeg h_t_local", + "form-group-class": "h_t_input h_t_h264 h_t_hls h_t_mp4 h_t_jpeg h_t_mjpeg h_t_mxpeg", + "form-group-class-pre-layer":"h_t_input h_t_h264 h_t_hls h_t_mp4 h_t_jpeg h_t_mjpeg h_t_mxpeg h_t_local", "default": "", "example": "", "fieldType": "select", @@ -342,7 +377,7 @@ module.exports = function(s,config,lang){ "name": "detail=fatal_max", "field": lang['Retry Connection'], "description": "The number of times to retry for network connection between the server and camera before setting the monitor to Disabled. No decimals. Set to 0 to retry forever.", - "default": "0", + "default": "10", "example": "", "possible": "", "form-group-class": "h_t_input h_t_h264 h_t_hls h_t_mp4 h_t_jpeg h_t_mjpeg h_t_local", @@ -385,6 +420,25 @@ module.exports = function(s,config,lang){ } ] }, + { + "name": "detail=onvif_non_standard", + "field": lang['Non-Standard ONVIF'], + "description": "Is this a Non-Standard ONVIF camera?", + "default": "0", + "example": "", + "form-group-class": "h_onvif_input h_onvif_1", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, { hidden: true, "name": "detail=onvif_port", @@ -394,6 +448,11 @@ module.exports = function(s,config,lang){ "example": "", "form-group-class": "h_onvif_input h_onvif_1", }, + { + "fieldType": "btn", + "class": `btn-success probe_config`, + "btnContent": `   ${lang.FFprobe}`, + }, ] }, "Input": { @@ -478,6 +537,25 @@ module.exports = function(s,config,lang){ "example": "25", "possible": "" }, + { + "name": "detail=wall_clock_timestamp_ignore", + "field": lang['Use Camera Timestamps'], + "description": "Base all incoming camera data in camera time instead of server time.", + "default": "0", + "example": "", + "form-group-class": "h_t_input h_t_h264", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, { hidden: true, "name": "height", @@ -1427,7 +1505,7 @@ module.exports = function(s,config,lang){ "possible": "1-23" }, { - "name": "preset_record", + "name": "detail=preset_record", "field": lang.Preset, "description": "Preset flag for certain video encoders. If you find your camera is crashing every few seconds : try leaving it blank.", "default": "", @@ -1493,7 +1571,7 @@ module.exports = function(s,config,lang){ }, { "name": "fps", - "field": lang["Video Record Rate (FPS)"], + "field": lang["Video Record Rate"], "description": "The speed in which frames are recorded to files, Frames Per Second. Be aware there is no default. This can lead to large files. Best to set this camera-side.", "default": "", "example": "2", @@ -1787,6 +1865,18 @@ module.exports = function(s,config,lang){ "form-group-class": "h_rec_ti_input h_rec_ti_1", "fieldType": "select", "possible": [ + { + "name": `.1 ${lang.minutes}`, + "value": "6" + }, + { + "name": `.25 ${lang.minutes}`, + "value": "15" + }, + { + "name": `.5 ${lang.minutes}`, + "value": "30" + }, { "name": `5 ${lang.minutes}`, "value": "300" @@ -1847,7 +1937,7 @@ module.exports = function(s,config,lang){ }, "Timelapse Watermark": { "id": "monSectionRecordingWatermark", - "name": lang['Recording Watermark'], + "name": lang['Timelapse Watermark'], "color": "red", isAdvanced: true, @@ -1977,6 +2067,16 @@ module.exports = function(s,config,lang){ "form-group-class": "shinobi-detector", "possible": "" }, + { + hidden: true, + "name": "detail=cust_detect_object", + "field": lang["Object Detector Flags"], + "description": "Custom Flags that bind to the stream Detector uses for analyzation.", + "default": "", + "example": "", + "form-group-class": "shinobi-detector", + "possible": "" + }, { hidden: true, "name": "detail=cust_sip_record", @@ -2076,6 +2176,36 @@ module.exports = function(s,config,lang){ } ] }, + { + hidden: true, + "form-group-class": "h_det_input h_det_1", + "name": "detail=detector_fps", + "field": lang["Detector Rate"], + "description": "How many frames a second to send to the motion detector; 2 is the default.", + "default": "2", + "example": "", + "possible": "" + }, + { + hidden: true, + "form-group-class": "h_det_input h_det_1", + "name": "detail=detector_scale_x", + "field": lang["Feed-in Image Width"], + "description": "Width of the image being detected. Smaller sizes take less CPU.", + "default": "", + "example": "640", + "possible": "" + }, + { + hidden: true, + "form-group-class": "h_det_input h_det_1", + "name": "detail=detector_scale_y", + "field": lang["Feed-in Image Height"], + "description": "Height of the image being detected. Smaller sizes take less CPU.", + "default": "", + "example": "480", + "possible": "" + }, { hidden: true, "name": "detail=detector_lock_timeout", @@ -2106,36 +2236,6 @@ module.exports = function(s,config,lang){ } ] }, - { - hidden: true, - "name": "detail=detector_fps", - "field": lang["Detector Rate"], - "description": "How many frames a second to send to the motion detector; 2 is the default.", - "default": "2", - "example": "", - "form-group-class": "h_det_input h_det_1", - "possible": "" - }, - { - hidden: true, - "name": "detail=detector_scale_x", - "field": lang["Feed-in Image Width"], - "description": "Width of the image being detected. Smaller sizes take less CPU.", - "default": "", - "example": "640", - "form-group-class": "h_det_input h_det_1", - "possible": "" - }, - { - hidden: true, - "name": "detail=detector_scale_y", - "field": lang["Feed-in Image Height"], - "description": "Height of the image being detected. Smaller sizes take less CPU.", - "default": "", - "example": "480", - "form-group-class": "h_det_input h_det_1", - "possible": "" - }, { hidden: true, "name": "detail=detector_record_method", @@ -2294,7 +2394,7 @@ module.exports = function(s,config,lang){ "class": "mdl-list" }, { - hidden: true, + hidden: true, "name": "detail=group_detector_multi", "field": "", "description": "", @@ -2403,6 +2503,7 @@ module.exports = function(s,config,lang){ "name": "detail=snap_seconds_inward", "field": lang['Delay for Snapshot'], "description": lang['in seconds'], + "form-group-class": "h_det_input h_det_1", "default": "0", }, { @@ -2431,7 +2532,6 @@ module.exports = function(s,config,lang){ "description": "The amount of time until a trigger is allowed to send another email with motion details and another image.", "default": "10", "example": "", - "form-group-class": "h_det_email_input h_det_email_1", "form-group-class-pre-layer": "h_det_input h_det_1", "possible": "" }, @@ -2575,37 +2675,37 @@ module.exports = function(s,config,lang){ } ] }, - { - "name": "detail=detector_show_matrix", - "field": lang["Show Matrices"], - "description": "Outline which pixels are detected as changed in one matrix.", - "default": "0", - "example": "", - "fieldType": "select", - "form-group-class": "h_det_pam_input h_det_pam_1", - "possible": [ - { - "name": lang.No, - "value": "0" - }, - { - "name": lang.Yes, - "value": "1" - } - ] - }, + // { + // "name": "detail=detector_show_matrix", + // "field": lang["Show Matrices"], + // "description": "Outline which pixels are detected as changed in one matrix.", + // "default": "0", + // "example": "", + // "fieldType": "select", + // "form-group-class": "h_det_pam_input h_det_pam_1", + // "possible": [ + // { + // "name": lang.No, + // "value": "0" + // }, + // { + // "name": lang.Yes, + // "value": "1" + // } + // ] + // }, { "name": "detail=detector_sensitivity", - "field": lang.Indifference, - "description": "This can mean multiple things depending on the detector used. Built-In Motion Detection defines this as \"Percentage Changed in View or Region\"", - "default": "0.5", + "field": lang['Minimum Change'], + "description": "The motion confidence rating must exceed this value to be seen as a trigger. This number correlates directly to the confidence rating returned by the motion detector. This option was previously named \"Indifference\".", + "default": "10", "example": "10", "possible": "" }, { "name": "detail=detector_max_sensitivity", - "field": lang["Max Indifference"], - "description": "An upperbound to indifference. Any value over this amount will be ignored.", + "field": lang["Maximum Change"], + "description": "The motion confidence rating must be lower than this value to be seen as a trigger. Leave blank for no maximum. This option was previously named \"Max Indifference\".", "default": "", "example": "75", "possible": "" @@ -2629,7 +2729,7 @@ module.exports = function(s,config,lang){ { "name": "detail=detector_frame", "field": lang["Full Frame Detection"], - "description": "This will read the entire frame for pixel differences.", + "description": "This will read the entire frame for pixel differences. This is the same as creating a region that covers the entire screen.", "default": "1", "example": "", "fieldType": "select", @@ -2936,6 +3036,26 @@ module.exports = function(s,config,lang){ } ] }, + { + hidden: true, + "name": "detail=detector_obj_count_in_region", + "field": lang["Count Objects only inside Regions"], + "description": "Count Objects only inside Regions.", + "default": "0", + "example": "", + "form-group-class": "h_det_count_input h_det_count_1", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, { "name": "detail=detector_obj_region", "field": lang['Require Object to be in Region'], @@ -2978,12 +3098,9 @@ module.exports = function(s,config,lang){ "name": "detail=detector_fps_object", "field": lang['Frame Rate'], "description": "", - "default": "1", + "default": "2", "example": "", - "form-group-class": "h_det_mot_fir_input h_det_mot_fir_1", - "form-group-class-pre-layer": "h_det_pam_input h_det_pam_1", - "fieldType": "number", - "numberMin": "1", + "form-group-class": "h_casc_input h_casc_1", "possible": "" }, { @@ -2993,8 +3110,7 @@ module.exports = function(s,config,lang){ "description": "", "default": "", "example": "", - "form-group-class": "h_det_mot_fir_input h_det_mot_fir_1", - "form-group-class-pre-layer": "h_det_pam_input h_det_pam_1", + "form-group-class": "h_casc_input h_casc_1", "fieldType": "number", "numberMin": "1", "possible": "" @@ -3006,8 +3122,7 @@ module.exports = function(s,config,lang){ "description": "", "default": "", "example": "", - "form-group-class": "h_det_mot_fir_input h_det_mot_fir_1", - "form-group-class-pre-layer": "h_det_pam_input h_det_pam_1", + "form-group-class": "h_casc_input h_casc_1", "fieldType": "number", "numberMin": "1", "possible": "" @@ -3324,6 +3439,53 @@ module.exports = function(s,config,lang){ "form-group-class": "h_cs_input h_cs_1", "possible": "" }, + { + "name": "detail=detector_ptz_follow", + "field": lang['PTZ Tracking'], + "description": "Follow the largest detected object with PTZ? Requires an Object Detector running or matrices provided with events.", + "default": "0", + "example": "", + "selector": "h_det_tracking", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + "name": "detail=detector_ptz_follow_target", + "field": lang['PTZ Tracking Target'], + "description": "", + "default": "person", + "example": "", + "form-group-class": "h_det_tracking_input h_det_tracking_1", + "possible": "" + }, + { + "name": "detail=detector_obj_count", + "field": lang["Count Objects"], + "description": "Count detected objects.", + "default": "0", + "example": "", + "selector": "h_det_count", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, { "name": "detail=control_url_center", "field": lang['Center'], @@ -3465,6 +3627,24 @@ module.exports = function(s,config,lang){ "form-group-class": "h_control_call_input h_control_call_GET h_control_call_PUT h_control_call_POST", "possible": "" }, + { + "name": "detail=control_invert_y", + "field": lang["Invert Y-Axis"], + "description": "For When your camera is mounted upside down or uses inverted vertical controls.", + "default": "0", + "example": "", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, ] }, "Grouping": { @@ -3489,7 +3669,6 @@ module.exports = function(s,config,lang){ "Copy Settings": { id: "monSectionCopying", "name": lang['Copy Settings'], - "color": "orange", isSection: true, "info": [ @@ -3851,6 +4030,43 @@ module.exports = function(s,config,lang){ "Account Settings": { "section": "Account Settings", "blocks": { + "ShinobiHub": { + "evaluation": "!details.sub && details.use_shinobihub !== '0'", + "name": lang["ShinobiHub"], + "color": "purple", + "info": [ + { + "name": "detail=shinobihub", + "selector":"autosave_shinobihub", + "field": lang.Autosave, + "description": "", + "default": "0", + "example": "", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + "hidden": true, + "field": lang['API Key'], + "name": "detail=shinobihub_key", + "placeholder": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "form-group-class": "autosave_shinobihub_input autosave_shinobihub_1", + "description": "", + "default": "", + "example": "", + "possible": "" + }, + ] + }, "2-Factor Authentication": { "name": lang['2-Factor Authentication'], "color": "grey", @@ -3910,7 +4126,7 @@ module.exports = function(s,config,lang){ } ], "form-group-class": "u_discord_bot_input u_discord_bot_1" - } + }, ] }, "Profile": { diff --git a/languages/en_CA.json b/languages/en_CA.json index 382edaa1..7fcd9d39 100644 --- a/languages/en_CA.json +++ b/languages/en_CA.json @@ -19,15 +19,41 @@ "Remember Me": "Remember Me", "Process Already Running": "Process Already Running", "Process Not Running": "Process Not Running", + "ShinobiHub": "ShinobiHub", + "Oldest": "Oldest", + "Newest": "Newest", + "Title": "Title", + "Subtitle": "Subtitle", + "Date Added": "Date Added", + "Date Updated": "Date Updated", + "Public on ShinobiHub": "Public on ShinobiHub", + "Uploaded Only": "Uploaded Only", + "Play": "Play", + "Pause": "Pause", "RAM": "RAM", "CPU": "CPU", "on": "on", + "OAuth Credentials": "OAuth Credentials", + "Token": "Token", + "OAuth Code": "OAuth Code", + "Google Drive": "Google Drive", + "Invert Y-Axis": "Invert Y-Axis", + "Get Code": "Get Code", + "PTZ Tracking": "PTZ Tracking", + "PTZ Tracking Target": "PTZ Tracking Target", + "Event Counts": "Event Counts", + "Already Processing": "Already Processing", + "No API Key": "No API Key", + "Use Camera Timestamps": "Use Camera Timestamps", "Power Viewer": "Power Viewer", "Power Video Viewer": "Power Video Viewer", "Time-lapse": "Time-lapse", "Montage": "Montage", + "Open All Monitors": "Open All Monitors", "Accounts": "Accounts", "Settings": "Settings", + "Count Objects only inside Regions": "Count Objects only inside Regions", + "Count Objects": "Count Objects", "Cards": "Cards", "No Region": "No Region", "Recording FPS": "Recording FPS", @@ -63,6 +89,9 @@ "Never": "Never", "API": "API", "ONVIF": "ONVIF", + "Set Home": "Set Home", + "Set Home Position (ONVIF-only)": "Set Home Position (ONVIF-only)", + "Non-Standard ONVIF": "Non-Standard ONVIF", "FFprobe": "Probe", "Monitor States": "Monitor States", "Schedule": "Schedule", @@ -174,6 +203,7 @@ "File Type": "File Type", "Filesize": "Filesize", "Video Status": "Video Status", + "Custom Auto Load": "Custom Auto Load", "Preferences": "Preferences", "Equal to": "Equal to", "Not Equal to": "Not Equal to", @@ -220,10 +250,14 @@ "Video stream only from first feed": "Video stream only from first feed", "Audio streams only": "Audio streams only", "Audio stream only from first feed": "Audio stream only from first feed", + "sorryNothingWasFound": "Sorry, nothing was found.", "ONVIF Port": "ONVIF Port", "ONVIF Scanner": "ONVIF Scanner", "ONVIFEventsNotAvailable": "ONVIF Events not Available", "ONVIFnotCompliantProfileT": "Camera is not ONVIF Profile T Compliant", + "ONVIFErr400": "Found ONVIF port but authorization failed when retrieving the Stream URL. Check username and password used for scan.", + "ONVIFErr405": "Method Not Allowed. Check username and password used for scan.", + "ONVIFErr404": "Not Found. This may just be the web panel for a network device.", "Scan Settings": "Scan Settings", "ONVIFnote": "Discover ONVIF devices on networks outside your own or leave it blank to scan your current network.
Username and Password can be left blank.", "Range or Single": "Range or Single", @@ -236,6 +270,8 @@ "Live Stream Toggle": "Live Stream Toggle", "RegionNote": "Points are only saved when you press Save on the Monitor Settings window.", "Points": "Points When adding points click on the edge of the polygon.", + "Minimum Change": "Minimum Change", + "Maximum Change": "Maximum Change", "Indifference": "Indifference", "Max Indifference": "Max Indifference", "Trigger Threshold": "Trigger Threshold", @@ -450,7 +486,15 @@ "StreamText": "

This section will designate the primary method of streaming out and its settings. This stream will be displayed in the dashboard. If you choose to use HLS, JPEG, or MJPEG then you can consume the stream through other programs.

Using JPEG stream essentially turns off the primary stream and uses the snapshot bin to get frames.

", "DetectorText": "

When the Width and Height boxes are shown you should set them to 640x480 or below. This will optimize the read speed of frames.

", "RecordingText": "It is recommended that you set Record File Type to WebMMP4 and Video Codec to libvpxcopy or libx264 because your Input Type is set to .", + "'Already Installing...'": "'Already Installing...'", + "Time Created": "Time Created", + "Last Modified": "Last Modified", "Mode": "Mode", + "Run Installer": "Run Installer", + "Install": "Install", + "Enable": "Enable", + "Disable": "Disable", + "Delete": "Delete", "Name": "Name", "Skip Ping": "Skip Ping", "Retry Connection": "Retry Connection Number of times allowed to fail", @@ -484,6 +528,7 @@ "Width": "Width", "Height": "Height", "Rotate": "Rotate", + "Trigger Event": "Trigger Event", "Primary Engine": "Primary Engine", "Video Filter": "Video Filter", "Font Path": "Font Path", @@ -492,7 +537,7 @@ "Text Box Color": "Text Box Color", "Position X": "Position X", "Position Y": "Position Y", - "Image Location": "Image Location Absolute Path or leave blank to use global", + "Image Location": "Image Location", "Image Position": "Image Position", "Frame Rate": "Frame Rate", "Image Width": "Image Width", @@ -501,9 +546,14 @@ "Notification Video Length": "Notification Video Length", "Video Codec": "Video Codec", "Delete Monitor States Preset": "Delete Monitor States Preset", + "Delete Schedule": "Delete Schedule", "Delete Monitor State?": "Delete Monitor State", - "deleteMonitorStateText1": "Do you want to delete this Monitor States Preset?", + "deleteScheduleText": "Do you want to delete this Schedule? Monitor Presets associated will not be modified..", + "deleteMonitorStateText1": "Do you want to delete this Monitor States Preset? The monitor configurations associated cannot be recovered.", "deleteMonitorStateText2": "Do you want to delete this Monitor's Preset?", + "undoAllUnsaveChanges": "Are you sure you want to do this? This will undo all unsaved changes.", + "monitorStatesError": "Monitor Presets Error", + "monitorStateNotEnoughChanges": "You need to make a change in your monitor configuration before attempting to add it to a Preset.", "Search Images": "Search Images", "Launch in New Window": "Launch in New Window", "Preset": "Preset", @@ -512,7 +562,7 @@ "sizePurgeLockedText": "The Size Purge Lock (deleteOverMax) appears to have failed to unlock. Unlocking now...", "Use coProcessor": "Use coProcessor", "Audio Codec": "Audio Codec", - "Video Record Rate": "Video Record Rate (FPS)", + "Video Record Rate": "Video Record Rate", "Record Width": "Record Width", "Record Height": "Record Height", "Double Quote Directory": "Double Quote Directory Some directories have spaces. Using this may crash some cameras.", @@ -520,6 +570,7 @@ "Record Video Filter": "Record Video Filter", "Input Flags": "Input Flags", "Snapshot Flags": "Snapshot Flags", + "Object Detector Flags": "Object Detector Flags", "Detector Flags": "Detector Flags", "Stream Flags": "Stream Flags", "Stream to YouTube": "Stream to YouTube", @@ -540,6 +591,7 @@ "Attach Video Clip": "Attach Video Clip", "Error While Decoding": "Error While Decoding", "ErrorWhileDecodingText": "Your hardware may have an unstable connection to the network. Check your network connections.", + "ErrorWhileDecodingTextAudio": "Your camera is providing broken data. Try disabling the Audio in the camera's internal settings.", "Discord": "Discord", "Discord Alert on Trigger": "Discord Alert on Trigger", "Allow Next Email": "Allow Next Email in Minutes", @@ -729,7 +781,7 @@ "Event": "Event", "CPU used by this stream": "CPU used by this stream", "Detector Buffer": "Detector Buffer", - "EventText1": "Triggered a motion event at", + "EventText1": "Triggered an event at", "EventText2": "Could not email image, file was not accessible", "MailError": "MAIL ERROR : Could not send email, Check conf.json. Skipping any features relying on mailing.", "updateKeyText1": "\"updateKey\" is missing from \"conf.json\", cannot do updates this way until you add it.", @@ -786,6 +838,7 @@ "Select a Monitor": "Select a Monitor", "Per Monitor": "Per Monitor", "Matrices": "Matrices", + "Preset Name": "Preset Name", "Show Matrices": "Show Matrices", "Show Matrix": "Show Matrix", "No Monitor ID Present in Form": "No Monitor ID Present in Form", @@ -890,6 +943,8 @@ "vda": "vda (Apple VDA Hardware Acceleration)", "videotoolbox": "videotoolbox", "cuvid": "cuvid (NVIDIA NVENC)", + "cuda": "cuda (NVIDIA NVENC)", + "opencl": "OpenCL", "Main": "Main", "Storage Location": "Storage Location", "Recommended": "Recommended", @@ -933,6 +988,7 @@ "Automatic":"Automatic", "Max Latency":"Max Latency", "Loop Stream":"Loop Stream", + "Object Count":"Object Count", "Object Tag":"Object Tag", "Noise Filter":"Noise Filter", "Noise Filter Range":"Noise Filter Range", @@ -942,6 +998,8 @@ "TV Channel Group":"TV Channel Group", "Emotion Average":"Emotion Average", "Require Object to be in Region":"Require Object to be in Region", + "Numeric criteria unsupported for Region tests, Ignoring Conditional":"Numeric criteria unsupported for Region tests, Ignoring Conditional", + "Text criteria unsupported for Object Count tests, Ignoring Conditional":"Text criteria unsupported for Object Count tests, Ignoring Conditional", "Show Regions of Interest":"Show Regions of Interest", "Confidence of Detection":"Confidence of Detection", "Edit Selected":"Edit Selected", diff --git a/libs/auth.js b/libs/auth.js index 81b43bab..f64543ff 100644 --- a/libs/auth.js +++ b/libs/auth.js @@ -8,14 +8,30 @@ module.exports = function(s,config,lang){ // var getUserByUid = function(params,columns,callback){ if(!columns)columns = '*' - s.sqlQuery(`SELECT ${columns} FROM Users WHERE uid=? AND ke=?`,[params.uid,params.ke],function(err,r){ + s.knexQuery({ + action: "select", + columns: columns, + table: "Users", + where: [ + ['uid','=',params.uid], + ['ke','=',params.ke], + ] + },(err,r) => { if(!r)r = [] var user = r[0] callback(err,user) }) } var getUserBySessionKey = function(params,callback){ - s.sqlQuery('SELECT * FROM Users WHERE auth=? AND ke=?',[params.auth,params.ke],function(err,r){ + s.knexQuery({ + action: "select", + columns: '*', + table: "Users", + where: [ + ['auth','=',params.auth], + ['ke','=',params.ke], + ] + },(err,r) => { if(!r)r = [] var user = r[0] callback(err,user) @@ -23,7 +39,18 @@ module.exports = function(s,config,lang){ } var loginWithUsernameAndPassword = function(params,columns,callback){ if(!columns)columns = '*' - s.sqlQuery(`SELECT ${columns} FROM Users WHERE mail=? AND (pass=? OR pass=?) LIMIT 1`,[params.username,params.password,s.createHash(params.password)],function(err,r){ + s.knexQuery({ + action: "select", + columns: columns, + table: "Users", + where: [ + ['mail','=',params.username], + ['pass','=',params.password], + ['or','mail','=',params.username], + ['pass','=',s.createHash(params.password)], + ], + limit: 1 + },(err,r) => { if(!r)r = [] var user = r[0] callback(err,user) @@ -31,7 +58,15 @@ module.exports = function(s,config,lang){ } var getApiKey = function(params,columns,callback){ if(!columns)columns = '*' - s.sqlQuery(`SELECT ${columns} FROM API WHERE code=? AND ke=?`,[params.auth,params.ke],function(err,r){ + s.knexQuery({ + action: "select", + columns: columns, + table: "API", + where: [ + ['code','=',params.auth], + ['ke','=',params.ke], + ] + },(err,r) => { if(!r)r = [] var apiKey = r[0] callback(err,apiKey) @@ -135,12 +170,12 @@ module.exports = function(s,config,lang){ activeSession && ( activeSession.ip.indexOf('0.0.0.0') > -1 || - activeSession.ip.indexOf(params.ip) > -1 + params.ip.indexOf(activeSession.ip) > -1 ) ){ if(!user.lang){ var details = s.parseJSON(user.details).lang - user.lang = s.getDefinitonFile(user.details.lang) || s.copySystemDefaultLanguage() + user.lang = s.getLanguageFile(user.details.lang) || s.copySystemDefaultLanguage() } onSuccessComplete(user) }else{ @@ -226,7 +261,14 @@ module.exports = function(s,config,lang){ } var foundUser = function(){ if(params.users === true){ - s.sqlQuery('SELECT * FROM Users WHERE details NOT LIKE ?',['%"sub"%'],function(err,r) { + s.knexQuery({ + action: "select", + columns: "*", + table: "Users", + where: [ + ['details','NOT LIKE','%"sub"%'], + ] + },(err,r) => { adminUsersSelected = r success() }) diff --git a/libs/basic.js b/libs/basic.js index 180f91de..b3c0d1ba 100644 --- a/libs/basic.js +++ b/libs/basic.js @@ -52,6 +52,11 @@ module.exports = function(s,config){ } return json } + s.stringContains = function(find,string,toLowerCase){ + var newString = string + '' + if(toLowerCase)newString = newString.toLowerCase() + return newString.indexOf(find) > -1 + } s.addUserPassToUrl = function(url,user,pass){ var splitted = url.split('://') splitted[1] = user + ':' + pass + '@' + splitted[1] @@ -64,6 +69,28 @@ module.exports = function(s,config){ } return x.replace('__DIR__',s.mainDirectory) } + s.mergeDeep = function(...objects) { + const isObject = obj => obj && typeof obj === 'object'; + + return objects.reduce((prev, obj) => { + Object.keys(obj).forEach(key => { + const pVal = prev[key]; + const oVal = obj[key]; + + if (Array.isArray(pVal) && Array.isArray(oVal)) { + prev[key] = pVal.concat(...oVal); + } + else if (isObject(pVal) && isObject(oVal)) { + prev[key] = s.mergeDeep(pVal, oVal); + } + else { + prev[key] = oVal; + } + }); + + return prev; + }, {}); + } s.md5 = function(x){return crypto.createHash('md5').update(x).digest("hex")} s.createHash = s.md5 switch(config.passwordType){ @@ -187,20 +214,27 @@ module.exports = function(s,config){ if(!e){e=''} if(config.systemLog===true){ if(typeof q==='string'&&s.databaseEngine){ - s.sqlQuery('INSERT INTO Logs (ke,mid,info) VALUES (?,?,?)',['$','$SYSTEM',s.s({type:q,msg:w})]); + s.knexQuery({ + action: "insert", + table: "Logs", + insert: { + ke: '$', + mid: '$SYSTEM', + info: s.s({type:q,msg:w}), + } + }) s.tx({f:'log',log:{time:s.timeObject(),ke:'$',mid:'$SYSTEM',time:s.timeObject(),info:s.s({type:q,msg:w})}},'$'); } return console.log(s.timeObject().format(),q,w,e) } } //system log - s.debugLog = function(q,w,e){ + s.debugLog = function(...args){ if(config.debugLog === true){ - if(!w){w = ''} - if(!e){e = ''} - console.log(s.timeObject().format(),q,w,e) + var logRow = ([s.timeObject().format()]).concat(...args) + console.log(...logRow) if(config.debugLogVerbose === true){ - console.log(new Error()) + console.log(new Error('VERBOSE STACK TRACE, THIS IS NOT AN ')) } } } @@ -221,15 +255,32 @@ module.exports = function(s,config){ break; case'delete': if(!e){return false;} - return exec('rm -f '+e,{detached: true},function(err){ - if(callback)callback(err) + fs.unlink(e,(err)=>{ + if(err){ + s.debugLog(err) + } + if(s.isWin){ + exec('rd /s /q "' + e + '"',{detached: true},function(err){ + if(callback)callback(err) + }) + }else{ + exec('rm -rf '+e,{detached: true},function(err){ + if(callback)callback(err) + }) + } }) break; case'deleteFolder': if(!e){return false;} - exec('rm -rf '+e,{detached: true},function(err){ - if(callback)callback(err) - }) + if(s.isWin){ + exec('rd /s /q "' + e + '"',{detached: true},function(err){ + if(callback)callback(err) + }) + }else{ + exec('rm -rf '+e,{detached: true},function(err){ + if(callback)callback(err) + }) + } break; case'deleteFiles': if(!e.age_type){e.age_type='min'};if(!e.age){e.age='1'}; @@ -283,7 +334,7 @@ module.exports = function(s,config){ } s.kilobyteToMegabyte = function(kb,places){ if(!places)places = 2 - return (kb/1000000).toFixed(places) + return (kb/1048576).toFixed(places) } Object.defineProperty(Array.prototype, 'chunk', { value: function(chunkSize){ diff --git a/libs/detector.js b/libs/cameraThread/detector.js similarity index 58% rename from libs/detector.js rename to libs/cameraThread/detector.js index 6a1f0204..6bf088e1 100644 --- a/libs/detector.js +++ b/libs/cameraThread/detector.js @@ -1,46 +1,64 @@ -// Matrix In Region Libs > -var SAT = require('sat') -var V = SAT.Vector; -var P = SAT.Polygon; -// Matrix In Region Libs /> var P2P = require('pipe2pam') -// pamDiff is based on https://www.npmjs.com/package/pam-diff var PamDiff = require('pam-diff') -module.exports = function(s,config){ - s.createPamDiffEngine = function(e){ +module.exports = function(jsonData,pamDiffResponder){ + var noiseFilterArray = {}; + const groupKey = jsonData.rawMonitorConfig.ke + const monitorId = jsonData.rawMonitorConfig.mid + const triggerTimer = {} + var pamDiff + var p2p + var writeToStderr = function(text){ + try{ + stdioWriters[2].write(Buffer.from(`${text}`, 'utf8' )) + // stdioWriters[2].write(Buffer.from(`${new Error('writeToStderr').stack}`, 'utf8' )) + }catch(err){ + fs.appendFileSync('/home/Shinobi/test.log',text + '\n','utf8') + } + } + if(typeof pamDiffResponder === 'function'){ + var sendDetectedData = function(detectorObject){ + pamDiffResponder(detectorObject) + } + }else{ + var sendDetectedData = function(detectorObject){ + pamDiffResponder.write(Buffer.from(JSON.stringify(detectorObject))) + } + } + createPamDiffEngine = function(){ var width, height, globalSensitivity, globalColorThreshold, fullFrame = false - if(s.group[e.ke].rawMonitorConfigurations[e.id].details.detector_scale_x===''||s.group[e.ke].rawMonitorConfigurations[e.id].details.detector_scale_y===''){ - width = s.group[e.ke].rawMonitorConfigurations[e.id].details.detector_scale_x; - height = s.group[e.ke].rawMonitorConfigurations[e.id].details.detector_scale_y; - }else{ - width = e.width - height = e.height + if(jsonData.rawMonitorConfig.details.detector_scale_x===''||jsonData.rawMonitorConfig.details.detector_scale_y===''){ + width = jsonData.rawMonitorConfig.details.detector_scale_x; + height = jsonData.rawMonitorConfig.details.detector_scale_y; } - if(e.details.detector_sensitivity===''){ + else{ + width = jsonData.rawMonitorConfig.width + height = jsonData.rawMonitorConfig.height + } + if(jsonData.rawMonitorConfig.details.detector_sensitivity===''){ globalSensitivity = 10 }else{ - globalSensitivity = parseInt(e.details.detector_sensitivity) + globalSensitivity = parseInt(jsonData.rawMonitorConfig.details.detector_sensitivity) } - if(e.details.detector_color_threshold===''){ + if(jsonData.rawMonitorConfig.details.detector_color_threshold===''){ globalColorThreshold = 9 }else{ - globalColorThreshold = parseInt(e.details.detector_color_threshold) + globalColorThreshold = parseInt(jsonData.rawMonitorConfig.details.detector_color_threshold) } - globalThreshold = parseInt(e.details.detector_threshold) || 0 + globalThreshold = parseInt(jsonData.rawMonitorConfig.details.detector_threshold) || 0 var regionJson try{ - regionJson = JSON.parse(s.group[e.ke].rawMonitorConfigurations[e.id].details.cords) + regionJson = JSON.parse(jsonData.rawMonitorConfig.details.cords) }catch(err){ - regionJson = s.group[e.ke].rawMonitorConfigurations[e.id].details.cords + regionJson = jsonData.rawMonitorConfig.details.cords } - if(Object.keys(regionJson).length === 0 || e.details.detector_frame === '1'){ + if(Object.keys(regionJson).length === 0 || jsonData.rawMonitorConfig.details.detector_frame === '1'){ fullFrame = { name:'FULL_FRAME', sensitivity:globalSensitivity, @@ -54,26 +72,25 @@ module.exports = function(s,config){ } } - e.triggerTimer = {} - var regions = s.createPamDiffRegionArray(regionJson,globalColorThreshold,globalSensitivity,fullFrame) + var regions = createPamDiffRegionArray(regionJson,globalColorThreshold,globalSensitivity,fullFrame) var pamDiffOptions = { grayscale: 'luminosity', regions : regions.forPam } - if(e.details.detector_show_matrix==='1'){ + if(jsonData.rawMonitorConfig.details.detector_show_matrix==='1'){ pamDiffOptions.response = 'bounds' } - s.group[e.ke].activeMonitors[e.id].pamDiff = new PamDiff(pamDiffOptions); - s.group[e.ke].activeMonitors[e.id].p2p = new P2P() + pamDiff = new PamDiff(pamDiffOptions) + p2p = new P2P() var regionArray = Object.values(regionJson) - if(config.detectorMergePamRegionTriggers === true){ + if(jsonData.globalInfo.config.detectorMergePamRegionTriggers === true){ // merge pam triggers for performance boost var buildTriggerEvent = function(trigger){ var detectorObject = { f:'trigger', - id:e.id, - ke:e.ke, + id:monitorId, + ke:groupKey, name:trigger.name, details:{ plug:'built-in', @@ -82,8 +99,8 @@ module.exports = function(s,config){ confidence:trigger.percent }, plates:[], - imgHeight:e.details.detector_scale_y, - imgWidth:e.details.detector_scale_x + imgHeight:jsonData.rawMonitorConfig.details.detector_scale_y, + imgWidth:jsonData.rawMonitorConfig.details.detector_scale_x } if(trigger.merged){ if(trigger.matrices)detectorObject.details.matrices = trigger.matrices @@ -91,13 +108,13 @@ module.exports = function(s,config){ var filteredCountSuccess = 0 trigger.merged.forEach(function(triggerPiece){ var region = regionArray.find(x => x.name == triggerPiece.name) - s.checkMaximumSensitivity(e, region, detectorObject, function(err1) { - s.checkTriggerThreshold(e, region, detectorObject, function(err2) { + checkMaximumSensitivity(region, detectorObject, function(err1) { + checkTriggerThreshold(region, detectorObject, function(err2) { ++filteredCount if(!err1 && !err2)++filteredCountSuccess if(filteredCount === trigger.merged.length && filteredCountSuccess > 0){ - detectorObject.doObjectDetection = (s.isAtleatOneDetectorPluginConnected && e.details.detector_use_detect_object === '1') - s.triggerEvent(detectorObject) + detectorObject.doObjectDetection = (jsonData.globalInfo.isAtleatOneDetectorPluginConnected && jsonData.rawMonitorConfig.details.detector_use_detect_object === '1') + sendDetectedData(detectorObject) } }) }) @@ -105,38 +122,36 @@ module.exports = function(s,config){ }else{ if(trigger.matrix)detectorObject.details.matrices = [trigger.matrix] var region = regionArray.find(x => x.name == detectorObject.name) - s.checkMaximumSensitivity(e, region, detectorObject, function(err1) { - s.checkTriggerThreshold(e, region, detectorObject, function(err2) { + checkMaximumSensitivity(region, detectorObject, function(err1) { + checkTriggerThreshold(region, detectorObject, function(err2) { if(!err1 && !err2){ - detectorObject.doObjectDetection = (s.isAtleatOneDetectorPluginConnected && e.details.detector_use_detect_object === '1') - s.triggerEvent(detectorObject) + detectorObject.doObjectDetection = (jsonData.globalInfo.isAtleatOneDetectorPluginConnected && jsonData.rawMonitorConfig.details.detector_use_detect_object === '1') + sendDetectedData(detectorObject) } }) }) } } - if(e.details.detector_noise_filter==='1'){ - if(!s.group[e.ke].activeMonitors[e.id].noiseFilterArray)s.group[e.ke].activeMonitors[e.id].noiseFilterArray = {} - var noiseFilterArray = s.group[e.ke].activeMonitors[e.id].noiseFilterArray + if(jsonData.rawMonitorConfig.details.detector_noise_filter==='1'){ Object.keys(regions.notForPam).forEach(function(name){ if(!noiseFilterArray[name])noiseFilterArray[name]=[]; }) - s.group[e.ke].activeMonitors[e.id].pamDiff.on('diff', (data) => { + pamDiff.on('diff', (data) => { var filteredCount = 0 var filteredCountSuccess = 0 data.trigger.forEach(function(trigger){ - s.filterTheNoise(e,noiseFilterArray,regions,trigger,function(err){ + filterTheNoise(noiseFilterArray,regions,trigger,function(err){ ++filteredCount if(!err)++filteredCountSuccess if(filteredCount === data.trigger.length && filteredCountSuccess > 0){ - buildTriggerEvent(s.mergePamTriggers(data)) + buildTriggerEvent(mergePamTriggers(data)) } }) }) }) }else{ - s.group[e.ke].activeMonitors[e.id].pamDiff.on('diff', (data) => { - buildTriggerEvent(s.mergePamTriggers(data)) + pamDiff.on('diff', (data) => { + buildTriggerEvent(mergePamTriggers(data)) }) } }else{ @@ -145,8 +160,8 @@ module.exports = function(s,config){ var buildTriggerEvent = function(trigger){ var detectorObject = { f:'trigger', - id:e.id, - ke:e.ke, + id: monitorId, + ke: groupKey, name:trigger.name, details:{ plug:'built-in', @@ -155,38 +170,36 @@ module.exports = function(s,config){ confidence:trigger.percent }, plates:[], - imgHeight:e.details.detector_scale_y, - imgWidth:e.details.detector_scale_x + imgHeight:jsonData.rawMonitorConfig.details.detector_scale_y, + imgWidth:jsonData.rawMonitorConfig.details.detector_scale_x } if(trigger.matrix)detectorObject.details.matrices = [trigger.matrix] var region = Object.values(regionJson).find(x => x.name == detectorObject.name) - s.checkMaximumSensitivity(e, region, detectorObject, function(err1) { - s.checkTriggerThreshold(e, region, detectorObject, function(err2) { + checkMaximumSensitivity(region, detectorObject, function(err1) { + checkTriggerThreshold(region, detectorObject, function(err2) { if(!err1 && ! err2){ - detectorObject.doObjectDetection = (s.isAtleatOneDetectorPluginConnected && e.details.detector_use_detect_object === '1') - s.triggerEvent(detectorObject) + detectorObject.doObjectDetection = (jsonData.globalInfo.isAtleatOneDetectorPluginConnected && jsonData.rawMonitorConfig.details.detector_use_detect_object === '1') + sendDetectedData(detectorObject) } }) }) } - if(e.details.detector_noise_filter==='1'){ - if(!s.group[e.ke].activeMonitors[e.id].noiseFilterArray)s.group[e.ke].activeMonitors[e.id].noiseFilterArray = {} - var noiseFilterArray = s.group[e.ke].activeMonitors[e.id].noiseFilterArray + if(jsonData.rawMonitorConfig.details.detector_noise_filter==='1'){ Object.keys(regions.notForPam).forEach(function(name){ if(!noiseFilterArray[name])noiseFilterArray[name]=[]; }) - s.group[e.ke].activeMonitors[e.id].pamDiff.on('diff', (data) => { + pamDiff.on('diff', (data) => { data.trigger.forEach(function(trigger){ - s.filterTheNoise(e,noiseFilterArray,regions,trigger,function(){ - s.createMatrixFromPamTrigger(trigger) + filterTheNoise(noiseFilterArray,regions,trigger,function(){ + createMatrixFromPamTrigger(trigger) buildTriggerEvent(trigger) }) }) }) }else{ - s.group[e.ke].activeMonitors[e.id].pamDiff.on('diff', (data) => { + pamDiff.on('diff', (data) => { data.trigger.forEach(function(trigger){ - s.createMatrixFromPamTrigger(trigger) + createMatrixFromPamTrigger(trigger) buildTriggerEvent(trigger) }) }) @@ -194,7 +207,7 @@ module.exports = function(s,config){ } } - s.createPamDiffRegionArray = function(regions,globalColorThreshold,globalSensitivity,fullFrame){ + createPamDiffRegionArray = function(regions,globalColorThreshold,globalSensitivity,fullFrame){ var pamDiffCompliantArray = [], arrayForOtherStuff = [], json @@ -236,11 +249,11 @@ module.exports = function(s,config){ return {forPam:pamDiffCompliantArray,notForPam:arrayForOtherStuff}; } - s.filterTheNoise = function(e,noiseFilterArray,regions,trigger,callback){ + filterTheNoise = function(noiseFilterArray,regions,trigger,callback){ if(noiseFilterArray[trigger.name].length > 2){ var thePreviousTriggerPercent = noiseFilterArray[trigger.name][noiseFilterArray[trigger.name].length - 1]; var triggerDifference = trigger.percent - thePreviousTriggerPercent; - var noiseRange = e.details.detector_noise_filter_range + var noiseRange = jsonData.rawMonitorConfig.details.detector_noise_filter_range if(!noiseRange || noiseRange === ''){ noiseRange = 6 } @@ -267,48 +280,48 @@ module.exports = function(s,config){ } } - s.checkMaximumSensitivity = function(monitor, region, detectorObject, callback) { + checkMaximumSensitivity = function(region, detectorObject, callback) { var logName = detectorObject.id + ':' + detectorObject.name - var globalMaxSensitivity = parseInt(monitor.details.detector_max_sensitivity) || undefined + var globalMaxSensitivity = parseInt(jsonData.rawMonitorConfig.details.detector_max_sensitivity) || undefined var maxSensitivity = parseInt(region.max_sensitivity) || globalMaxSensitivity if (maxSensitivity === undefined || detectorObject.details.confidence <= maxSensitivity) { callback(null) } else { callback(true) - if (monitor.triggerTimer[detectorObject.name] !== undefined) { - clearTimeout(monitor.triggerTimer[detectorObject.name].timeout) - monitor.triggerTimer[detectorObject.name] = undefined + if (triggerTimer[detectorObject.name] !== undefined) { + clearTimeout(triggerTimer[detectorObject.name].timeout) + triggerTimer[detectorObject.name] = undefined } } } - s.checkTriggerThreshold = function(monitor, region, detectorObject, callback){ + checkTriggerThreshold = function(region, detectorObject, callback){ var threshold = parseInt(region.threshold) || globalThreshold if (threshold <= 1) { callback(null) } else { - if (monitor.triggerTimer[detectorObject.name] === undefined) { - monitor.triggerTimer[detectorObject.name] = { + if (triggerTimer[detectorObject.name] === undefined) { + triggerTimer[detectorObject.name] = { count : threshold, timeout : null } } - if (--monitor.triggerTimer[detectorObject.name].count == 0) { + if (--triggerTimer[detectorObject.name].count == 0) { callback(null) - clearTimeout(monitor.triggerTimer[detectorObject.name].timeout) - monitor.triggerTimer[detectorObject.name] = undefined + clearTimeout(triggerTimer[detectorObject.name].timeout) + triggerTimer[detectorObject.name] = undefined } else { callback(true) - var fps = parseFloat(monitor.details.detector_fps) || 2 - if (monitor.triggerTimer[detectorObject.name].timeout !== null) - clearTimeout(monitor.triggerTimer[detectorObject.name].timeout) - monitor.triggerTimer[detectorObject.name].timeout = setTimeout(function() { - monitor.triggerTimer[detectorObject.name] = undefined + var fps = parseFloat(jsonData.rawMonitorConfig.details.detector_fps) || 2 + if (triggerTimer[detectorObject.name].timeout !== null) + clearTimeout(triggerTimer[detectorObject.name].timeout) + triggerTimer[detectorObject.name].timeout = setTimeout(function() { + triggerTimer[detectorObject.name] = undefined }, ((threshold+0.5) * 1000) / fps) } } } - s.mergePamTriggers = function(data){ + mergePamTriggers = function(data){ if(data.trigger.length > 1){ var n = 0 var sum = 0 @@ -318,7 +331,7 @@ module.exports = function(s,config){ name.push(trigger.name + ' ('+trigger.percent+'%)') ++n sum += trigger.percent - s.createMatrixFromPamTrigger(trigger) + createMatrixFromPamTrigger(trigger) if(trigger.matrix)matrices.push(trigger.matrix) }) var average = sum / n @@ -332,47 +345,12 @@ module.exports = function(s,config){ } }else{ var trigger = data.trigger[0] - s.createMatrixFromPamTrigger(trigger) + createMatrixFromPamTrigger(trigger) trigger.matrices = [trigger.matrix] } return trigger } - s.isAtleastOneMatrixInRegion = function(regions,matrices,callback){ - var regionPolys = [] - var matrixPoints = [] - regions.forEach(function(region,n){ - var polyPoints = [] - region.points.forEach(function(point){ - polyPoints.push(new V(parseInt(point[0]),parseInt(point[1]))) - }) - regionPolys[n] = new P(new V(0,0), polyPoints) - }) - var collisions = [] - var foundInRegion = false - matrices.forEach(function(matrix){ - var matrixPoints = [ - new V(matrix.x,matrix.y), - new V(matrix.width,matrix.y), - new V(matrix.width,matrix.height), - new V(matrix.x,matrix.height) - ] - var matrixPoly = new P(new V(0,0), matrixPoints) - regionPolys.forEach(function(region,n){ - var response = new SAT.Response() - var collided = SAT.testPolygonPolygon(matrixPoly, region, response) - if(collided === true){ - collisions.push({ - matrix: matrix, - region: regions[n] - }) - foundInRegion = true - } - }) - }) - if(callback)callback(foundInRegion,collisions) - return foundInRegion - } - s.createMatrixFromPamTrigger = function(trigger){ + createMatrixFromPamTrigger = function(trigger){ if( trigger.minX && trigger.maxX && @@ -396,4 +374,15 @@ module.exports = function(s,config){ } return trigger } + + return function(cameraProcess,fallback){ + if(jsonData.rawMonitorConfig.details.detector === '1' && jsonData.rawMonitorConfig.coProcessor === false){ + //frames from motion detect + if(jsonData.rawMonitorConfig.details.detector_pam === '1'){ + createPamDiffEngine() + + cameraProcess.stdio[3].pipe(p2p).pipe(pamDiff) + } + } + }; } diff --git a/libs/cameraThread/singleCamera.js b/libs/cameraThread/singleCamera.js new file mode 100644 index 00000000..b922ecf3 --- /dev/null +++ b/libs/cameraThread/singleCamera.js @@ -0,0 +1,213 @@ +const fs = require('fs') +const request = require('request') +const exec = require('child_process').exec +const spawn = require('child_process').spawn +const isWindows = (process.platform === 'win32' || process.platform === 'win64') +process.send = process.send || function () {}; + +if(!process.argv[2] || !process.argv[3]){ + return writeToStderr('Missing FFMPEG Command String or no command operator') +} +var jsonData = JSON.parse(fs.readFileSync(process.argv[3],'utf8')) +const ffmpegAbsolutePath = process.argv[2].trim() +const ffmpegCommandString = jsonData.cmd +const rawMonitorConfig = jsonData.rawMonitorConfig +const stdioPipes = jsonData.pipes || [] +var newPipes = [] +var stdioWriters = []; + +var writeToStderr = function(text){ + try{ + process.stderr.write(Buffer.from(`${text}`, 'utf8' )) + // stdioWriters[2].write(Buffer.from(`${new Error('writeToStderr').stack}`, 'utf8' )) + }catch(err){ + // fs.appendFileSync('/home/Shinobi/test.log',text + '\n','utf8') + } +} + +const buildMonitorUrl = function(e,noPath){ + var authd = '' + var url + if(e.details.muser&&e.details.muser!==''&&e.host.indexOf('@')===-1) { + e.username = e.details.muser + e.password = e.details.mpass + authd = e.details.muser+':'+e.details.mpass+'@' + } + if(e.port==80&&e.details.port_force!=='1'){e.porty=''}else{e.porty=':'+e.port} + url = e.protocol+'://'+authd+e.host+e.porty + if(noPath !== true)url += e.path + return url +} + +// [CTRL] + [C] = exit +process.on('uncaughtException', function (err) { + writeToStderr('Uncaught Exception occured!'); + writeToStderr(err.stack); +}); +const exitAction = function(){ + try{ + if(isWindows){ + spawn("taskkill", ["/pid", cameraProcess.pid, '/f', '/t']) + }else{ + process.kill(-cameraProcess.pid) + } + }catch(err){ + + } +} +process.on('SIGTERM', exitAction); +process.on('SIGINT', exitAction); +process.on('exit', exitAction); + +for(var i=0; i < stdioPipes; i++){ + switch(i){ + case 0: + newPipes[i] = 'pipe' + break; + case 1: + newPipes[i] = 1 + break; + case 2: + newPipes[i] = 2 + break; + case 3: + stdioWriters[i] = fs.createWriteStream(null, {fd: i, end:false}); + if(rawMonitorConfig.details.detector === '1' && rawMonitorConfig.details.detector_pam === '1'){ + newPipes[i] = 'pipe' + }else{ + newPipes[i] = stdioWriters[i] + } + break; + case 5: + stdioWriters[i] = fs.createWriteStream(null, {fd: i, end:false}); + newPipes[i] = 'pipe' + break; + default: + stdioWriters[i] = fs.createWriteStream(null, {fd: i, end:false}); + newPipes[i] = stdioWriters[i] + break; + } +} +stdioWriters.forEach((writer)=>{ + writer.on('error', (err) => { + writeToStderr(err.stack); + }); +}) +writeToStderr(JSON.stringify(ffmpegCommandString)) +var cameraProcess = spawn(ffmpegAbsolutePath,ffmpegCommandString,{detached: true,stdio:newPipes}) +cameraProcess.on('close',()=>{ + writeToStderr('Process Closed') + stdioWriters.forEach((writer)=>{ + writer.end() + }) + process.exit(); +}) +cameraProcess.stdio[5].on('data',(data)=>{ + stdioWriters[5].write(data) +}) +writeToStderr('Thread Opening') + + + +if(rawMonitorConfig.details.detector === '1' && rawMonitorConfig.details.detector_pam === '1'){ + try{ + const attachPamDetector = require(__dirname + '/detector.js')(jsonData,stdioWriters[3]) + attachPamDetector(cameraProcess,(err)=>{ + writeToStderr(err) + }) + }catch(err){ + writeToStderr(err.stack) + } +} + +if(rawMonitorConfig.type === 'jpeg'){ + var recordingSnapRequest + var recordingSnapper + var errorTimeout + var errorCount = 0 + var capture_fps = parseFloat(rawMonitorConfig.details.sfps || 1) + if(isNaN(capture_fps))capture_fps = 1 + try{ + cameraProcess.stdio[0].on('error',function(err){ + if(err && rawMonitorConfig.details.loglevel !== 'quiet'){ + // s.userLog(e,{type:'STDIN ERROR',msg:err}); + } + }) + }catch(err){ + writeToStderr(err.stack) + } + setTimeout(() => { + if(!cameraProcess.stdio[0])return writeToStderr('No Camera Process Found for Snapper'); + const captureOne = function(f){ + recordingSnapRequest = request({ + url: buildMonitorUrl(rawMonitorConfig), + method: 'GET', + encoding: null, + timeout: 15000 + },function(err,data){ + if(err){ + writeToStderr(JSON.stringify(err)) + return; + } + // writeToStderr(data.body.length) + cameraProcess.stdio[0].write(data.body) + recordingSnapper = setTimeout(function(){ + captureOne() + },1000 / capture_fps) + if(!errorTimeout){ + clearTimeout(errorTimeout) + errorTimeout = setTimeout(function(){ + errorCount = 0; + delete(errorTimeout) + },3000) + } + }).on('error', function(err){ + ++errorCount + clearTimeout(errorTimeout) + errorTimeout = null + writeToStderr(JSON.stringify(err)) + if(rawMonitorConfig.details.loglevel !== 'quiet'){ + // s.userLog(e,{ + // type: lang['JPEG Error'], + // msg: { + // msg: lang.JPEGErrorText, + // info: err + // } + // }); + switch(err.code){ + case'ESOCKETTIMEDOUT': + case'ETIMEDOUT': + // ++s.group[e.ke].activeMonitors[e.id].errorSocketTimeoutCount + // if( + // rawMonitorConfig.details.fatal_max !== 0 && + // s.group[e.ke].activeMonitors[e.id].errorSocketTimeoutCount > rawMonitorConfig.details.fatal_max + // ){ + // // s.userLog(e,{type:lang['Fatal Maximum Reached'],msg:{code:'ESOCKETTIMEDOUT',msg:lang.FatalMaximumReachedText}}); + // // s.camera('stop',e) + // }else{ + // // s.userLog(e,{type:lang['Restarting Process'],msg:{code:'ESOCKETTIMEDOUT',msg:lang.FatalMaximumReachedText}}); + // // s.camera('restart',e) + // } + // return; + break; + } + } + // if(rawMonitorConfig.details.fatal_max !== 0 && errorCount > rawMonitorConfig.details.fatal_max){ + // clearTimeout(recordingSnapper) + // process.exit() + // } + }) + } + captureOne() + },5000) +} + +if( + rawMonitorConfig.type === 'dashcam' || + rawMonitorConfig.type === 'socket' +){ + process.stdin.on('data',(data) => { + //confirmed receiving data this way. + cameraProcess.stdin.write(data) + }) +} diff --git a/libs/cameraThread/snapshot.js b/libs/cameraThread/snapshot.js new file mode 100644 index 00000000..4e06b647 --- /dev/null +++ b/libs/cameraThread/snapshot.js @@ -0,0 +1,60 @@ +const fs = require('fs') +const spawn = require('child_process').spawn +const isWindows = process.platform === "win32"; +var writeToStderr = function(text){ + // fs.appendFileSync(rawMonitorConfig.sdir + 'errors.log',text + '\n','utf8') + process.stderr.write(Buffer.from(`${text}`, 'utf8' )) +} +if(!process.argv[2] || !process.argv[3]){ + return writeToStderr('Missing FFMPEG Command String or no command operator') +} +process.send = process.send || function () {}; +process.on('uncaughtException', function (err) { + writeToStderr('Uncaught Exception occured!'); + writeToStderr(err.stack); +}); +// [CTRL] + [C] = exit +const exitAction = function(){ + if(isWindows){ + spawn("taskkill", ["/pid", snapProcess.pid, '/f', '/t']) + }else{ + try{ + process.kill(-snapProcess.pid) + }catch(err){ + + } + } +} +process.on('SIGTERM', exitAction); +process.on('SIGINT', exitAction); +process.on('exit', exitAction); + +var jsonData = JSON.parse(fs.readFileSync(process.argv[3],'utf8')) +// fs.unlink(process.argv[3],()=>{}) +const ffmpegAbsolutePath = process.argv[2].trim() +const ffmpegCommandString = jsonData.cmd +const temporaryImageFile = jsonData.temporaryImageFile +const iconImageFile = jsonData.iconImageFile +const useIcon = jsonData.useIcon +const rawMonitorConfig = jsonData.rawMonitorConfig +// var writeToStderr = function(text){ +// process.stderr.write(Buffer.from(text)) +// } + +var snapProcess = spawn(ffmpegAbsolutePath,ffmpegCommandString,{detached: true}) +snapProcess.stderr.on('data',(data)=>{ + writeToStderr(data.toString()) +}) +snapProcess.stdout.on('data',(data)=>{ + writeToStderr(data.toString()) +}) +snapProcess.on('close',function(data){ + if(useIcon){ + var fileCopy = fs.createReadStream(temporaryImageFile).pipe(fs.createWriteStream(iconImageFile)) + fileCopy.on('close',function(){ + process.exit(); + }) + }else{ + process.exit(); + } +}); diff --git a/libs/childNode.js b/libs/childNode.js index 9bd492ae..e7207d65 100644 --- a/libs/childNode.js +++ b/libs/childNode.js @@ -3,6 +3,7 @@ var http = require('http'); var https = require('https'); var express = require('express'); module.exports = function(s,config,lang,app,io){ + const { cameraDestroy } = require('./monitor/utils.js')(s,config,lang) //setup Master for childNodes if(config.childNodes.enabled === true && config.childNodes.mode === 'master'){ s.childNodes = {}; @@ -66,6 +67,11 @@ module.exports = function(s,config,lang,app,io){ cn.emit('c',{f:'sqlCallback',rows:rows,err:err,callbackId:d.callbackId}); }); break; + case'knex': + s.knexQuery(d.options,function(err,rows){ + cn.emit('c',{f:'sqlCallback',rows:rows,err:err,callbackId:d.callbackId}); + }); + break; case'clearCameraFromActiveList': if(s.childNodes[ipAddress])delete(s.childNodes[ipAddress].activeCameras[d.ke + d.id]) break; @@ -145,16 +151,16 @@ module.exports = function(s,config,lang,app,io){ dir : s.getVideoDirectory(d.d), file : d.filename, filename : d.filename, - filesizeMB : parseFloat((d.filesize/1000000).toFixed(2)) + filesizeMB : parseFloat((d.filesize/1048576).toFixed(2)) } s.insertDatabaseRow(d.d,insert) s.insertCompletedVideoExtensions.forEach(function(extender){ extender(d.d,insert) }) //purge over max - s.purgeDiskForGroup(d) + s.purgeDiskForGroup(d.ke) //send new diskUsage values - s.setDiskUsedForGroup(d,insert.filesizeMB) + s.setDiskUsedForGroup(d.ke,insert.filesizeMB) clearTimeout(s.group[d.ke].activeMonitors[d.mid].recordingChecker) clearTimeout(s.group[d.ke].activeMonitors[d.mid].streamChecker) break; @@ -213,11 +219,19 @@ module.exports = function(s,config,lang,app,io){ s.queuedSqlCallbacks[callbackId] = onMoveOn s.cx({f:'sql',query:query,values:values,callbackId:callbackId}); } - setInterval(function(){ - s.cpuUsage(function(cpu){ - s.cx({f:'cpu',cpu:parseFloat(cpu)}) + s.knexQuery = function(options,onMoveOn){ + var callbackId = s.gid() + if(typeof onMoveOn !== 'function'){onMoveOn=function(){}} + s.queuedSqlCallbacks[callbackId] = onMoveOn + s.cx({f:'knex',options:options,callbackId:callbackId}); + } + setInterval(async () => { + const cpu = await s.cpuUsage() + s.cx({ + f: 'cpu', + cpu: parseFloat(cpu) }) - },2000) + },5000) childIO.on('connect', function(d){ console.log('CHILD CONNECTION SUCCESS') s.cx({ @@ -241,7 +255,7 @@ module.exports = function(s,config,lang,app,io){ break; case'kill': s.initiateMonitorObject(d.d); - s.cameraDestroy(s.group[d.d.ke].activeMonitors[d.d.id].spawn,d.d) + cameraDestroy(d.d) var childNodeIp = s.group[d.d.ke].activeMonitors[d.d.id] break; case'sync': diff --git a/libs/codeTester.js b/libs/codeTester.js index 94d596fd..78bc36c1 100644 --- a/libs/codeTester.js +++ b/libs/codeTester.js @@ -46,7 +46,7 @@ module.exports = function(s,config,lang){ } ],null,3)) setTimeout(function(){ - require(s.mainDirectory + '/test/run.js')(s,config,lang,io) + require('../test/run.js')(s,config,lang,io) },500) } } diff --git a/libs/commander.js b/libs/commander.js new file mode 100644 index 00000000..d6e7e01a --- /dev/null +++ b/libs/commander.js @@ -0,0 +1,48 @@ +module.exports = function(s,config,lang){ + if(config.p2pEnabled){ + const { Worker, isMainThread } = require('worker_threads'); + const startWorker = () => { + // set the first parameter as a string. + const pathToWorkerScript = __dirname + '/commander/worker.js' + const workerProcess = new Worker(pathToWorkerScript) + workerProcess.on('message',function(data){ + switch(data.f){ + case'debugLog': + s.debugLog(...data.data) + break; + case'systemLog': + s.systemLog(...data.data) + break; + } + }) + setTimeout(() => { + workerProcess.postMessage({ + f: 'init', + config: config, + lang: lang + }) + },2000) + // workerProcess is an Emitter. + // it also contains a direct handle to the `spawn` at `workerProcess.spawnProcess` + return workerProcess + } + config.machineId = config.p2pApiKey + '' + config.p2pGroupId + config.p2pTargetAuth = config.p2pTargetAuth || s.gid(30) + if(config.p2pTargetGroupId && config.p2pTargetUserId){ + startWorker() + }else{ + s.knexQuery({ + action: "select", + columns: "ke,uid", + table: "Users", + where: [], + limit: 1 + },(err,r) => { + const firstUser = r[0] + config.p2pTargetUserId = firstUser.uid + config.p2pTargetGroupId = firstUser.ke + startWorker() + }) + } + } +} diff --git a/libs/commander/worker.js b/libs/commander/worker.js new file mode 100644 index 00000000..48d26344 --- /dev/null +++ b/libs/commander/worker.js @@ -0,0 +1,230 @@ +const { parentPort } = require('worker_threads'); +const request = require('request'); +const socketIOClient = require('socket.io-client'); +const p2pClientConnectionStaticName = 'Commander' +const p2pClientConnections = {} +const runningRequests = {} +const connectedUserWebSockets = {} +const s = { + debugLog: (...args) => { + parentPort.postMessage({ + f: 'debugLog', + data: args + }) + }, + systemLog: (...args) => { + parentPort.postMessage({ + f: 'systemLog', + data: args + }) + }, +} +parentPort.on('message',(data) => { + switch(data.f){ + case'init': + initialize(data.config,data.lang) + break; + } +}) + +const initialize = (config,lang) => { + if(!config.p2pHost)config.p2pHost = 'ws://163.172.180.205:8084' + const parseJSON = function(string){ + var parsed = string + try{ + parsed = JSON.parse(string) + }catch(err){ + + } + return parsed + } + const createQueryStringFromObject = function(obj){ + var queryString = '' + var keys = Object.keys(obj) + keys.forEach(function(key){ + var value = obj[key] + queryString += `&${key}=${value}` + }) + return queryString + } + const doRequest = function(url,method,data,callback,onDataReceived){ + var requestEndpoint = `${config.sslEnabled ? `https` : 'http'}://localhost:${config.sslEnabled ? config.ssl.port : config.port}` + url + if(method === 'GET' && data){ + requestEndpoint += '?' + createQueryStringFromObject(data) + } + return request(requestEndpoint,{ + method: method, + json: method !== 'GET' ? (data ? data : null) : null + }, function(err,resp,body){ + // var json = parseJSON(body) + if(err)console.error(err,data) + callback(err,body,resp) + }).on('data', function(data) { + onDataReceived(data) + }) + } + const createShinobiSocketConnection = (connectionId) => { + const masterConnectionToMachine = socketIOClient(`ws://localhost:${config.port}`, {transports:['websocket']}) + p2pClientConnections[connectionId || p2pClientConnectionStaticName] = masterConnectionToMachine + return masterConnectionToMachine + } + // + s.debugLog('p2p',`Connecting to ${config.p2pHost}...`) + const connectionToP2PServer = socketIOClient(config.p2pHost, {transports:['websocket']}); + if(!config.p2pApiKey){ + s.systemLog('p2p',`Please fill 'p2pApiKey' in your conf.json.`) + } + if(!config.p2pGroupId){ + s.systemLog('p2p',`Please fill 'p2pGroupId' in your conf.json.`) + } + connectionToP2PServer.on('connect', () => { + s.systemLog('p2p',`Connected ${config.p2pHost}!`) + connectionToP2PServer.emit('initMachine',{ + port: config.port, + apiKey: config.p2pApiKey, + groupId: config.p2pGroupId, + targetUserId: config.p2pTargetUserId, + targetGroupId: config.p2pTargetGroupId, + subscriptionId: config.subscriptionId || 'notActivated' + }) + }) + connectionToP2PServer.on('httpClose',(requestId) => { + if(runningRequests[requestId] && runningRequests[requestId].abort){ + runningRequests[requestId].abort() + delete(runningRequests[requestId]) + } + }) + connectionToP2PServer.on('http',(rawRequest) => { + runningRequests[rawRequest.rid] = doRequest( + rawRequest.url, + rawRequest.method, + rawRequest.data, + function(err,json,resp){ + connectionToP2PServer.emit('httpResponse',{ + err: err, + json: rawRequest.bodyOnEnd ? json : null, + rid: rawRequest.rid + }) + }, + (data) => { + if(!rawRequest.bodyOnEnd)connectionToP2PServer.emit('httpResponseChunk',{ + data: data, + rid: rawRequest.rid + }) + }) + }) + const masterConnectionToMachine = createShinobiSocketConnection() + masterConnectionToMachine.on('connect', () => { + masterConnectionToMachine.emit('f',{ + f: 'init', + auth: config.p2pTargetAuth, + ke: config.p2pTargetGroupId, + uid: config.p2pTargetUserId + }) + }) + masterConnectionToMachine.on('f',(data) => { + connectionToP2PServer.emit('f',data) + }) + + connectionToP2PServer.on('wsInit',(rawRequest) => { + s.debugLog('p2pWsInit',rawRequest) + const user = rawRequest.user + const clientConnectionToMachine = createShinobiSocketConnection(rawRequest.cnid) + connectedUserWebSockets[user.auth_token] = user; + clientConnectionToMachine.on('connect', () => { + s.debugLog('init',user.auth_token) + clientConnectionToMachine.emit('f',{ + f: 'init', + auth: user.auth_token, + ke: user.ke, + uid: user.uid, + }) + }); + ([ + 'f', + ]).forEach((target) => { + connectionToP2PServer.on(target,(data) => { + clientConnectionToMachine.emit(target,data) + }) + clientConnectionToMachine.on(target,(data) => { + connectionToP2PServer.emit(target,{data: data, cnid: rawRequest.cnid}) + }) + }) + }); + ([ + 'a', + 'r', + 'gps', + 'e', + 'super', + ]).forEach((target) => { + connectionToP2PServer.on(target,(data) => { + var clientConnectionToMachine + if(data.f === 'init'){ + clientConnectionToMachine = createShinobiSocketConnection(data.cnid) + clientConnectionToMachine.on('connect', () => { + clientConnectionToMachine.on(target,(fromData) => { + connectionToP2PServer.emit(target,{data: fromData, cnid: data.cnid}) + }) + clientConnectionToMachine.on('f',(fromData) => { + connectionToP2PServer.emit('f',{data: fromData, cnid: data.cnid}) + }) + clientConnectionToMachine.emit(target,data) + }); + }else{ + clientConnectionToMachine = p2pClientConnections[data.cnid] + clientConnectionToMachine.emit(target,data) + } + }) + + }); + ([ + 'h265', + 'Base64', + 'FLV', + 'MP4', + ]).forEach((target) => { + connectionToP2PServer.on(target,(initData) => { + if(connectedUserWebSockets[initData.auth]){ + const clientConnectionToMachine = createShinobiSocketConnection(initData.auth + initData.ke + initData.id) + clientConnectionToMachine.on('connect', () => { + clientConnectionToMachine.emit(target,initData) + }); + clientConnectionToMachine.on('data',(data) => { + connectionToP2PServer.emit('data',{data: data, cnid: initData.cnid}) + }); + }else{ + s.debugLog('disconnect now!') + } + }) + }); + connectionToP2PServer.on('wsDestroyStream',(clientKey) => { + if(p2pClientConnections[clientKey]){ + p2pClientConnections[clientKey].disconnect(); + } + delete(p2pClientConnections[clientKey]) + }); + connectionToP2PServer.on('wsDestroy',(rawRequest) => { + if(p2pClientConnections[rawRequest.cnid]){ + p2pClientConnections[rawRequest.cnid].disconnect(); + } + delete(p2pClientConnections[rawRequest.cnid]) + }); + + connectionToP2PServer.on('allowDisconnect',(bool) => { + connectionToP2PServer.allowDisconnect = true; + connectionToP2PServer.disconnect() + s.debugLog('p2p','Server Forced Disconnection') + }); + const onDisconnect = () => { + s.systemLog('p2p','Disconnected') + if(!connectionToP2PServer.allowDisconnect){ + s.systemLog('p2p','Attempting Reconnection...') + setTimeout(() => { + connectionToP2PServer.connect() + },3000) + } + } + connectionToP2PServer.on('error',onDisconnect) + connectionToP2PServer.on('disconnect',onDisconnect) +} diff --git a/libs/common.js b/libs/common.js new file mode 100644 index 00000000..ce8cab4f --- /dev/null +++ b/libs/common.js @@ -0,0 +1,11 @@ +const async = require("async"); +exports.copyObject = (obj) => { + return Object.assign({},obj) +} +exports.createQueue = (timeoutInSeconds, queueItemsRunningInParallel) => { + return async.queue(function(action, callback) { + setTimeout(function(){ + action(callback) + },timeoutInSeconds * 1000 || 1000) + },queueItemsRunningInParallel || 3) +} diff --git a/libs/config.js b/libs/config.js index 10d1e6fb..a2c77e18 100644 --- a/libs/config.js +++ b/libs/config.js @@ -4,7 +4,11 @@ module.exports = function(s){ config : s.mainDirectory+'/conf.json', languages : s.mainDirectory+'/languages' } - var config = require(s.location.config); + try{ + var config = require(s.location.config) + }catch(err){ + var config = {} + } if(!config.productType){ config.productType = 'CE' } diff --git a/libs/control.js b/libs/control.js new file mode 100644 index 00000000..812a231c --- /dev/null +++ b/libs/control.js @@ -0,0 +1,6 @@ +var os = require('os'); +var exec = require('child_process').exec; +module.exports = function(s,config,lang,app,io){ + require('./control/onvif.js')(s,config,lang,app,io) + // const ptz = require('./control/ptz.js')(s,config,lang,app,io) +} diff --git a/libs/control/onvif.js b/libs/control/onvif.js new file mode 100644 index 00000000..9b6abab4 --- /dev/null +++ b/libs/control/onvif.js @@ -0,0 +1,145 @@ +var os = require('os'); +var exec = require('child_process').exec; +var onvif = require("node-onvif"); +module.exports = function(s,config,lang,app,io){ + const createOnvifDevice = async (onvifAuth) => { + var response = {ok: false} + const monitorConfig = s.group[onvifAuth.ke].rawMonitorConfigurations[onvifAuth.id] + const controlBaseUrl = monitorConfig.details.control_base_url || s.buildMonitorUrl(monitorConfig, true) + const controlURLOptions = s.cameraControlOptionsFromUrl(controlBaseUrl,monitorConfig) + //create onvif connection + const device = new onvif.OnvifDevice({ + address : controlURLOptions.host + ':' + controlURLOptions.port, + user : controlURLOptions.username, + pass : controlURLOptions.password + }) + s.group[onvifAuth.ke].activeMonitors[onvifAuth.id].onvifConnection = device + try{ + const info = await device.init() + response.ok = true + response.device = device + }catch(err){ + response.msg = 'Device responded with an error' + response.error = err + } + return response + } + const replaceDynamicInOptions = (Camera,options) => { + const newOptions = {} + Object.keys(options).forEach((key) => { + const value = options[key] + if(typeof value === 'string'){ + newOptions[key] = value.replace(/__CURRENT_TOKEN/g,Camera.current_profile.token) + }else if(value !== undefined && value !== null){ + newOptions[key] = value + } + }) + return newOptions + } + const runOnvifMethod = async (onvifOptions,callback) => { + var onvifAuth = onvifOptions.auth + var response = {ok: false} + var errorMessage = function(msg,error){ + response.ok = false + response.msg = msg + response.error = error + callback(response) + } + var actionCallback = function(onvifActionResponse){ + response.ok = true + if(onvifActionResponse.data){ + response.responseFromDevice = onvifActionResponse.data + }else{ + response.responseFromDevice = onvifActionResponse + } + if(onvifActionResponse.soap)response.soap = onvifActionResponse.soap + callback(response) + } + var isEmpty = function(obj) { + for(var key in obj) { + if(obj.hasOwnProperty(key)) + return false; + } + return true; + } + var doAction = function(Camera){ + var completeAction = function(command){ + if(command && command.then){ + command.then(actionCallback).catch(function(error){ + errorMessage('Device Action responded with an error',error) + }) + }else if(command){ + response.ok = true + response.repsonseFromDevice = command + callback(response) + }else{ + response.error = 'Big Errors, Please report it to Shinobi Development' + callback(response) + } + } + var action + if(onvifAuth.service){ + if(Camera.services[onvifAuth.service] === undefined){ + return errorMessage('This is not an available service. Please use one of the following : '+Object.keys(Camera.services).join(', ')) + } + if(Camera.services[onvifAuth.service] === null){ + return errorMessage('This service is not activated. Maybe you are not connected through ONVIF. You can test by attempting to use the "Control" feature with ONVIF in Shinobi.') + } + action = Camera.services[onvifAuth.service][onvifAuth.action] + }else{ + action = Camera[onvifAuth.action] + } + if(!action || typeof action !== 'function'){ + errorMessage(onvifAuth.action+' is not an available ONVIF function. See https://github.com/futomi/node-onvif for functions.') + }else{ + var argNames = s.getFunctionParamNames(action) + var options + var command + if(argNames[0] === 'options' || argNames[0] === 'params'){ + options = replaceDynamicInOptions(Camera,onvifOptions.options || {}) + response.options = options + } + if(onvifAuth.service){ + command = Camera.services[onvifAuth.service][onvifAuth.action](options) + }else{ + command = Camera[onvifAuth.action](options) + } + completeAction(command) + } + } + if(!s.group[onvifAuth.ke].activeMonitors[onvifAuth.id].onvifConnection){ + const response = await createOnvifDevice(onvifAuth) + if(response.ok){ + doAction(response.device) + }else{ + errorMessage(response.msg,response.error) + } + }else{ + doAction(s.group[onvifAuth.ke].activeMonitors[onvifAuth.id].onvifConnection) + } + } + /** + * API : ONVIF Method Controller + */ + app.all([ + config.webPaths.apiPrefix+':auth/onvif/:ke/:id/:action', + config.webPaths.apiPrefix+':auth/onvif/:ke/:id/:service/:action' + ],function (req,res){ + s.auth(req.params,function(user){ + const options = s.getPostData(req,'options',true) || s.getPostData(req,'params',true) + runOnvifMethod({ + auth: { + ke: req.params.ke, + id: req.params.id, + action: req.params.action, + service: req.params.service, + }, + options: options, + },(endData) => { + s.closeJsonResponse(res,endData) + }) + },res,req); + }) + s.createOnvifDevice = createOnvifDevice + s.runOnvifMethod = runOnvifMethod +} diff --git a/libs/control/ptz.js b/libs/control/ptz.js new file mode 100644 index 00000000..f023750f --- /dev/null +++ b/libs/control/ptz.js @@ -0,0 +1,382 @@ +var os = require('os'); +var exec = require('child_process').exec; +var request = require('request') +module.exports = function(s,config,lang){ + const moveLock = {} + const ptzTimeoutsUntilResetToHome = {} + const startMove = async function(options,callback){ + const device = s.group[options.ke].activeMonitors[options.id].onvifConnection + if(!device){ + const response = await s.createOnvifDevice({ + ke: options.ke, + id: options.id, + }) + const device = s.group[options.ke].activeMonitors[options.id].onvifConnection + } + options.controlOptions.ProfileToken = device.current_profile.token + s.runOnvifMethod({ + auth: { + ke: options.ke, + id: options.id, + action: 'continuousMove', + service: 'ptz', + }, + options: options.controlOptions, + },callback) + } + const stopMove = function(options,callback){ + const device = s.group[options.ke].activeMonitors[options.id].onvifConnection + s.runOnvifMethod({ + auth: { + ke: options.ke, + id: options.id, + action: 'stop', + service: 'ptz', + }, + options: { + 'PanTilt': true, + 'Zoom': true, + ProfileToken: device.current_profile.token + }, + },callback) + } + const moveOnvifCamera = function(options,callback){ + const monitorConfig = s.group[options.ke].rawMonitorConfigurations[options.id] + const invertedVerticalAxis = monitorConfig.details.control_invert_y === '1' + const controlUrlStopTimeout = parseInt(monitorConfig.details.control_url_stop_timeout) || 1000 + switch(options.direction){ + case'center': + callback({type:'Center button inactive'}) + break; + case'stopMove': + callback({type:'Control Trigger Ended'}) + stopMove({ + ke: options.ke, + id: options.id, + },(response) => { + + }) + break; + default: + try{ + var controlOptions = { + Velocity : {} + } + if(options.axis){ + options.axis.forEach((axis) => { + controlOptions.Velocity[axis.direction] = axis.amount + }) + }else{ + var onvifDirections = { + "left": [-1.0,'x'], + "right": [1.0,'x'], + "down": [invertedVerticalAxis ? 1.0 : -1.0,'y'], + "up": [invertedVerticalAxis ? -1.0 : 1.0,'y'], + "zoom_in": [1.0,'z'], + "zoom_out": [-1.0,'z'] + } + var direction = onvifDirections[options.direction] + controlOptions.Velocity[direction[1]] = direction[0] + } + (['x','y','z']).forEach(function(axis){ + if(!controlOptions.Velocity[axis]) + controlOptions.Velocity[axis] = 0 + }) + if(monitorConfig.details.control_stop === '1'){ + startMove({ + ke: options.ke, + id: options.id, + controlOptions: controlOptions + },(response) => { + if(response.ok){ + if(controlUrlStopTimeout != '0'){ + setTimeout(function(){ + stopMove({ + ke: options.ke, + id: options.id, + },(response) => { + if(!response.ok){ + console.log(error) + } + }) + callback({type: 'Control Triggered'}) + },controlUrlStopTimeout) + } + }else{ + s.debugLog(response) + } + }) + }else{ + controlOptions.Speed = {'x': 1, 'y': 1, 'z': 1} + controlOptions.Translation = Object.assign(controlOptions.Velocity,{}) + delete(controlOptions.Velocity) + s.runOnvifMethod({ + auth: { + ke: options.ke, + id: options.id, + action: 'relativeMove', + service: 'ptz', + }, + options: controlOptions, + },(response) => { + if(response.ok){ + callback({type: 'Control Triggered'}) + }else{ + callback({type: 'Control Triggered', error: response.error}) + } + }) + } + }catch(err){ + console.log(err) + console.log(new Error()) + } + break; + } + } + const ptzControl = async function(options,callback){ + if(!s.group[options.ke] || !s.group[options.ke].activeMonitors[options.id]){return} + const monitorConfig = s.group[options.ke].rawMonitorConfigurations[options.id] + const controlUrlMethod = monitorConfig.details.control_url_method || 'GET' + const controlBaseUrl = monitorConfig.details.control_base_url || s.buildMonitorUrl(monitorConfig, true) + if(monitorConfig.details.control !== "1"){ + s.userLog(e,{type:lang['Control Error'],msg:lang.ControlErrorText1}); + return + } + if(monitorConfig.details.control_url_stop_timeout === '0' && monitorConfig.details.control_stop === '1' && s.group[options.ke].activeMonitors[options.id].ptzMoving === true){ + options.direction = 'stopMove' + s.group[options.ke].activeMonitors[options.id].ptzMoving = false + }else{ + s.group[options.ke].activeMonitors[options.id].ptzMoving = true + } + if(controlUrlMethod === 'ONVIF'){ + try{ + //create onvif connection + if( + !s.group[options.ke].activeMonitors[options.id].onvifConnection || + !s.group[options.ke].activeMonitors[options.id].onvifConnection.current_profile || + !s.group[options.ke].activeMonitors[options.id].onvifConnection.current_profile.token + ){ + const response = await s.createOnvifDevice({ + ke: options.ke, + id: options.id, + }) + if(response.ok){ + moveOnvifCamera({ + ke: options.ke, + id: options.id, + direction: options.direction, + axis: options.axis, + },(msg) => { + msg.msg = options.direction + callback(msg) + }) + }else{ + s.userLog(e,{type:lang['Control Error'],msg:response.error}) + } + }else{ + moveOnvifCamera({ + ke: options.ke, + id: options.id, + direction: options.direction, + axis: options.axis, + },(msg) => { + if(!msg.msg)msg.msg = {direction: options.direction} + callback(msg) + }) + } + }catch(err){ + s.debugLog(err) + callback({ + type: lang['Control Error'], + msg: { + msg: lang.ControlErrorText2, + error: err, + direction: options.direction + } + }) + } + }else{ + const controlUrlStopTimeout = parseInt(monitorConfig.details.control_url_stop_timeout) || 1000 + var stopCamera = function(){ + let stopURL = controlBaseUrl + monitorConfig.details[`control_url_${options.direction}_stop`] + let controlOptions = s.cameraControlOptionsFromUrl(stopURL,monitorConfig) + let requestOptions = { + url : stopURL, + method : controlOptions.method, + auth : { + user : controlOptions.username, + pass : controlOptions.password + } + } + if(monitorConfig.details.control_digest_auth === '1'){ + requestOptions.sendImmediately = true + } + request(requestOptions,function(err,data){ + const msg = { + ok: true, + type:'Control Trigger Ended' + } + if(err){ + msg.ok = false + msg.type = 'Control Error' + msg.msg = err + } + callback(msg) + s.userLog(e,msg); + }) + } + if(options.direction === 'stopMove'){ + stopCamera() + }else{ + let controlURL = controlBaseUrl + monitorConfig.details[`control_url_${options.direction}`] + let controlOptions = s.cameraControlOptionsFromUrl(controlURL,monitorConfig) + let requestOptions = { + url: controlURL, + method: controlOptions.method, + auth: { + user: controlOptions.username, + pass: controlOptions.password + } + } + if(monitorConfig.details.control_digest_auth === '1'){ + requestOptions.sendImmediately = true + } + request(requestOptions,function(err,data){ + if(err){ + callback({ok:false,type:'Control Error',msg:err}) + return + } + if(monitorConfig.details.control_stop == '1' && options.direction !== 'center' ){ + s.userLog(e,{type:'Control Triggered Started'}); + if(controlUrlStopTimeout > 0){ + setTimeout(function(){ + stopCamera() + },controlUrlStopTimeout) + } + }else{ + callback({ok:true,type:'Control Triggered'}) + } + }) + } + } + } + const getPresetPositions = (options,callback) => { + const profileToken = options.ProfileToken || "__CURRENT_TOKEN" + return s.runOnvifMethod({ + auth: { + ke: options.ke, + id: options.id, + service: 'ptz', + action: 'getPresets', + }, + options: { + ProfileToken: profileToken + }, + },callback) + } + const setPresetForCurrentPosition = (options,callback) => { + const nonStandardOnvif = s.group[options.ke].rawMonitorConfigurations[options.id].details.onvif_non_standard === '1' + const profileToken = options.ProfileToken || "__CURRENT_TOKEN" + s.runOnvifMethod({ + auth: { + ke: options.ke, + id: options.id, + service: 'ptz', + action: 'setPreset', + }, + options: { + ProfileToken: profileToken, + PresetToken: nonStandardOnvif ? null : options.PresetToken || profileToken, + PresetName: options.PresetName || nonStandardOnvif ? '1' : profileToken + }, + },(endData) => { + callback(endData) + }) + } + const moveToPresetPosition = (options,callback) => { + const nonStandardOnvif = s.group[options.ke].rawMonitorConfigurations[options.id].details.onvif_non_standard === '1' + const profileToken = options.ProfileToken || "__CURRENT_TOKEN" + return s.runOnvifMethod({ + auth: { + ke: options.ke, + id: options.id, + service: 'ptz', + action: 'gotoPreset', + }, + options: { + ProfileToken: profileToken, + PresetToken: options.PresetToken || nonStandardOnvif ? '1' : profileToken, + Speed: { + "x": 1, + "y": 1, + "z": 1 + }, + }, + },callback) + } + const getLargestMatrix = (matrices) => { + var largestMatrix = {width: 0, height: 0} + matrices.forEach((matrix) => { + if(matrix.width > largestMatrix.width && matrix.height > largestMatrix.height)largestMatrix = matrix + }) + return largestMatrix.x ? largestMatrix : null + } + const moveCameraPtzToMatrix = function(event,trackingTarget){ + if(moveLock[event.ke + event.id])return; + clearTimeout(moveLock[event.ke + event.id]) + moveLock[event.ke + event.id] = setTimeout(() => { + delete(moveLock[event.ke + event.id]) + },1000) + const imgHeight = event.details.imgHeight + const imgWidth = event.details.imgWidth + const thresholdX = imgWidth * 0.125 + const thresholdY = imgHeight * 0.125 + const imageCenterX = imgWidth / 2 + const imageCenterY = imgHeight / 2 + const matrices = event.details.matrices + const largestMatrix = getLargestMatrix(matrices.filter(matrix => matrix.tag === (trackingTarget || 'person'))) + // console.log(matrices.find(matrix => matrix.tag === 'person')) + if(!largestMatrix)return; + const matrixCenterX = largestMatrix.x + (largestMatrix.width / 2) + const matrixCenterY = largestMatrix.y + (largestMatrix.height / 2) + const rawDistanceX = (matrixCenterX - imageCenterX) + const rawDistanceY = (matrixCenterY - imageCenterY) + const distanceX = imgWidth / rawDistanceX + const distanceY = imgHeight / rawDistanceY + const axisX = rawDistanceX > thresholdX || rawDistanceX < -thresholdX ? distanceX : 0 + const axisY = largestMatrix.y < 30 && largestMatrix.height > imgHeight * 0.8 ? 0.5 : rawDistanceY > thresholdY || rawDistanceY < -thresholdY ? -distanceY : 0 + if(axisX !== 0 || axisY !== 0){ + ptzControl({ + axis: [ + {direction: 'x', amount: axisX === 0 ? 0 : axisX > 0 ? 0.01 : -0.01}, + {direction: 'y', amount: axisY === 0 ? 0 : axisY > 0 ? 0.01 : -0.01}, + {direction: 'z', amount: 0}, + ], + // axis: [{direction: 'x', amount: 1.0}], + id: event.id, + ke: event.ke + },(msg) => { + s.userLog(event,msg) + // console.log(msg) + clearTimeout(ptzTimeoutsUntilResetToHome[event.ke + event.id]) + ptzTimeoutsUntilResetToHome[event.ke + event.id] = setTimeout(() => { + moveToPresetPosition({ + ke: event.ke, + id: event.id, + },(endData) => { + console.log(endData) + }) + },7000) + }) + } + } + return { + ptzControl: ptzControl, + startMove: startMove, + stopMove: stopMove, + getPresetPositions: getPresetPositions, + setPresetForCurrentPosition: setPresetForCurrentPosition, + moveToPresetPosition: moveToPresetPosition, + moveCameraPtzToMatrix: moveCameraPtzToMatrix + } +} diff --git a/libs/customAutoLoad.js b/libs/customAutoLoad.js index 5860b0bd..af950262 100644 --- a/libs/customAutoLoad.js +++ b/libs/customAutoLoad.js @@ -1,174 +1,463 @@ -var fs = require('fs') -var express = require('express') -module.exports = function(s,config,lang,app,io){ - function mergeDeep(...objects) { - const isObject = obj => obj && typeof obj === 'object'; - - return objects.reduce((prev, obj) => { - Object.keys(obj).forEach(key => { - const pVal = prev[key]; - const oVal = obj[key]; - - if (Array.isArray(pVal) && Array.isArray(oVal)) { - prev[key] = pVal.concat(...oVal); - } - else if (isObject(pVal) && isObject(oVal)) { - prev[key] = mergeDeep(pVal, oVal); - } - else { - prev[key] = oVal; - } - }); - - return prev; - }, {}); +const fs = require('fs-extra'); +const express = require('express') +const request = require('request') +const unzipper = require('unzipper') +const fetch = require("node-fetch") +const spawn = require('child_process').spawn +module.exports = async (s,config,lang,app,io) => { + const runningInstallProcesses = {} + const modulesBasePath = s.mainDirectory + '/libs/customAutoLoad/' + const searchText = function(searchFor,searchIn){ + return searchIn.indexOf(searchFor) > -1 } - s.customAutoLoadModules = {} - s.customAutoLoadTree = { - pages: [], - PageBlocks: [], - LibsJs: [], - LibsCss: [], - adminPageBlocks: [], - adminLibsJs: [], - adminLibsCss: [], - superPageBlocks: [], - superLibsJs: [], - superLibsCss: [] + const extractNameFromPackage = (filePath) => { + const filePathParts = filePath.split('/') + const packageName = filePathParts[filePathParts.length - 1].split('.')[0] + return packageName } - var folderPath = s.mainDirectory + '/libs/customAutoLoad' - var search = function(searchFor,searchIn){return searchIn.indexOf(searchFor) > -1} - fs.readdir(folderPath,function(err,folderContents){ - if(!err && folderContents){ - folderContents.forEach(function(filename){ - s.customAutoLoadModules[filename] = {} - var customModulePath = folderPath + '/' + filename - if(filename.indexOf('.js') > -1){ - s.customAutoLoadModules[filename].type = 'file' - try{ - require(customModulePath)(s,config,lang,app,io) - }catch(err){ - console.log('Failed to Load Module : ' + filename) - console.log(err) - } - }else{ - if(fs.lstatSync(customModulePath).isDirectory()){ - s.customAutoLoadModules[filename].type = 'folder' - try{ - require(customModulePath)(s,config,lang,app,io) - fs.readdir(customModulePath,function(err,folderContents){ - folderContents.forEach(function(name){ - switch(name){ - case'web': - var webFolder = s.checkCorrectPathEnding(customModulePath) + 'web/' - fs.readdir(webFolder,function(err,webFolderContents){ - webFolderContents.forEach(function(name){ - switch(name){ - case'libs': - case'pages': - if(name === 'libs'){ - if(config.webPaths.home !== '/'){ - app.use('/libs',express.static(webFolder + '/libs')) - } - app.use(s.checkCorrectPathEnding(config.webPaths.home)+'libs',express.static(webFolder + '/libs')) - app.use(s.checkCorrectPathEnding(config.webPaths.admin)+'libs',express.static(webFolder + '/libs')) - app.use(s.checkCorrectPathEnding(config.webPaths.super)+'libs',express.static(webFolder + '/libs')) - } - var libFolder = webFolder + name + '/' - fs.readdir(libFolder,function(err,webFolderContents){ - webFolderContents.forEach(function(libName){ - var thirdLevelName = libFolder + libName - switch(libName){ - case'js': - case'css': - case'blocks': - fs.readdir(thirdLevelName,function(err,webFolderContents){ - webFolderContents.forEach(function(filename){ - var fullPath = thirdLevelName + '/' + filename - var blockPrefix = '' - switch(true){ - case search('super.',filename): - blockPrefix = 'super' - break; - case search('admin.',filename): - blockPrefix = 'admin' - break; - } - switch(libName){ - case'js': - s.customAutoLoadTree[blockPrefix + 'LibsJs'].push(filename) - break; - case'css': - s.customAutoLoadTree[blockPrefix + 'LibsCss'].push(filename) - break; - case'blocks': - s.customAutoLoadTree[blockPrefix + 'PageBlocks'].push(fullPath) - break; - } - }) - }) - break; - default: - if(libName.indexOf('.ejs') > -1){ - s.customAutoLoadTree.pages.push(thirdLevelName) - } - break; - } - }) - }) - break; - } - }) - }) - break; - case'languages': - var languagesFolder = s.checkCorrectPathEnding(customModulePath) + 'languages/' - fs.readdir(languagesFolder,function(err,files){ - if(err)return console.log(err); - files.forEach(function(filename){ - var fileData = require(languagesFolder + filename) - var rule = filename.replace('.json','') - if(config.language === rule){ - lang = Object.assign(lang,fileData) - } - if(s.loadedLanguages[rule]){ - s.loadedLanguages[rule] = Object.assign(s.loadedLanguages[rule],fileData) - }else{ - s.loadedLanguages[rule] = Object.assign(s.copySystemDefaultLanguage(),fileData) - } - }) - }) - break; - case'definitions': - var definitionsFolder = s.checkCorrectPathEnding(customModulePath) + 'definitions/' - fs.readdir(definitionsFolder,function(err,files){ - if(err)return console.log(err); - files.forEach(function(filename){ - var fileData = require(definitionsFolder + filename) - var rule = filename.replace('.json','').replace('.js','') - if(config.language === rule){ - s.definitions = mergeDeep(s.definitions,fileData) - } - if(s.loadedDefinitons[rule]){ - s.loadedDefinitons[rule] = mergeDeep(s.loadedDefinitons[rule],fileData) - }else{ - s.loadedDefinitons[rule] = mergeDeep(s.copySystemDefaultDefinitions(),fileData) - } - }) - }) - break; - } - }) + const getModulePath = (name) => { + return modulesBasePath + name + '/' + } + const getModule = (moduleName) => { + const modulePath = modulesBasePath + moduleName + const stats = fs.lstatSync(modulePath) + const isDirectory = stats.isDirectory() + const newModule = { + name: moduleName, + path: modulePath + '/', + size: stats.size, + lastModified: stats.mtime, + created: stats.ctime, + isDirectory: isDirectory, + } + if(isDirectory){ + var hasInstaller = false + if(!fs.existsSync(modulePath + '/index.js')){ + hasInstaller = true + newModule.noIndex = true + } + if(fs.existsSync(modulePath + '/package.json')){ + hasInstaller = true + newModule.properties = getModuleProperties(moduleName) + }else{ + newModule.properties = { + name: moduleName + } + } + newModule.hasInstaller = hasInstaller + }else{ + newModule.isIgnitor = (moduleName.indexOf('.js') > -1) + newModule.properties = { + name: moduleName + } + } + return newModule + } + const getModules = (asArray) => { + const foundModules = {} + fs.readdirSync(modulesBasePath).forEach((moduleName) => { + foundModules[moduleName] = getModule(moduleName) + }) + return asArray ? Object.values(foundModules) : foundModules + } + const downloadModule = (downloadUrl,packageName) => { + const downloadPath = modulesBasePath + packageName + fs.mkdirSync(downloadPath) + return new Promise(async (resolve, reject) => { + fs.mkdir(downloadPath, () => { + request(downloadUrl).pipe(fs.createWriteStream(downloadPath + '.zip')) + .on('finish',() => { + zip = fs.createReadStream(downloadPath + '.zip') + .pipe(unzipper.Parse()) + .on('entry', async (file) => { + if(file.type === 'Directory'){ + try{ + fs.mkdirSync(modulesBasePath + file.path, { recursive: true }) + }catch(err){ + + } + }else{ + const content = await file.buffer(); + fs.writeFile(modulesBasePath + file.path,content,(err) => { + if(err)console.log(err) }) - }catch(err){ - console.log('Failed to Load Module : ' + filename) - console.log(err) } + }) + .promise() + .then(() => { + fs.remove(downloadPath + '.zip', () => {}) + resolve() + }) + }) + }) + }) + } + const getModuleProperties = (name) => { + const modulePath = getModulePath(name) + const propertiesPath = modulePath + 'package.json' + const properties = fs.existsSync(propertiesPath) ? s.parseJSON(fs.readFileSync(propertiesPath)) : { + name: name + } + return properties + } + const installModule = (name) => { + return new Promise((resolve, reject) => { + if(!runningInstallProcesses[name]){ + //depending on module this may only work for Ubuntu + const modulePath = getModulePath(name) + const properties = getModuleProperties(name); + const installerPath = modulePath + `INSTALL.sh` + const propertiesPath = modulePath + 'package.json' + var installProcess + // check for INSTALL.sh (ubuntu only) + if(fs.existsSync(installerPath)){ + installProcess = spawn(`sh`,[installerPath]) + }else if(fs.existsSync(propertiesPath)){ + // no INSTALL.sh found, check for package.json and do `npm install --unsafe-perm` + installProcess = spawn(`npm`,['install','--unsafe-perm','--prefix',modulePath]) + }else{ + resolve() + } + if(installProcess){ + const sendData = (data,channel) => { + const clientData = { + f: 'module-info', + module: name, + process: 'install-' + channel, + data: data.toString(), + } + s.tx(clientData,'$') + s.debugLog(clientData) } + installProcess.stderr.on('data',(data) => { + sendData(data,'stderr') + }) + installProcess.stdout.on('data',(data) => { + sendData(data,'stdout') + }) + installProcess.on('exit',(data) => { + runningInstallProcesses[name] = null; + resolve() + }) + runningInstallProcesses[name] = installProcess + } + }else{ + resolve(lang['Already Installing...']) + } + }) + } + const disableModule = (name,status) => { + // set status to `false` to enable + const modulePath = getModulePath(name) + const properties = getModuleProperties(name); + const propertiesPath = modulePath + 'package.json' + var packageJson = { + name: name + } + try{ + packageJson = JSON.parse(fs.readFileSync(propertiesPath)) + }catch(err){ + + } + packageJson.disabled = status; + fs.writeFileSync(propertiesPath,s.prettyPrint(packageJson)) + } + const deleteModule = (name) => { + // requires restart for changes to take effect + try{ + const modulePath = modulesBasePath + name + fs.remove(modulePath, (err) => { + console.log(err) + }) + return true + }catch(err){ + console.log(err) + return false + } + } + const loadModule = (shinobiModule) => { + const moduleName = shinobiModule.name + s.customAutoLoadModules[moduleName] = {} + var customModulePath = modulesBasePath + '/' + moduleName + if(shinobiModule.isIgnitor){ + s.customAutoLoadModules[moduleName].type = 'file' + try{ + require(customModulePath)(s,config,lang,app,io) + }catch(err){ + s.systemLog('Failed to Load Module : ' + moduleName) + s.systemLog(err) + } + }else if(shinobiModule.isDirectory){ + s.customAutoLoadModules[moduleName].type = 'folder' + try{ + require(customModulePath)(s,config,lang,app,io) + fs.readdir(customModulePath,function(err,folderContents){ + folderContents.forEach(function(name){ + switch(name){ + case'web': + var webFolder = s.checkCorrectPathEnding(customModulePath) + 'web/' + fs.readdir(webFolder,function(err,webFolderContents){ + webFolderContents.forEach(function(name){ + switch(name){ + case'libs': + case'pages': + if(name === 'libs'){ + if(config.webPaths.home !== '/'){ + app.use('/libs',express.static(webFolder + '/libs')) + } + app.use(s.checkCorrectPathEnding(config.webPaths.home)+'libs',express.static(webFolder + '/libs')) + app.use(s.checkCorrectPathEnding(config.webPaths.admin)+'libs',express.static(webFolder + '/libs')) + app.use(s.checkCorrectPathEnding(config.webPaths.super)+'libs',express.static(webFolder + '/libs')) + } + var libFolder = webFolder + name + '/' + fs.readdir(libFolder,function(err,webFolderContents){ + webFolderContents.forEach(function(libName){ + var thirdLevelName = libFolder + libName + switch(libName){ + case'js': + case'css': + case'blocks': + fs.readdir(thirdLevelName,function(err,webFolderContents){ + webFolderContents.forEach(function(filename){ + var fullPath = thirdLevelName + '/' + filename + var blockPrefix = '' + switch(true){ + case searchText('super.',filename): + blockPrefix = 'super' + break; + case searchText('admin.',filename): + blockPrefix = 'admin' + break; + } + switch(libName){ + case'js': + s.customAutoLoadTree[blockPrefix + 'LibsJs'].push(filename) + break; + case'css': + s.customAutoLoadTree[blockPrefix + 'LibsCss'].push(filename) + break; + case'blocks': + s.customAutoLoadTree[blockPrefix + 'PageBlocks'].push(fullPath) + break; + } + }) + }) + break; + default: + if(libName.indexOf('.ejs') > -1){ + s.customAutoLoadTree.pages.push(thirdLevelName) + } + break; + } + }) + }) + break; + } + }) + }) + break; + case'languages': + var languagesFolder = s.checkCorrectPathEnding(customModulePath) + 'languages/' + fs.readdir(languagesFolder,function(err,files){ + if(err)return console.log(err); + files.forEach(function(filename){ + var fileData = require(languagesFolder + filename) + var rule = filename.replace('.json','') + if(config.language === rule){ + lang = Object.assign(lang,fileData) + } + if(s.loadedLanguages[rule]){ + s.loadedLanguages[rule] = Object.assign(s.loadedLanguages[rule],fileData) + }else{ + s.loadedLanguages[rule] = Object.assign(s.copySystemDefaultLanguage(),fileData) + } + }) + }) + break; + case'definitions': + var definitionsFolder = s.checkCorrectPathEnding(customModulePath) + 'definitions/' + fs.readdir(definitionsFolder,function(err,files){ + if(err)return console.log(err); + files.forEach(function(filename){ + var fileData = require(definitionsFolder + filename) + var rule = filename.replace('.json','').replace('.js','') + if(config.language === rule){ + s.definitions = s.mergeDeep(s.definitions,fileData) + } + if(s.loadedDefinitons[rule]){ + s.loadedDefinitons[rule] = s.mergeDeep(s.loadedDefinitons[rule],fileData) + }else{ + s.loadedDefinitons[rule] = s.mergeDeep(s.copySystemDefaultDefinitions(),fileData) + } + }) + }) + break; + } + }) + }) + }catch(err){ + s.systemLog('Failed to Load Module : ' + moduleName) + s.systemLog(err) + } + } + } + const moveModuleToNameInProperties = (modulePath,packageRoot,properties) => { + return new Promise((resolve,reject) => { + const packageRootParts = packageRoot.split('/') + const filename = packageRootParts[packageRootParts.length - 1] + fs.move(modulePath + packageRoot,modulesBasePath + filename,(err) => { + if(packageRoot){ + fs.remove(modulePath, (err) => { + if(err)console.log(err) + resolve(filename) + }) + }else{ + resolve(filename) } }) - }else{ - fs.mkdirSync(folderPath) + }) + } + const initializeAllModules = async () => { + s.customAutoLoadModules = {} + s.customAutoLoadTree = { + pages: [], + PageBlocks: [], + LibsJs: [], + LibsCss: [], + adminPageBlocks: [], + adminLibsJs: [], + adminLibsCss: [], + superPageBlocks: [], + superLibsJs: [], + superLibsCss: [] } + fs.readdir(modulesBasePath,function(err,folderContents){ + if(!err && folderContents.length > 0){ + getModules(true).forEach((shinobiModule) => { + if(shinobiModule.properties.disabled){ + return; + } + loadModule(shinobiModule) + }) + }else{ + fs.mkdir(modulesBasePath,() => {}) + } + }) + } + /** + * API : Superuser : Custom Auto Load Package Download. + */ + app.get(config.webPaths.superApiPrefix+':auth/package/list', async (req,res) => { + s.superAuth(req.params, async (resp) => { + s.closeJsonResponse(res,{ + ok: true, + modules: getModules() + }) + },res,req) }) + /** + * API : Superuser : Custom Auto Load Package Download. + */ + app.post(config.webPaths.superApiPrefix+':auth/package/download', async (req,res) => { + s.superAuth(req.params, async (resp) => { + try{ + const url = req.body.downloadUrl + const packageRoot = req.body.packageRoot || '' + const packageName = req.body.packageName || extractNameFromPackage(url) + const modulePath = getModulePath(packageName) + await downloadModule(url,packageName) + const properties = getModuleProperties(packageName) + const newName = await moveModuleToNameInProperties(modulePath,packageRoot,properties) + const chosenName = newName ? newName : packageName + disableModule(chosenName,true) + s.closeJsonResponse(res,{ + ok: true, + moduleName: chosenName, + newModule: getModule(chosenName) + }) + }catch(err){ + s.closeJsonResponse(res,{ + ok: false, + error: err + }) + } + },res,req) + }) + // /** + // * API : Superuser : Custom Auto Load Package Update. + // */ + // app.post(config.webPaths.superApiPrefix+':auth/package/update', async (req,res) => { + // s.superAuth(req.params, async (resp) => { + // try{ + // const url = req.body.downloadUrl + // const packageRoot = req.body.packageRoot || '' + // const packageName = req.body.packageName || extractNameFromPackage(url) + // const modulePath = getModulePath(packageName) + // await downloadModule(url,packageName) + // const properties = getModuleProperties(packageName) + // const newName = await moveModuleToNameInProperties(modulePath,packageRoot,properties) + // const chosenName = newName ? newName : packageName + // + // disableModule(chosenName,true) + // s.closeJsonResponse(res,{ + // ok: true, + // moduleName: chosenName, + // newModule: getModule(chosenName) + // }) + // }catch(err){ + // s.closeJsonResponse(res,{ + // ok: false, + // error: err + // }) + // } + // },res,req) + // }) + /** + * API : Superuser : Custom Auto Load Package Install. + */ + app.post(config.webPaths.superApiPrefix+':auth/package/install', (req,res) => { + s.superAuth(req.params, async (resp) => { + const packageName = req.body.packageName + const response = {ok: true} + const error = await installModule(packageName) + if(error){ + response.ok = false + response.msg = error + } + s.closeJsonResponse(res,response) + },res,req) + }) + /** + * API : Superuser : Custom Auto Load Package set Status (Enabled or Disabled). + */ + app.post(config.webPaths.superApiPrefix+':auth/package/status', (req,res) => { + s.superAuth(req.params, async (resp) => { + const status = req.body.status + const packageName = req.body.packageName + const selection = status == 'true' ? true : false + disableModule(packageName,selection) + s.closeJsonResponse(res,{ok: true, status: selection}) + },res,req) + }) + /** + * API : Superuser : Custom Auto Load Package Delete + */ + app.post(config.webPaths.superApiPrefix+':auth/package/delete', async (req,res) => { + s.superAuth(req.params, async (resp) => { + const packageName = req.body.packageName + const response = deleteModule(packageName) + s.closeJsonResponse(res,{ok: response}) + },res,req) + }) + /** + * API : Superuser : Custom Auto Load Package Reload All + */ + app.post(config.webPaths.superApiPrefix+':auth/package/reloadAll', async (req,res) => { + s.superAuth(req.params, async (resp) => { + await initializeAllModules(); + s.closeJsonResponse(res,{ok: true}) + },res,req) + }) + // Initialize Modules on Start + await initializeAllModules(); } diff --git a/libs/detectorPamDiff.js b/libs/detectorPamDiff.js deleted file mode 100644 index 690adcc5..00000000 --- a/libs/detectorPamDiff.js +++ /dev/null @@ -1,692 +0,0 @@ -// -// Shinobi - fork of pam-diff -// Copyright (C) 2018 Kevin Godell -// Author : Kevin Godell, https://github.com/kevinGodell -// npmjs : https://www.npmjs.com/package/pam-diff -// Github : https://github.com/kevinGodell/pam-diff -// -'use strict'; - -const { Transform } = require('stream'); - -const PP = require('polygon-points'); -/** - * - * @param chunk - * @private - */ - var _getMatrixFromPoints = function(thisRegion) { - var coordinates = [ - thisRegion.topLeft, - {"x" : thisRegion.bottomRight.x, "y" : thisRegion.topLeft.y}, - thisRegion.bottomRight - ] - var width = Math.sqrt( Math.pow(coordinates[1].x - coordinates[0].x, 2) + Math.pow(coordinates[1].y - coordinates[0].y, 2)); - var height = Math.sqrt( Math.pow(coordinates[2].x - coordinates[1].x, 2) + Math.pow(coordinates[2].y - coordinates[1].y, 2)) - return { - x: coordinates[0].x, - y: coordinates[0].y, - width: width, - height: height, - tag: thisRegion.name - } - } -class PamDiff extends Transform { - - /** - * - * @param [options] {Object} - * @param [callback] {Function} - */ - constructor(options, callback) { - super(options); - Transform.call(this, {objectMode: true}); - this.difference = PamDiff._parseOptions('difference', options);//global option, can be overridden per region - this.percent = PamDiff._parseOptions('percent', options);//global option, can be overridden per region - this.regions = PamDiff._parseOptions('regions', options);//can be no regions or a single region or multiple regions. if no regions, all pixels will be compared. - this.drawMatrix = PamDiff._parseOptions('drawMatrix', options);//can be no regions or a single region or multiple regions. if no regions, all pixels will be compared. - this.callback = callback;//callback function to be called when pixel difference is detected - this._parseChunk = this._parseFirstChunk;//first parsing will be reading settings and configuring internal pixel reading - } - - /** - * - * @param option {String} - * @param options {Object} - * @return {*} - * @private - */ - static _parseOptions(option, options) { - if (options && options.hasOwnProperty(option)) { - return options[option]; - } - return null; - } - - /** - * - * @param number {Number} - * @param def {Number} - * @param low {Number} - * @param high {Number} - * @return {Number} - * @private - */ - static _validateNumber(number, def, low, high) { - if (isNaN(number)) { - return def; - } else if (number < low) { - return low; - } else if (number > high) { - return high; - } else { - return number; - } - } - - /** - * - * @deprecated - * @param string {String} - */ - setGrayscale(string) { - console.warn('grayscale option has been removed, "average" has proven to most accurate and is the default'); - } - - /** - * - * @param number {Number} - */ - set difference(number) { - this._difference = PamDiff._validateNumber(parseInt(number), 5, 1, 255); - } - - /** - * - * @return {Number} - */ - get difference() { - return this._difference; - } - - /** - * - * @param number {Number} - * @return {PamDiff} - */ - setDifference(number) { - this.difference = number; - return this; - } - - /** - * - * @param number {Number} - */ - set percent(number) { - this._percent = PamDiff._validateNumber(parseInt(number), 5, 1, 100); - } - - /** - * - * @return {Number} - */ - get percent() { - return this._percent; - } - - /** - * - * @param number {Number} - * @return {PamDiff} - */ - setPercent(number) { - this.percent = number; - return this; - } - - /** - * - * @param array {Array} - */ - set regions(array) { - if (!array) { - if (this._regions) { - delete this._regions; - delete this._regionsLength; - delete this._minDiff; - } - this._diffs = 0; - } else if (!Array.isArray(array) || array.length < 1) { - throw new Error(`Regions must be an array of at least 1 region object {name: 'region1', difference: 10, percent: 10, polygon: [[0, 0], [0, 50], [50, 50], [50, 0]]}`); - } else { - this._regions = []; - this._minDiff = 255; - for (const region of array) { - if (!region.hasOwnProperty('name') || !region.hasOwnProperty('polygon')) { - throw new Error('Region must include a name and a polygon property'); - } - const polygonPoints = new PP(region.polygon); - const difference = PamDiff._validateNumber(parseInt(region.difference), this._difference, 1, 255); - const percent = PamDiff._validateNumber(parseInt(region.percent), this._percent, 1, 100); - this._minDiff = Math.min(this._minDiff, difference); - this._regions.push( - { - name: region.name, - polygon: polygonPoints, - difference: difference, - percent: percent, - diffs: 0 - } - ); - } - this._regionsLength = this._regions.length; - this._createPointsInPolygons(this._regions, this._width, this._height); - } - } - - /** - * - * @return {Array} - */ - get regions() { - return this._regions; - } - - /** - * - * @param array {Array} - * @return {PamDiff} - */ - setRegions(array) { - this.regions = array; - return this; - } - - /** - * - * @param func {Function} - */ - set callback(func) { - if (!func) { - delete this._callback; - } else if (typeof func === 'function' && func.length === 1) { - this._callback = func; - } else { - throw new Error('Callback must be a function that accepts 1 argument.'); - } - } - - /** - * - * @return {Function} - */ - get callback() { - return this._callback; - } - - /** - * - * @param func {Function} - * @return {PamDiff} - */ - setCallback(func) { - this.callback = func; - return this; - } - - /** - * - * @return {PamDiff} - */ - resetCache() { - //delete this._oldPix; - //delete this._newPix; - //delete this._width; - //delete this._length; - this._parseChunk = this._parseFirstChunk; - return this; - } - - /** - * - * @param regions {Array} - * @param width {Number} - * @param height {Number} - * @private - */ - _createPointsInPolygons(regions, width, height) { - if (regions && width && height) { - this._pointsInPolygons = []; - for (const region of regions) { - const bitset = region.polygon.getBitset(this._width, this._height); - region.pointsLength = bitset.count; - this._pointsInPolygons.push(bitset.buffer); - } - } - } - - /** - * - * @param chunk - * @private - */ - _blackAndWhitePixelDiff(chunk) { - this._newPix = chunk.pixels; - for (let y = 0, i = 0; y < this._height; y++) { - for (let x = 0; x < this._width; x++, i++) { - const diff = this._oldPix[i] !== this._newPix[i]; - if (this._regions && diff === true) { - for (let j = 0; j < this._regionsLength; j++) { - if (this._pointsInPolygons[j][i]) { - this._regions[j].diffs++; - } - } - } else if (diff === true) { - this._diffs++; - } - } - } - if (this._regions) { - const regionDiffArray = []; - for (let i = 0; i < this._regionsLength; i++) { - const percent = Math.floor(100 * this._regions[i].diffs / this._regions[i].pointsLength); - if (percent >= this._regions[i].percent) { - regionDiffArray.push({name: this._regions[i].name, percent: percent}); - } - this._regions[i].diffs = 0; - } - if (regionDiffArray.length > 0) { - const data = {trigger: regionDiffArray, pam: chunk.pam}; - if (this._callback) { - this._callback(data); - } - if (this._readableState.pipesCount > 0) { - this.push(data); - } - if (this.listenerCount('diff') > 0) { - this.emit('diff', data); - } - } - } else { - const percent = Math.floor(100 * this._diffs / this._length); - if (percent >= this._percent) { - const data = {trigger: [{name: 'percent', percent: percent}], pam: chunk.pam}; - if (this._callback) { - this._callback(data); - } - if (this._readableState.pipesCount > 0) { - this.push(data); - } - if (this.listenerCount('diff') > 0) { - this.emit('diff', data); - } - } - this._diffs = 0; - } - this._oldPix = this._newPix; - } - - /** - * - * @param chunk - * @private - */ - _grayScalePixelDiffWithMatrices(chunk) { - this._newPix = chunk.pixels; - for (let j = 0; j < this._regionsLength; j++) { - this._regions[j].topLeft = { - x: this._width, - y: this._height - } - this._regions[j].bottomRight = { - x: 0, - y: 0 - } - } - this.topLeft = { - x: this._width, - y: this._height - } - this.bottomRight = { - x: 0, - y: 0 - } - for (let y = 0, i = 0; y < this._height; y++) { - for (let x = 0; x < this._width; x++, i++) { - if (this._oldPix[i] !== this._newPix[i]) { - const diff = Math.abs(this._oldPix[i] - this._newPix[i]); - if (this._regions && diff >= this._minDiff) { - for (let j = 0; j < this._regionsLength; j++) { - if (this._pointsInPolygons[j][i] && diff >= this._regions[j].difference) { - var theRegion = this._regions[j] - theRegion.diffs++; - if(theRegion.topLeft.x > x && theRegion.topLeft.y > y){ - theRegion.topLeft.x = x - theRegion.topLeft.y = y - } - if(theRegion.bottomRight.x < x && theRegion.bottomRight.y < y){ - theRegion.bottomRight.x = x - theRegion.bottomRight.y = y - } - } - } - } else if (diff >= this._difference) { - this._diffs++; - if(this.topLeft.x > x && this.topLeft.y > y){ - this.topLeft.x = x - this.topLeft.y = y - } - if(this.bottomRight.x < x && this.bottomRight.y < y){ - this.bottomRight.x = x - this.bottomRight.y = y - } - } - } - } - } - if (this._regions) { - const regionDiffArray = []; - for (let i = 0; i < this._regionsLength; i++) { - var thisRegion = this._regions[i] - const percent = Math.floor(100 * thisRegion.diffs / thisRegion.pointsLength); - if (percent >= thisRegion.percent) { - // create matrix from points >> - thisRegion._matrix = _getMatrixFromPoints(thisRegion) - // create matrix from points />> - regionDiffArray.push({name: thisRegion.name, percent: percent, matrix: thisRegion._matrix}); - } - thisRegion.diffs = 0; - } - if (regionDiffArray.length > 0) { - this._matrix = _getMatrixFromPoints(this) - const data = {trigger: regionDiffArray, pam: chunk.pam, matrix: this._matrix}; - if (this._callback) { - this._callback(data); - } - if (this._readableState.pipesCount > 0) { - this.push(data); - } - if (this.listenerCount('diff') > 0) { - this.emit('diff', data); - } - } - } else { - const percent = Math.floor(100 * this._diffs / this._length); - if (percent >= this._percent) { - this._matrix = _getMatrixFromPoints(this) - const data = {trigger: [{name: 'percent', percent: percent, matrix: this._matrix}], pam: chunk.pam}; - if (this._callback) { - this._callback(data); - } - if (this._readableState.pipesCount > 0) { - this.push(data); - } - if (this.listenerCount('diff') > 0) { - this.emit('diff', data); - } - } - this._diffs = 0; - } - this._oldPix = this._newPix; - } - - /** - * - * @param chunk - * @private - */ - _grayScalePixelDiff(chunk) { - this._newPix = chunk.pixels; - for (let y = 0, i = 0; y < this._height; y++) { - for (let x = 0; x < this._width; x++, i++) { - if (this._oldPix[i] !== this._newPix[i]) { - const diff = Math.abs(this._oldPix[i] - this._newPix[i]); - if (this._regions && diff >= this._minDiff) { - for (let j = 0; j < this._regionsLength; j++) { - if (this._pointsInPolygons[j][i] && diff >= this._regions[j].difference) { - this._regions[j].diffs++; - } - } - } else { - if (diff >= this._difference) { - this._diffs++; - } - } - } - } - } - if (this._regions) { - const regionDiffArray = []; - for (let i = 0; i < this._regionsLength; i++) { - const percent = Math.floor(100 * this._regions[i].diffs / this._regions[i].pointsLength); - if (percent >= this._regions[i].percent) { - regionDiffArray.push({name: this._regions[i].name, percent: percent}); - } - this._regions[i].diffs = 0; - } - if (regionDiffArray.length > 0) { - const data = {trigger: regionDiffArray, pam: chunk.pam}; - if (this._callback) { - this._callback(data); - } - if (this._readableState.pipesCount > 0) { - this.push(data); - } - if (this.listenerCount('diff') > 0) { - this.emit('diff', data); - } - } - } else { - const percent = Math.floor(100 * this._diffs / this._length); - if (percent >= this._percent) { - const data = {trigger: [{name: 'percent', percent: percent}], pam: chunk.pam}; - if (this._callback) { - this._callback(data); - } - if (this._readableState.pipesCount > 0) { - this.push(data); - } - if (this.listenerCount('diff') > 0) { - this.emit('diff', data); - } - } - this._diffs = 0; - } - this._oldPix = this._newPix; - } - - /** - * - * @param chunk - * @private - */ - _rgbPixelDiff(chunk) { - this._newPix = chunk.pixels; - for (let y = 0, i = 0, p = 0; y < this._height; y++) { - for (let x = 0; x < this._width; x++, i += 3, p++) { - if (this._oldPix[i] !== this._newPix[i] || this._oldPix[i + 1] !== this._newPix[i + 1] || this._oldPix[i + 2] !== this._newPix[i + 2]) { - const diff = Math.abs(this._oldPix[i] + this._oldPix[i + 1] + this._oldPix[i + 2] - this._newPix[i] - this._newPix[i + 1] - this._newPix[i + 2])/3; - if (this._regions && diff >= this._minDiff) { - for (let j = 0; j < this._regionsLength; j++) { - if (this._pointsInPolygons[j][p] && diff >= this._regions[j].difference) { - this._regions[j].diffs++; - } - } - } else { - if (diff >= this._difference) { - this._diffs++; - } - } - } - } - } - if (this._regions) { - const regionDiffArray = []; - for (let i = 0; i < this._regionsLength; i++) { - const percent = Math.floor(100 * this._regions[i].diffs / this._regions[i].pointsLength); - if (percent >= this._regions[i].percent) { - regionDiffArray.push({name: this._regions[i].name, percent: percent}); - } - this._regions[i].diffs = 0; - } - if (regionDiffArray.length > 0) { - const data = {trigger: regionDiffArray, pam: chunk.pam}; - if (this._callback) { - this._callback(data); - } - if (this._readableState.pipesCount > 0) { - this.push(data); - } - if (this.listenerCount('diff') > 0) { - this.emit('diff', data); - } - } - } else { - const percent = Math.floor(100 * this._diffs / this._length); - if (percent >= this._percent) { - const data = {trigger: [{name: 'percent', percent: percent}], pam: chunk.pam}; - if (this._callback) { - this._callback(data); - } - if (this._readableState.pipesCount > 0) { - this.push(data); - } - if (this.listenerCount('diff') > 0) { - this.emit('diff', data); - } - } - this._diffs = 0; - } - this._oldPix = this._newPix; - } - - /** - * - * @param chunk - * @private - */ - _rgbAlphaPixelDiff(chunk) { - this._newPix = chunk.pixels; - for (let y = 0, i = 0, p = 0; y < this._height; y++) { - for (let x = 0; x < this._width; x++, i += 4, p++) { - if (this._oldPix[i] !== this._newPix[i] || this._oldPix[i + 1] !== this._newPix[i + 1] || this._oldPix[i + 2] !== this._newPix[i + 2]) { - const diff = Math.abs(this._oldPix[i] + this._oldPix[i + 1] + this._oldPix[i + 2] - this._newPix[i] - this._newPix[i + 1] - this._newPix[i + 2])/3; - if (this._regions && diff >= this._minDiff) { - for (let j = 0; j < this._regionsLength; j++) { - if (this._pointsInPolygons[j][p] && diff >= this._regions[j].difference) { - this._regions[j].diffs++; - } - } - } else { - if (diff >= this._difference) { - this._diffs++; - } - } - } - } - } - if (this._regions) { - const regionDiffArray = []; - for (let i = 0; i < this._regionsLength; i++) { - const percent = Math.floor(100 * this._regions[i].diffs / this._regions[i].pointsLength); - if (percent >= this._regions[i].percent) { - regionDiffArray.push({name: this._regions[i].name, percent: percent}); - } - this._regions[i].diffs = 0; - } - if (regionDiffArray.length > 0) { - const data = {trigger: regionDiffArray, pam: chunk.pam}; - if (this._callback) { - this._callback(data); - } - if (this._readableState.pipesCount > 0) { - this.push(data); - } - if (this.listenerCount('diff') > 0) { - this.emit('diff', data); - } - } - } else { - const percent = Math.floor(100 * this._diffs / this._length); - if (percent >= this._percent) { - const data = {trigger: [{name: 'percent', percent: percent}], pam: chunk.pam}; - if (this._callback) { - this._callback(data); - } - if (this._readableState.pipesCount > 0) { - this.push(data); - } - if (this.listenerCount('diff') > 0) { - this.emit('diff', data); - } - } - this._diffs = 0; - } - this._oldPix = this._newPix; - } - - /** - * - * @param chunk - * @private - */ - _parseFirstChunk(chunk) { - this._width = parseInt(chunk.width); - this._height = parseInt(chunk.height); - this._oldPix = chunk.pixels; - this._length = this._width * this._height; - this._createPointsInPolygons(this._regions, this._width, this._height); - switch (chunk.tupltype) { - case 'blackandwhite' : - this._parseChunk = this._blackAndWhitePixelDiff; - break; - case 'grayscale' : - if(this.drawMatrix === "1"){ - this._parseChunk = this._grayScalePixelDiffWithMatrices; - }else{ - this._parseChunk = this._grayScalePixelDiff; - } - break; - case 'rgb' : - this._parseChunk = this._rgbPixelDiff; - //this._increment = 3;//future use - break; - case 'rgb_alpha' : - this._parseChunk = this._rgbAlphaPixelDiff; - //this._increment = 4;//future use - break; - default : - throw Error(`Unsupported tupltype: ${chunk.tupltype}. Supported tupltypes include grayscale(gray), blackandwhite(monob), rgb(rgb24), and rgb_alpha(rgba).`); - } - } - - /** - * - * @param chunk - * @param encoding - * @param callback - * @private - */ - _transform(chunk, encoding, callback) { - this._parseChunk(chunk); - callback(); - } - - /** - * - * @param callback - * @private - */ - _flush(callback) { - this.resetCache(); - callback(); - } -} - -/** - * - * @type {PamDiff} - */ -module.exports = PamDiff; -//todo get bounding box of all regions combined to exclude some pixels before checking if they exist inside specific regions diff --git a/libs/dropInEvents.js b/libs/dropInEvents.js index 26e69122..50d8b7af 100644 --- a/libs/dropInEvents.js +++ b/libs/dropInEvents.js @@ -1,9 +1,142 @@ var fs = require('fs') +var exec = require('child_process').exec module.exports = function(s,config,lang,app,io){ if(config.dropInEventServer === true){ if(config.dropInEventForceSaveEvent === undefined)config.dropInEventForceSaveEvent = true if(config.dropInEventDeleteFileAfterTrigger === undefined)config.dropInEventDeleteFileAfterTrigger = true - var beforeMonitorsLoadedOnStartup = function(){ + var fileQueueForDeletion = {} + var fileQueue = {} + var search = function(searchIn,searchFor){ + return searchIn.indexOf(searchFor) > -1 + } + var getFileNameFromPath = function(filePath){ + fileParts = filePath.split('/') + return fileParts[fileParts.length - 1] + } + var clipPathEnding = function(filePath){ + var newPath = filePath + '' + if (newPath.substring(newPath.length-1) == "/"){ + newPath = newPath.substring(0, newPath.length-1); + } + return newPath; + } + var processFile = function(filePath,monitorConfig){ + var ke = monitorConfig.ke + var mid = monitorConfig.mid + var filename = getFileNameFromPath(filePath) + if(search(filename,'.jpg') || search(filename,'.jpeg')){ + var snapPath = s.dir.streams + ke + '/' + mid + '/s.jpg' + fs.unlink(snapPath,function(err){ + fs.createReadStream(filePath).pipe(fs.createWriteStream(snapPath)) + s.triggerEvent({ + id: mid, + ke: ke, + details: { + confidence: 100, + name: filename, + plug: "dropInEvent", + reason: "ftpServer" + }, + },config.dropInEventForceSaveEvent) + }) + }else{ + var reason = "ftpServer" + if(search(filename,'.mp4')){ + fs.stat(filePath,function(err,stats){ + if(err)return; + var startTime = stats.ctime + var endTime = stats.mtime + var shinobiFilename = s.formattedTime(startTime) + '.mp4' + var recordingPath = s.getVideoDirectory(monitorConfig) + shinobiFilename + var writeStream = fs.createWriteStream(recordingPath) + fs.createReadStream(filePath).pipe(writeStream) + writeStream.on('finish', () => { + s.insertCompletedVideo(s.group[monitorConfig.ke].rawMonitorConfigurations[monitorConfig.mid],{ + file: shinobiFilename, + events: [ + { + id: mid, + ke: ke, + time: new Date(), + details: { + confidence: 100, + name: filename, + plug: "dropInEvent", + reason: "ftpServer" + } + } + ], + },function(){ + }) + }) + }) + } + var completeAction = function(){ + s.triggerEvent({ + id: mid, + ke: ke, + details: { + confidence: 100, + name: filename, + plug: "dropInEvent", + reason: reason + }, + doObjectDetection: (s.isAtleatOneDetectorPluginConnected && s.group[ke].rawMonitorConfigurations[mid].details.detector_use_detect_object === '1') + },config.dropInEventForceSaveEvent) + } + if(search(filename,'.txt')){ + fs.readFile(filePath,{encoding: 'utf-8'},function(err,data){ + if(data){ + reason = data.split('\n')[0] || filename + }else if(filename){ + reason = filename + } + completeAction() + }) + }else{ + completeAction() + } + + } + } + var onFileOrFolderFound = function(filePath,deletionKey,monitorConfig){ + fs.stat(filePath,function(err,stats){ + if(!err){ + if(stats.isDirectory()){ + fs.readdir(filePath,function(err,files){ + if(files){ + files.forEach(function(filename){ + onFileOrFolderFound(clipPathEnding(filePath) + '/' + filename,deletionKey,monitorConfig) + }) + }else if(err){ + console.log(err) + } + }) + }else{ + if(!fileQueue[filePath]){ + processFile(filePath,monitorConfig) + if(config.dropInEventDeleteFileAfterTrigger){ + clearTimeout(fileQueue[filePath]) + fileQueue[filePath] = setTimeout(function(){ + exec('rm -rf ' + filePath,function(err){ + delete(fileQueue[filePath]) + }) + },1000 * 60 * 5) + } + } + } + if(config.dropInEventDeleteFileAfterTrigger){ + clearTimeout(fileQueueForDeletion[deletionKey]) + fileQueueForDeletion[deletionKey] = setTimeout(function(){ + exec('rm -rf ' + deletionKey,function(err){ + delete(fileQueueForDeletion[deletionKey]) + }) + },1000 * 60 * 5) + } + } + }) + } + var createDropInEventsDirectory = function(){ if(!config.dropInEventsDir){ config.dropInEventsDir = s.dir.streams + 'dropInEvents/' } @@ -37,6 +170,7 @@ module.exports = function(s,config,lang,app,io){ directory = s.dir.dropInEvents + e.ke + '/' + (e.id || e.mid) + '/' fs.mkdir(directory,function(err){ s.handleFolderError(err) + exec('rm -rf "' + directory + '*"',function(){}) callback(err,directory) }) }) @@ -45,125 +179,36 @@ module.exports = function(s,config,lang,app,io){ var ke = monitorConfig.ke var mid = monitorConfig.mid var groupEventDropDir = s.dir.dropInEvents + ke - createDropInEventDirectory(monitorConfig,function(err,monitorEventDropDir){ - var monitorEventDropDir = getDropInEventDir(monitorConfig) - var fileQueue = {} - s.group[monitorConfig.ke].activeMonitors[monitorConfig.mid].dropInEventFileQueue = fileQueue - var search = function(searchIn,searchFor){ - return searchIn.indexOf(searchFor) > -1 - } - var processFile = function(filename){ - var filePath = monitorEventDropDir + filename - if(search(filename,'.jpg') || search(filename,'.jpeg')){ - var snapPath = s.dir.streams + ke + '/' + mid + '/s.jpg' - fs.unlink(snapPath,function(err){ - fs.createReadStream(filePath).pipe(fs.createWriteStream(snapPath)) - s.triggerEvent({ - id: mid, - ke: ke, - details: { - confidence: 100, - name: filename, - plug: "dropInEvent", - reason: "ftpServer" - }, - },config.dropInEventForceSaveEvent) - }) - }else{ - var reason = "ftpServer" - if(search(filename,'.mp4')){ - fs.stat(filePath,function(err,stats){ - var startTime = stats.ctime - var endTime = stats.mtime - var shinobiFilename = s.formattedTime(startTime) + '.mp4' - var recordingPath = s.getVideoDirectory(monitorConfig) + shinobiFilename - var writeStream = fs.createWriteStream(recordingPath) - fs.createReadStream(filePath).pipe(writeStream) - writeStream.on('finish', () => { - s.insertCompletedVideo(s.group[monitorConfig.ke].rawMonitorConfigurations[monitorConfig.mid],{ - file : shinobiFilename - },function(){ - }) - }) - }) - } - var completeAction = function(){ - s.triggerEvent({ - id: mid, - ke: ke, - details: { - confidence: 100, - name: filename, - plug: "dropInEvent", - reason: reason - } - },config.dropInEventForceSaveEvent) - } - if(search(filename,'.txt')){ - fs.readFile(filePath,{encoding: 'utf-8'},function(err,data){ - if(data){ - reason = data.split('\n')[0] || filename - }else if(filename){ - reason = filename - } - completeAction() - }) - }else{ - completeAction() - } - - } - if(config.dropInEventDeleteFileAfterTrigger){ - setTimeout(function(){ - fs.unlink(filePath,function(err){ - - }) - },1000 * 60 * 5) - } - } - var eventTrigger = function(eventType,filename,stats){ - if(stats.isDirectory()){ - fs.readdir(monitorEventDropDir + filename,function(err,files){ - if(files){ - files.forEach(function(filename){ - processFile(filename) - }) - }else if(err){ - console.log(err) - } - }) - }else{ - processFile(filename) - } - } - var directoryWatch = fs.watch(monitorEventDropDir,function(eventType,filename){ - fs.stat(monitorEventDropDir + filename,function(err,stats){ - if(!err){ - clearTimeout(fileQueue[filename]) - fileQueue[filename] = setTimeout(function(){ - eventTrigger(eventType,filename,stats) - },1750) - } - }) - }) - s.group[monitorConfig.ke].activeMonitors[monitorConfig.mid].dropInEventWatcher = directoryWatch - }) + createDropInEventDirectory(monitorConfig,function(err,monitorEventDropDir){}) } // FTP Server if(config.ftpServer === true){ + createDropInEventsDirectory() if(!config.ftpServerPort)config.ftpServerPort = 21 if(!config.ftpServerUrl)config.ftpServerUrl = `ftp://0.0.0.0:${config.ftpServerPort}` config.ftpServerUrl = config.ftpServerUrl.replace('{{PORT}}',config.ftpServerPort) const FtpSrv = require('ftp-srv') const ftpServer = new FtpSrv({ url: config.ftpServerUrl, + log: require('bunyan').createLogger({ + name: 'ftp-srv', + level: 100 + }), }) - ftpServer.on('login', (data, resolve, reject) => { - var username = data.username - var password = data.password + ftpServer.on('login', ({connection, username, password}, resolve, reject) => { s.basicOrApiAuthentication(username,password,function(err,user){ if(user){ + connection.on('STOR', (error, fileName) => { + if(!fileName)return; + var pathPieces = fileName.replace(s.dir.dropInEvents,'').split('/') + var ke = pathPieces[0] + var mid = pathPieces[1] + var firstDroppedPart = pathPieces[2] + var monitorEventDropDir = s.dir.dropInEvents + ke + '/' + mid + '/' + var deleteKey = monitorEventDropDir + firstDroppedPart + onFileOrFolderFound(monitorEventDropDir + firstDroppedPart,deleteKey,{ke: ke, mid: mid}) + }) resolve({root: s.dir.dropInEvents + user.ke}) }else{ // reject(new Error('Failed Authorization')) @@ -171,7 +216,7 @@ module.exports = function(s,config,lang,app,io){ }) }) ftpServer.on('client-error', ({connection, context, error}) => { - console.log('error') + console.log('client-error',error) }) ftpServer.listen().then(() => { s.systemLog(`FTP Server running on port ${config.ftpServerPort}...`) @@ -180,7 +225,6 @@ module.exports = function(s,config,lang,app,io){ }) } //add extensions - s.beforeMonitorsLoadedOnStartup(beforeMonitorsLoadedOnStartup) s.onMonitorInit(onMonitorInit) s.onMonitorStop(onMonitorStop) } @@ -190,7 +234,9 @@ module.exports = function(s,config,lang,app,io){ if(config.smtpServerHideStartTls === undefined)config.smtpServerHideStartTls = null var SMTPServer = require("smtp-server").SMTPServer; if(!config.smtpServerPort && (config.smtpServerSsl && config.smtpServerSsl.enabled !== false || config.ssl)){config.smtpServerPort = 465}else if(!config.smtpServerPort){config.smtpServerPort = 25} - var smtpOptions = { + config.smtpServerOptions = config.smtpServerOptions ? config.smtpServerOptions : {} + var smtpOptions = Object.assign({ + logger: config.debugLog || config.smtpServerLog, hideSTARTTLS: config.smtpServerHideStartTls, onAuth(auth, session, callback) { var username = auth.username @@ -207,7 +253,7 @@ module.exports = function(s,config,lang,app,io){ var split = address.address.split('@') var monitorId = split[0] var ke = session.user - if(s.group[ke].rawMonitorConfigurations[monitorId] && s.group[ke].activeMonitors[monitorId].isStarted === true){ + if(s.group[ke] && s.group[ke].rawMonitorConfigurations[monitorId] && s.group[ke].activeMonitors[monitorId].isStarted === true){ session.monitorId = monitorId }else{ return callback(new Error(lang['No Monitor Exists with this ID.'])) @@ -218,6 +264,7 @@ module.exports = function(s,config,lang,app,io){ if(session.monitorId){ var ke = session.user var monitorId = session.monitorId + var details = s.group[ke].rawMonitorConfigurations[monitorId].details var reasonTag = 'smtpServer' var text = '' stream.on('data',function(data){ @@ -255,7 +302,8 @@ module.exports = function(s,config,lang,app,io){ name: 'smtpServer', plug: "dropInEvent", reason: reasonTag - } + }, + doObjectDetection: (s.isAtleatOneDetectorPluginConnected && details.detector_use_detect_object === '1') },config.dropInEventForceSaveEvent) callback() }) @@ -263,7 +311,7 @@ module.exports = function(s,config,lang,app,io){ callback() } } - } + },config.smtpServerOptions) if(config.smtpServerSsl && config.smtpServerSsl.enabled !== false || config.ssl && config.ssl.cert && config.ssl.key){ var key = config.ssl.key || fs.readFileSync(config.smtpServerSsl.key) var cert = config.ssl.cert || fs.readFileSync(config.smtpServerSsl.cert) diff --git a/libs/events.js b/libs/events.js index 36c80a25..3424e133 100644 --- a/libs/events.js +++ b/libs/events.js @@ -3,7 +3,83 @@ var execSync = require('child_process').execSync; var exec = require('child_process').exec; var spawn = require('child_process').spawn; var request = require('request'); +// Matrix In Region Libs > +var SAT = require('sat') +var V = SAT.Vector; +var P = SAT.Polygon; +var B = SAT.Box; +// Matrix In Region Libs /> module.exports = function(s,config,lang){ + const { + moveCameraPtzToMatrix, + } = require('./control/ptz.js')(s,config,lang) + const countObjects = async (event) => { + const matrices = event.details.matrices + const eventsCounted = s.group[event.ke].activeMonitors[event.id].eventsCounted || {} + if(matrices){ + matrices.forEach((matrix)=>{ + const id = matrix.tag + if(!eventsCounted[id])eventsCounted[id] = {times: [], count: {}, tag: matrix.tag} + if(!isNaN(matrix.id))eventsCounted[id].count[matrix.id] = 1 + eventsCounted[id].times.push(new Date().getTime()) + }) + } + return eventsCounted + } + const isAtleastOneMatrixInRegion = function(regions,matrices,callback){ + var regionPolys = [] + var matrixPoints = [] + regions.forEach(function(region,n){ + var polyPoints = [] + region.points.forEach(function(point){ + polyPoints.push(new V(parseInt(point[0]),parseInt(point[1]))) + }) + regionPolys[n] = new P(new V(0,0), polyPoints) + }) + var collisions = [] + var foundInRegion = false + matrices.forEach(function(matrix){ + var matrixPoly = new B(new V(matrix.x, matrix.y), matrix.width, matrix.height).toPolygon() + regionPolys.forEach(function(region,n){ + var response = new SAT.Response() + var collided = SAT.testPolygonPolygon(matrixPoly, region, response) + if(collided === true){ + collisions.push({ + matrix: matrix, + region: regions[n] + }) + foundInRegion = true + } + }) + }) + if(callback)callback(foundInRegion,collisions) + return foundInRegion + } + const scanMatricesforCollisions = function(region,matrices){ + var matrixPoints = [] + var collisions = [] + if (!region || !matrices){ + if(callback)callback(collisions) + return collisions + } + var polyPoints = [] + region.points.forEach(function(point){ + polyPoints.push(new V(parseInt(point[0]),parseInt(point[1]))) + }) + var regionPoly = new P(new V(0,0), polyPoints) + matrices.forEach(function(matrix){ + if (matrix){ + var matrixPoly = new B(new V(matrix.x, matrix.y), matrix.width, matrix.height).toPolygon() + var response = new SAT.Response() + var collided = SAT.testPolygonPolygon(matrixPoly, regionPoly, response) + if(collided === true){ + collisions.push(matrix) + } + } + }) + return collisions + } + const nonEmpty = (element) => element.length !== 0; s.addEventDetailsToString = function(eventData,string,addOps){ //d = event data if(!addOps)addOps = {} @@ -15,12 +91,19 @@ module.exports = function(s,config,lang){ .replace(/{{REGION_NAME}}/g,d.details.name) .replace(/{{SNAP_PATH}}/g,s.dir.streams+'/'+d.ke+'/'+d.id+'/s.jpg') .replace(/{{MONITOR_ID}}/g,d.id) + .replace(/{{MONITOR_NAME}}/g,s.group[d.ke].rawMonitorConfigurations[d.id].name) .replace(/{{GROUP_KEY}}/g,d.ke) .replace(/{{DETAILS}}/g,detailString) if(d.details.confidence){ newString = newString .replace(/{{CONFIDENCE}}/g,d.details.confidence) } + if(newString.includes("REASON")) { + if(d.details.reason) { + newString = newString + .replace(/{{REASON}}/g, d.details.reason) + } + } return newString } s.filterEvents = function(x,d){ @@ -41,7 +124,8 @@ module.exports = function(s,config,lang){ extender(x,d) }) } - s.triggerEvent = function(d,forceSave){ + s.triggerEvent = async (d,forceSave) => { + var didCountingAlready = false var filter = { halt : false, addToMotionCounter : true, @@ -50,17 +134,18 @@ module.exports = function(s,config,lang){ webhook : true, command : true, record : true, - indifference : false + indifference : false, + countObjects : true } - s.onEventTriggerBeforeFilterExtensions.forEach(function(extender){ - extender(d,filter) - }) var detailString = JSON.stringify(d.details); if(!s.group[d.ke]||!s.group[d.ke].activeMonitors[d.id]){ return s.systemLog(lang['No Monitor Found, Ignoring Request']) } d.mon=s.group[d.ke].rawMonitorConfigurations[d.id]; - var currentConfig = s.group[d.ke].activeMonitors[d.id].details + var currentConfig = s.group[d.ke].rawMonitorConfigurations[d.id].details + s.onEventTriggerBeforeFilterExtensions.forEach(function(extender){ + extender(d,filter) + }) var hasMatrices = (d.details.matrices && d.details.matrices.length > 0) //read filters if( @@ -126,6 +211,7 @@ module.exports = function(s,config,lang){ case'y': case'height': case'width': + case'confidence': if(d.details.matrices){ d.details.matrices.forEach(function(matrix,position){ modifyFilters(matrix,position) @@ -188,10 +274,14 @@ module.exports = function(s,config,lang){ var eventTime = new Date() //motion counter if(filter.addToMotionCounter && filter.record){ - if(!s.group[d.ke].activeMonitors[d.id].detector_motion_count){ - s.group[d.ke].activeMonitors[d.id].detector_motion_count=0 - } - s.group[d.ke].activeMonitors[d.id].detector_motion_count+=1 + s.group[d.ke].activeMonitors[d.id].detector_motion_count.push(d) + } + if(filter.countObjects && currentConfig.detector_obj_count === '1' && currentConfig.detector_obj_count_in_region !== '1'){ + didCountingAlready = true + countObjects(d) + } + if(currentConfig.detector_ptz_follow === '1'){ + moveCameraPtzToMatrix(d,currentConfig.detector_ptz_follow_target) } if(filter.useLock){ if(s.group[d.ke].activeMonitors[d.id].motion_lock){ @@ -214,9 +304,12 @@ module.exports = function(s,config,lang){ // check if object should be in region if(hasMatrices && currentConfig.detector_obj_region === '1'){ var regions = s.group[d.ke].activeMonitors[d.id].parsedObjects.cords - var isMatrixInRegions = s.isAtleastOneMatrixInRegion(regions,d.details.matrices) + var isMatrixInRegions = isAtleastOneMatrixInRegion(regions,d.details.matrices) if(isMatrixInRegions){ s.debugLog('Matrix in region!') + if(filter.countObjects && currentConfig.detector_obj_count === '1' && currentConfig.detector_obj_count_in_region === '1' && !didCountingAlready){ + countObjects(d) + } }else{ return } @@ -236,7 +329,9 @@ module.exports = function(s,config,lang){ time : s.formattedTime(), frame : s.group[d.ke].activeMonitors[d.id].lastJpegDetectorFrame }) - }else{ + } + // + if(currentConfig.detector_use_motion === '0' || d.doObjectDetection !== true ){ if(currentConfig.det_multi_trig === '1'){ s.getCamerasForMultiTrigger(d.mon).forEach(function(monitor){ if(monitor.mid !== d.id){ @@ -255,7 +350,16 @@ module.exports = function(s,config,lang){ } //save this detection result in SQL, only coords. not image. if(forceSave || (filter.save && currentConfig.detector_save === '1')){ - s.sqlQuery('INSERT INTO Events (ke,mid,details,time) VALUES (?,?,?,?)',[d.ke,d.id,detailString,eventTime]) + s.knexQuery({ + action: "insert", + table: "Events", + insert: { + ke: d.ke, + mid: d.id, + details: detailString, + time: eventTime, + } + }) } if(currentConfig.detector === '1' && currentConfig.detector_notrigger === '1'){ s.setNoEventsDetector(s.group[d.ke].rawMonitorConfigurations[d.id]) @@ -299,13 +403,9 @@ module.exports = function(s,config,lang){ } d.currentTime = new Date() d.currentTimestamp = s.timeObject(d.currentTime).format() - d.screenshotName = 'Motion_'+(d.mon.name.replace(/[^\w\s]/gi,''))+'_'+d.id+'_'+d.ke+'_'+s.formattedTime() + d.screenshotName = d.details.reason + '_'+(d.mon.name.replace(/[^\w\s]/gi,''))+'_'+d.id+'_'+d.ke+'_'+s.formattedTime() d.screenshotBuffer = null - s.onEventTriggerExtensions.forEach(function(extender){ - extender(d,filter) - }) - if(filter.webhook && currentConfig.detector_webhook === '1'){ var detector_webhook_url = s.addEventDetailsToString(d,currentConfig.detector_webhook_url) var webhookMethod = currentConfig.detector_webhook_method @@ -325,6 +425,11 @@ module.exports = function(s,config,lang){ if(err)s.debugLog(err) }) } + + for (var i = 0; i < s.onEventTriggerExtensions.length; i++) { + const extender = s.onEventTriggerExtensions[i] + await extender(d,filter) + } } //show client machines the event d.cx={f:'detector_trigger',id:d.id,ke:d.ke,details:d.details,doObjectDetection:d.doObjectDetection}; @@ -349,6 +454,7 @@ module.exports = function(s,config,lang){ s.group[d.ke].activeMonitors[d.id].eventBasedRecording.allowEnd = true s.group[d.ke].activeMonitors[d.id].eventBasedRecording.process.stdin.setEncoding('utf8') s.group[d.ke].activeMonitors[d.id].eventBasedRecording.process.stdin.write('q') + s.group[d.ke].activeMonitors[d.id].eventBasedRecording.process.kill('SIGINT') delete(s.group[d.ke].activeMonitors[d.id].eventBasedRecording.timeout) },detector_timeout * 1000 * 60) } @@ -371,7 +477,8 @@ module.exports = function(s,config,lang){ return } s.insertCompletedVideo(d.mon,{ - file : filename + file : filename, + events: s.group[d.ke].activeMonitors[d.id].detector_motion_count }) s.userLog(d,{type:lang["Traditional Recording"],msg:lang["Detector Recording Complete"]}) s.userLog(d,{type:lang["Traditional Recording"],msg:lang["Clear Recorder Process"]}) diff --git a/libs/extenders.js b/libs/extenders.js index 6c7fd86f..d1bf37ff 100644 --- a/libs/extenders.js +++ b/libs/extenders.js @@ -46,6 +46,7 @@ module.exports = function(s,config){ } // s.cloudDiskUseStartupExtensions = {} + s.cloudDiskUseOnGetVideoDataExtensions = {} ////// EVENTS ////// s.onEventTriggerExtensions = [] @@ -143,6 +144,11 @@ module.exports = function(s,config){ s.onWebSocketDisconnectionExtensions.push(callback) } // + s.onWebsocketMessageSendExtensions = [] + s.onWebsocketMessageSend = function(callback){ + s.onWebsocketMessageSendExtensions.push(callback) + } + // s.onGetCpuUsageExtensions = [] s.onGetCpuUsage = function(callback){ s.onGetCpuUsageExtensions.push(callback) diff --git a/libs/ffmpeg.js b/libs/ffmpeg.js index a6381e3b..a30a0cf0 100644 --- a/libs/ffmpeg.js +++ b/libs/ffmpeg.js @@ -103,6 +103,8 @@ module.exports = function(s,config,lang,onFinish){ auto: {label:lang['Auto'],value:'auto'}, drm: {label:lang['drm'],value:'drm'}, cuvid: {label:lang['cuvid'],value:'cuvid'}, + cuda: {label:lang['cuda'],value:'cuda'}, + opencl: {label:lang['opencl'],value:'opencl'}, vaapi: {label:lang['vaapi'],value:'vaapi'}, qsv: {label:lang['qsv'],value:'qsv'}, vdpau: {label:lang['vdpau'],value:'vdpau'}, @@ -158,8 +160,10 @@ module.exports = function(s,config,lang,onFinish){ }) }else{ var primaryMap = '0:0' - if(e.details.primary_input && e.details.primary_input !== '')primaryMap = e.details.primary_input - string += ' -map ' + primaryMap + if(e.details.primary_input && e.details.primary_input !== ''){ + var primaryMap = e.details.primary_input || '0:0' + string += ' -map ' + primaryMap + } } } return string; @@ -421,6 +425,8 @@ module.exports = function(s,config,lang,onFinish){ // x.hwaccel = '' x.cust_input = '' + //wallclock fix for strangely long, single frame videos + if((config.wallClockTimestampAsDefault || e.details.wall_clock_timestamp_ignore !== '1') && e.type === 'h264' && x.cust_input.indexOf('-use_wallclock_as_timestamps 1') === -1){x.cust_input+=' -use_wallclock_as_timestamps 1';} //input - frame rate (capture rate) if(e.details.sfps && e.details.sfps!==''){x.input_fps=' -r '+e.details.sfps}else{x.input_fps=''} //input - analyze duration @@ -635,21 +641,17 @@ module.exports = function(s,config,lang,onFinish){ //add input feed map x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.snap) } - if(!e.details.snap_fps || e.details.snap_fps === ''){e.details.snap_fps = 1} - if(e.details.snap_vf && e.details.snap_vf !== '' || e.cudaEnabled){ - var snapVf = e.details.snap_vf.split(',') - if(e.details.snap_vf === '')snapVf.shift() - if(e.cudaEnabled){ - snapVf.push('hwdownload,format=nv12') - } - //-vf "thumbnail_cuda=2,hwdownload,format=nv12" - x.snap_vf=' -vf "'+snapVf.join(',')+'"' - }else{ - x.snap_vf='' + var snapVf = e.details.snap_vf ? e.details.snap_vf.split(',') : [] + if(e.details.snap_vf === '')snapVf.shift() + if(e.cudaEnabled){ + snapVf.push('hwdownload,format=nv12') } - if(e.details.snap_scale_x && e.details.snap_scale_x !== '' && e.details.snap_scale_y && e.details.snap_scale_y !== ''){x.snap_ratio = ' -s '+e.details.snap_scale_x+'x'+e.details.snap_scale_y}else{x.snap_ratio=''} - if(e.details.cust_snap && e.details.cust_snap !== ''){x.cust_snap = ' '+e.details.cust_snap}else{x.cust_snap=''} - x.pipe+=' -update 1 -r '+e.details.snap_fps+x.cust_snap+x.snap_ratio+x.snap_vf+' "'+e.sdir+'s.jpg" -y'; + snapVf.push(`fps=${e.details.snap_fps || '1'}`) + //-vf "thumbnail_cuda=2,hwdownload,format=nv12" + x.pipe += ` -vf "${snapVf.join(',')}"` + if(e.details.snap_scale_x && e.details.snap_scale_x !== '' && e.details.snap_scale_y && e.details.snap_scale_y !== '')x.pipe += ' -s '+e.details.snap_scale_x+'x'+e.details.snap_scale_y + if(e.details.cust_snap)x.pipe += ' ' + e.details.cust_snap + x.pipe += ` -update 1 "${e.sdir}s.jpg" -y` } //custom - output if(e.details.custom_output&&e.details.custom_output!==''){x.pipe+=' '+e.details.custom_output;} @@ -670,7 +672,7 @@ module.exports = function(s,config,lang,onFinish){ x.dimensions = e.details.stream_scale_x+'x'+e.details.stream_scale_y; } //record - segmenting - x.segment = ' -f segment -segment_atclocktime 1 -reset_timestamps 1 -strftime 1 -segment_list pipe:2 -segment_time '+(60*e.cutoff)+' "'+e.dir+'%Y-%m-%dT%H-%M-%S.'+e.ext+'"'; + x.segment = ' -f segment -segment_atclocktime 1 -reset_timestamps 1 -strftime 1 -segment_list pipe:8 -segment_time '+(60*e.cutoff)+' "'+e.dir+'%Y-%m-%dT%H-%M-%S.'+e.ext+'"'; //record - set defaults for extension, video quality switch(e.ext){ case'mp4': @@ -692,7 +694,6 @@ module.exports = function(s,config,lang,onFinish){ if(e.details.acodec&&e.details.acodec!==''&&e.details.acodec!=='default'){x.acodec=e.details.acodec} if(e.details.cust_record.indexOf('-strict -2') === -1){x.cust_record.push(' -strict -2')} if(e.details.cust_record.indexOf('-threads')===-1){x.cust_record.push(' -threads 1')} - // if(e.details.cust_input&&(e.details.cust_input.indexOf('-use_wallclock_as_timestamps 1')>-1)===false){e.details.cust_input+=' -use_wallclock_as_timestamps 1';} //record - ready or reset codecs if(x.acodec!=='no'){ if(x.acodec.indexOf('none')>-1){x.acodec=''}else{x.acodec=' -acodec '+x.acodec} @@ -794,40 +795,58 @@ module.exports = function(s,config,lang,onFinish){ //add input feed map x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.detector) } - if(!e.details.detector_fps || e.details.detector_fps === ''){x.detector_fps = 2}else{x.detector_fps = parseInt(e.details.detector_fps)} - if(e.details.detector_scale_x && e.details.detector_scale_x !== '' && e.details.detector_scale_y && e.details.detector_scale_y !== ''){x.dratio=' -s '+e.details.detector_scale_x+'x'+e.details.detector_scale_y}else{x.dratio=' -s 320x240'} + x.dratio = ` -s ${e.details.detector_scale_x ? e.details.detector_scale_x : '640'}x${e.details.detector_scale_y ? e.details.detector_scale_y : '480'}` if(e.details.cust_detect&&e.details.cust_detect!==''){x.cust_detect+=e.details.cust_detect;} - if(sendFramesGlobally)x.pipe += ' -r ' + x.detector_fps + x.dratio + x.cust_detect + if(sendFramesGlobally)x.pipe += x.dratio + x.cust_detect x.detector_vf = [] if(e.cudaEnabled){ x.detector_vf.push('hwdownload,format=nv12') } + x.detector_vf.push('fps=' + (e.details.detector_fps ? e.details.detector_fps : '2')) if(sendFramesGlobally && x.detector_vf.length > 0)x.pipe += ' -vf "'+x.detector_vf.join(',')+'"' var h264Output = ' -q:v 1 -an -c:v libx264 -f hls -tune zerolatency -g 1 -hls_time 2 -hls_list_size 3 -start_number 0 -live_start_index 3 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "'+e.sdir+'detectorStreamX.m3u8"' + var setObjectDetectValues = () => { + //for object detection + if( + e.details.detector_scale_x_object && + e.details.detector_scale_x_object !=='' && + e.details.detector_scale_y_object && + e.details.detector_scale_y_object!=='' + ){ + x.dobjratio = ' -s '+e.details.detector_scale_x_object+'x'+e.details.detector_scale_y_object + }else{ + x.dobjratio = x.dratio + } + if(e.details.cust_detect_object)x.pipe += e.details.cust_detect_object + x.pipe += x.dobjratio + ' -vf fps=' + (e.details.detector_fps_object || '2') + } if(e.details.detector_pam === '1'){ if(sendFramesGlobally && e.cudaEnabled){ x.pipe += ' -vf "hwdownload,format=nv12"' } - if(sendFramesGlobally)x.pipe += ' -an -c:v pam -pix_fmt gray -f image2pipe pipe:3' - if(e.details.detector_use_detect_object === '1'){ - //for object detection - x.detector_fps_object = '2' + if(sendFramesGlobally){ x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.detector) - if(e.details.detector_scale_x_object&&e.details.detector_scale_x_object!==''&&e.details.detector_scale_y_object&&e.details.detector_scale_y_object!==''){x.dobjratio=' -s '+e.details.detector_scale_x_object+'x'+e.details.detector_scale_y_object}else{x.dobjratio=x.dratio} - if(e.details.detector_fps_object){x.detector_fps_object = e.details.detector_fps_object} - x.pipe += ' -r ' + x.detector_fps_object + x.dobjratio + x.cust_detect + x.pipe += ' -an -c:v pam -pix_fmt gray -f image2pipe pipe:3' + } + if(e.details.detector_use_detect_object === '1'){ + x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.detector) + setObjectDetectValues() if(e.details.detector_h264 === '1'){ x.pipe += h264Output }else{ x.pipe += ' -an -f singlejpeg pipe:4' } } - }else if(sendFramesGlobally){ + }else if(sendFramesGlobally || sendFramesToObjectDetector){ + if(sendFramesToObjectDetector){ + x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.detector) + setObjectDetectValues() + } if(e.details.detector_h264 === '1'){ x.pipe += h264Output }else{ - x.pipe += ' -an -f singlejpeg pipe:3' + x.pipe += ' -an -f singlejpeg pipe:4' } } } @@ -908,39 +927,38 @@ module.exports = function(s,config,lang,onFinish){ } ffmpeg.buildTimelapseOutput = function(e,x){ if(e.details.record_timelapse === '1'){ - x.record_timelapse_video_filters = [] + var recordTimelapseVideoFilters = [] + var flags = [] if(e.details.input_map_choices&&e.details.input_map_choices.record_timelapse){ //add input feed map x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.record_timelapse) } - var flags = [] - if(e.details.record_timelapse_fps && e.details.record_timelapse_fps !== ''){ - flags.push('-r 1/' + e.details.record_timelapse_fps) - }else{ - flags.push('-r 1/900') // 15 minutes - } + recordTimelapseVideoFilters.push('fps=1/' + (e.details.record_timelapse_fps ? e.details.record_timelapse_fps : '900')) if(e.details.record_timelapse_vf && e.details.record_timelapse_vf !== '')flags.push('-vf ' + e.details.record_timelapse_vf) if(e.details.record_timelapse_scale_x && e.details.record_timelapse_scale_x !== '' && e.details.record_timelapse_scale_y && e.details.record_timelapse_scale_y !== '')flags.push(`-s ${e.details.record_timelapse_scale_x}x${e.details.record_timelapse_scale_y}`) //record - watermark for -vf if(e.details.record_timelapse_watermark&&e.details.record_timelapse_watermark=="1"&&e.details.record_timelapse_watermark_location&&e.details.record_timelapse_watermark_location!==''){ switch(e.details.record_timelapse_watermark_position){ case'tl'://top left - x.record_timelapse_watermark_position='10:10' + x.record_timelapse_watermark_position = '10:10' break; case'tr'://top right - x.record_timelapse_watermark_position='main_w-overlay_w-10:10' + x.record_timelapse_watermark_position = 'main_w-overlay_w-10:10' break; case'bl'://bottom left - x.record_timelapse_watermark_position='10:main_h-overlay_h-10' + x.record_timelapse_watermark_position = '10:main_h-overlay_h-10' break; default://bottom right - x.record_timelapse_watermark_position='(main_w-overlay_w-10)/2:(main_h-overlay_h-10)/2' + x.record_timelapse_watermark_position = '(main_w-overlay_w-10)/2:(main_h-overlay_h-10)/2' break; } - x.record_timelapse_video_filters.push('movie='+e.details.record_timelapse_watermark_location+'[watermark],[in][watermark]overlay='+x.record_timelapse_watermark_position+'[out]'); + recordTimelapseVideoFilters.push( + 'movie=' + e.details.record_timelapse_watermark_location, + `[watermark],[in][watermark]overlay=${x.record_timelapse_watermark_position}[out]` + ) } - if(x.record_timelapse_video_filters.length > 0){ - var videoFilter = `-vf "${x.record_timelapse_video_filters.join(',').trim()}"` + if(recordTimelapseVideoFilters.length > 0){ + var videoFilter = `-vf "${recordTimelapseVideoFilters.join(',').trim()}"` flags.push(videoFilter) } x.pipe += ` -f singlejpeg ${flags.join(' ')} -an -q:v 1 pipe:7` @@ -951,7 +969,7 @@ module.exports = function(s,config,lang,onFinish){ x.ffmpegCommandString = x.loglevel+x.input_fps; //progress pipe x.ffmpegCommandString += ' -progress pipe:5'; - + const url = s.buildMonitorUrl(e); switch(e.type){ case'dashcam': x.ffmpegCommandString += x.cust_input+x.hwaccel+' -i -'; @@ -960,17 +978,17 @@ module.exports = function(s,config,lang,onFinish){ x.ffmpegCommandString += ' -pattern_type glob -f image2pipe'+x.record_fps+' -vcodec mjpeg'+x.cust_input+x.hwaccel+' -i -'; break; case'mjpeg': - x.ffmpegCommandString += ' -reconnect 1 -f mjpeg'+x.cust_input+x.hwaccel+' -i "'+e.url+'"'; + x.ffmpegCommandString += ' -reconnect 1 -f mjpeg'+x.cust_input+x.hwaccel+' -i "'+url+'"'; break; case'mxpeg': - x.ffmpegCommandString += ' -reconnect 1 -f mxg'+x.cust_input+x.hwaccel+' -i "'+e.url+'"'; + x.ffmpegCommandString += ' -reconnect 1 -f mxg'+x.cust_input+x.hwaccel+' -i "'+url+'"'; break; case'rtmp': if(!e.details.rtmp_key)e.details.rtmp_key = '' x.ffmpegCommandString += x.cust_input+x.hwaccel+` -i "rtmp://127.0.0.1:1935/${e.ke + '_' + e.mid + '_' + e.details.rtmp_key}"`; break; case'h264':case'hls':case'mp4': - x.ffmpegCommandString += x.cust_input+x.hwaccel+' -i "'+e.url+'"'; + x.ffmpegCommandString += x.cust_input+x.hwaccel+' -i "'+url+'"'; break; case'local': x.ffmpegCommandString += x.cust_input+x.hwaccel+' -i "'+e.path+'"'; @@ -1013,11 +1031,40 @@ module.exports = function(s,config,lang,onFinish){ ffmpeg.assembleMainPieces(e,x) ffmpeg.createPipeArray(e,x) //hold ffmpeg command for log stream - s.group[e.ke].activeMonitors[e.mid].ffmpeg = x.ffmpegCommandString + var sanitizedCmd = x.ffmpegCommandString + if(e.details.muser && e.details.mpass){ + sanitizedCmd = sanitizedCmd + .replace(`//${e.details.muser}:${e.details.mpass}@`,'//') + .replace(`=${e.details.muser}`,'=USERNAME') + .replace(`=${e.details.mpass}`,'=PASSWORD') + }else if(e.details.muser){ + sanitizedCmd = sanitizedCmd.replace(`//${e.details.muser}:@`,'//').replace(`=${e.details.muser}`,'=USERNAME') + } + s.group[e.ke].activeMonitors[e.mid].ffmpeg = sanitizedCmd //clean the string of spatial impurities and split for spawn() x.ffmpegCommandString = s.splitForFFPMEG(x.ffmpegCommandString) //launch that bad boy - return spawn(config.ffmpegDir,x.ffmpegCommandString,{detached: true,stdio:x.stdioPipes}) + // return spawn(config.ffmpegDir,x.ffmpegCommandString,{detached: true,stdio:x.stdioPipes}) + try{ + fs.unlinkSync(e.sdir + 'cmd.txt') + }catch(err){ + + } + fs.writeFileSync(e.sdir + 'cmd.txt',JSON.stringify({ + cmd: x.ffmpegCommandString, + pipes: x.stdioPipes.length, + rawMonitorConfig: s.group[e.ke].rawMonitorConfigurations[e.id], + globalInfo: { + config: config, + isAtleatOneDetectorPluginConnected: s.isAtleatOneDetectorPluginConnected + } + },null,3),'utf8') + var cameraCommandParams = [ + './libs/cameraThread/singleCamera.js', + config.ffmpegDir, + e.sdir + 'cmd.txt' + ] + return spawn('node',cameraCommandParams,{detached: true,stdio:x.stdioPipes}) } if(!config.ffmpegDir){ ffmpeg.checkForWindows(function(){ diff --git a/libs/fileBin.js b/libs/fileBin.js index 02e1f422..22de6f8c 100644 --- a/libs/fileBin.js +++ b/libs/fileBin.js @@ -1,13 +1,254 @@ var fs = require('fs') var moment = require('moment') module.exports = function(s,config,lang,app,io){ - s.getFileBinDirectory = function(e){ - if(e.mid&&!e.id){e.id=e.mid} - s.checkDetails(e) - if(e.details&&e.details.dir&&e.details.dir!==''){ - return s.checkCorrectPathEnding(e.details.dir)+e.ke+'/'+e.id+'/' - }else{ - return s.dir.fileBin+e.ke+'/'+e.id+'/'; - } + const getFileBinDirectory = function(monitor){ + return s.dir.fileBin + monitor.ke + '/' + monitor.mid + '/' } + const getFileBinEntry = (options) => { + return new Promise((resolve, reject) => { + s.knexQuery({ + action: "select", + columns: "*", + table: "Files", + where: options + },(err,rows) => { + if(rows[0]){ + resolve(rows[0]) + }else{ + resolve() + } + }) + }) + } + const getFileBinEntries = (options) => { + return new Promise((resolve, reject) => { + s.knexQuery({ + action: "select", + columns: "*", + table: "Files", + where: options + },(err,rows) => { + if(rows){ + resolve(rows) + }else{ + resolve([]) + } + }) + }) + } + const updateFileBinEntry = (options) => { + return new Promise((resolve, reject) => { + const groupKey = options.ke + const monitorId = options.mid + const filename = options.name + const update = options.update + if(!filename){ + resolve('No Filename') + return + } + if(!update){ + resolve('No Update Options') + return + } + s.knexQuery({ + action: "select", + columns: "size", + table: "Files", + where: { + ke: groupKey, + mid: monitorId, + name: filename, + } + },(err,rows) => { + if(rows[0]){ + const fileSize = rows[0].size + s.knexQuery({ + action: "update", + table: "Files", + where: { + ke: groupKey, + mid: monitorId, + name: filename, + }, + update: update + },(err) => { + resolve() + if(update.size){ + s.setDiskUsedForGroup(groupKey,-(fileSize/1048576),'fileBin') + s.setDiskUsedForGroup(groupKey,(update.size/1048576),'fileBin') + s.purgeDiskForGroup(groupKey) + } + }) + }else{ + resolve() + } + }) + }) + } + const deleteFileBinEntry = (options) => { + return new Promise((resolve, reject) => { + const groupKey = options.ke + const monitorId = options.mid + const filename = options.name + if(!filename){ + resolve('No Filename') + return + } + s.knexQuery({ + action: "select", + columns: "size", + table: "Files", + where: { + ke: groupKey, + mid: monitorId, + name: filename, + } + },(err,rows) => { + if(rows[0]){ + const fileSize = rows[0].size + s.knexQuery({ + action: "delete", + table: "Files", + where: { + ke: groupKey, + mid: monitorId, + name: filename, + } + },(err) => { + resolve() + s.setDiskUsedForGroup(groupKey,-(fileSize/1048576),'fileBin') + s.purgeDiskForGroup(groupKey) + }) + }else{ + resolve() + } + }) + }) + } + const insertFileBinEntry = (options) => { + return new Promise((resolve, reject) => { + const groupKey = options.ke + const monitorId = options.mid + const filename = options.name + if(!filename){ + resolve('No Filename') + return + } + const monitorFileBinDirectory = getFileBinDirectory({ke: groupKey,mid: monitorId,}) + const fileSize = options.size || fs.lstatSync(monitorFileBinDirectory + filename).size + const details = options.details instanceof Object ? JSON.stringify(options.details) : options.details + const status = options.status || 0 + const time = options.time || new Date() + s.knexQuery({ + action: "insert", + table: "Files", + insert: { + ke: groupKey, + mid: monitorId, + name: filename, + size: fileSize, + details: details, + status: status, + time: time, + } + },(err) => { + resolve() + s.setDiskUsedForGroup(groupKey,(fileSize/1048576),'fileBin') + s.purgeDiskForGroup(groupKey) + }) + }) + } + s.getFileBinDirectory = getFileBinDirectory + s.getFileBinEntry = getFileBinEntry + s.insertFileBinEntry = insertFileBinEntry + s.updateFileBinEntry = updateFileBinEntry + s.deleteFileBinEntry = deleteFileBinEntry + /** + * API : Get fileBin file rows + */ + app.get([config.webPaths.apiPrefix+':auth/fileBin/:ke',config.webPaths.apiPrefix+':auth/fileBin/:ke/:id'],async (req,res) => { + s.auth(req.params,(user) => { + const userDetails = user.details + const monitorId = req.params.id + const groupKey = req.params.ke + const hasRestrictions = userDetails.sub && userDetails.allmonitors !== '1'; + s.sqlQueryBetweenTimesWithPermissions({ + table: 'Files', + user: user, + groupKey: req.params.ke, + monitorId: req.params.id, + startTime: req.query.start, + endTime: req.query.end, + startTimeOperator: req.query.startOperator, + endTimeOperator: req.query.endOperator, + limit: req.query.limit, + endIsStartTo: true, + noFormat: true, + noCount: true, + preliminaryValidationFailed: ( + user.permissions.get_monitors === "0" + ) + },(response) => { + response.forEach(function(v){ + v.details = s.parseJSON(v.details) + v.href = '/'+req.params.auth+'/fileBin/'+req.params.ke+'/'+req.params.id+'/'+v.details.year+'/'+v.details.month+'/'+v.details.day+'/'+v.name; + }) + s.closeJsonResponse(res,{ + ok: true, + files: response + }) + }) + },res,req); + }); + /** + * API : Get fileBin file + */ + app.get(config.webPaths.apiPrefix+':auth/fileBin/:ke/:id/:year/:month/:day/:file', async (req,res) => { + s.auth(req.params,function(user){ + var failed = function(){ + res.end(user.lang['File Not Found']) + } + if (!s.group[req.params.ke].fileBin[req.params.id+'/'+req.params.file]){ + const groupKey = req.params.ke + const monitorId = req.params.id + const monitorRestrictions = s.getMonitorRestrictions(user.details,monitorId) + if(user.details.sub && user.details.allmonitors === '0' && (user.permissions.get_monitors === "0" || monitorRestrictions.length === 0)){ + s.closeJsonResponse(res,{ + ok: false, + msg: lang['Not Permitted'] + }) + return + } + s.knexQuery({ + action: "select", + columns: "*", + table: "Files", + where: [ + ['ke','=',groupKey], + ['mid','=',req.params.id], + ['name','=',req.params.file], + monitorRestrictions + ] + },(err,r) => { + if(r && r[0]){ + r = r[0] + r.details = JSON.parse(r.details) + req.dir = s.dir.fileBin + req.params.ke + '/' + req.params.id + '/' + r.details.year + '/' + r.details.month + '/' + r.details.day + '/' + req.params.file; + fs.stat(req.dir,function(err,stats){ + if(!err){ + res.on('finish',function(){res.end()}) + fs.createReadStream(req.dir).pipe(res) + }else{ + failed() + } + }) + }else{ + failed() + } + }) + }else{ + res.end(user.lang['Please Wait for Completion']) + } + },res,req); + }); } diff --git a/libs/folders.js b/libs/folders.js index e0c9fc91..54f9acc6 100644 --- a/libs/folders.js +++ b/libs/folders.js @@ -11,6 +11,7 @@ module.exports = function(s,config,lang){ }else{ config.streamDir = config.windowsTempDir } + config.shmDir = `${s.checkCorrectPathEnding(config.streamDir)}` if(!fs.existsSync(config.streamDir)){ config.streamDir = s.mainDirectory+'/streams/' }else{ diff --git a/libs/health.js b/libs/health.js index 0ead4917..31d2755e 100644 --- a/libs/health.js +++ b/libs/health.js @@ -1,79 +1,130 @@ +var fs = require('fs'); var exec = require('child_process').exec; var spawn = require('child_process').spawn; +const { getCpuUsageOnLinux, getRamUsageOnLinux } = require('./health/utils.js') module.exports = function(s,config,lang,io){ s.heartBeat = function(){ setTimeout(s.heartBeat, 8000); io.sockets.emit('ping',{beat:1}); } s.heartBeat() - s.cpuUsage = function(callback){ - k={} - switch(s.platform){ - case'win32': - k.cmd="@for /f \"skip=1\" %p in ('wmic cpu get loadpercentage') do @echo %p%" - break; - case'darwin': - k.cmd="ps -A -o %cpu | awk '{s+=$1} END {print s}'"; - break; - case'linux': - k.cmd='LANG=C top -b -n 2 | grep "^'+config.cpuUsageMarker+'" | awk \'{print $2}\' | tail -n1'; - break; - case'freebsd': - k.cmd='vmstat 1 2 | tail -1 | awk \'{print $17}\'' - break; + let hasProcStat = false + try{ + fs.statSync("/proc/stat") + hasProcStat = true + }catch(err){ + + } + if(hasProcStat){ + s.cpuUsage = async () => { + const percent = await getCpuUsageOnLinux() + return percent } - if(config.customCpuCommand){ - exec(config.customCpuCommand,{encoding:'utf8',detached: true},function(err,d){ - if(s.isWin===true) { - d = d.replace(/(\r\n|\n|\r)/gm, "").replace(/%/g, "") - } - callback(d) - s.onGetCpuUsageExtensions.forEach(function(extender){ - extender(d) - }) - }) - } else if(k.cmd){ - exec(k.cmd,{encoding:'utf8',detached: true},function(err,d){ - if(s.isWin===true){ - d=d.replace(/(\r\n|\n|\r)/gm,"").replace(/%/g,"") - } - callback(d) - s.onGetCpuUsageExtensions.forEach(function(extender){ - extender(d) - }) - }) - } else { - callback(0) + }else{ + s.cpuUsage = () => { + return new Promise((resolve, reject) => { + var k = {} + switch(s.platform){ + case'win32': + k.cmd = "@for /f \"skip=1\" %p in ('wmic cpu get loadpercentage') do @echo %p%" + break; + case'darwin': + k.cmd = "ps -A -o %cpu | awk '{s+=$1} END {print s}'"; + break; + case'linux': + k.cmd = 'top -b -n 2 | awk \'toupper($0) ~ /^.?CPU/ {gsub("id,","100",$8); gsub("%","",$8); print 100-$8}\' | tail -n 1'; + break; + case'freebsd': + k.cmd = 'vmstat 1 2 | awk \'END{print 100-$19}\'' + break; + case'openbsd': + k.cmd = 'vmstat 1 2 | awk \'END{print 100-$18}\'' + break; + } + if(config.customCpuCommand){ + exec(config.customCpuCommand,{encoding:'utf8',detached: true},function(err,d){ + if(s.isWin===true) { + d = d.replace(/(\r\n|\n|\r)/gm, "").replace(/%/g, "") + } + resolve(d) + s.onGetCpuUsageExtensions.forEach(function(extender){ + extender(d) + }) + }) + } else if(k.cmd){ + exec(k.cmd,{encoding:'utf8',detached: true},function(err,d){ + if(s.isWin===true){ + d=d.replace(/(\r\n|\n|\r)/gm,"").replace(/%/g,"") + } + resolve(d) + s.onGetCpuUsageExtensions.forEach(function(extender){ + extender(d) + }) + }) + } else { + resolve(0) + } + }) } } - s.ramUsage = function(callback){ - k={} - switch(s.platform){ - case'win32': - k.cmd = "wmic OS get FreePhysicalMemory /Value" - break; - case'darwin': - k.cmd = "vm_stat | awk '/^Pages free: /{f=substr($3,1,length($3)-1)} /^Pages active: /{a=substr($3,1,length($3-1))} /^Pages inactive: /{i=substr($3,1,length($3-1))} /^Pages speculative: /{s=substr($3,1,length($3-1))} /^Pages wired down: /{w=substr($4,1,length($4-1))} /^Pages occupied by compressor: /{c=substr($5,1,length($5-1)); print ((a+w)/(f+a+i+w+s+c))*100;}'" - break; - case'freebsd': - k.cmd = "echo \"scale=4; $(vmstat -H | tail -1 | awk '{print $5}')*1024*100/$(sysctl hw.physmem | awk '{print $2}')\" | bc" - break; - default: - k.cmd = "LANG=C free | grep Mem | awk '{print $7/$2 * 100.0}'"; - break; + let hasProcMeminfo = false + try{ + fs.statSync("/proc/meminfo") + hasProcMeminfo = true + }catch(err){ + + } + if(hasProcMeminfo){ + s.ramUsage = async () => { + const used = await getRamUsageOnLinux() + return used } - if(k.cmd){ - exec(k.cmd,{encoding:'utf8',detached: true},function(err,d){ - if(s.isWin===true){ - d=(parseInt(d.split('=')[1])/(s.totalmem/1000))*100 - } - callback(d) - s.onGetRamUsageExtensions.forEach(function(extender){ - extender(d) - }) - }) - }else{ - callback(0) + }else{ + s.ramUsage = () => { + return new Promise((resolve, reject) => { + k={} + switch(s.platform){ + case'win32': + k.cmd = "wmic OS get FreePhysicalMemory /Value" + break; + case'darwin': + k.cmd = "vm_stat | awk '/^Pages free: /{f=substr($3,1,length($3)-1)} /^Pages active: /{a=substr($3,1,length($3-1))} /^Pages inactive: /{i=substr($3,1,length($3-1))} /^Pages speculative: /{s=substr($3,1,length($3-1))} /^Pages wired down: /{w=substr($4,1,length($4-1))} /^Pages occupied by compressor: /{c=substr($5,1,length($5-1)); print ((a+w)/(f+a+i+w+s+c))*100;}'" + break; + case'freebsd': + k.cmd = "echo \"scale=4; $(vmstat -H | awk 'END{print $5}')*1024*100/$(sysctl -n hw.physmem)\" | bc" + break; + case'openbsd': + k.cmd = "echo \"scale=4; $(vmstat | awk 'END{ gsub(\"M\",\"\",$4); print $4 }')*104857600/$(sysctl -n hw.physmem)\" | bc" + break; + default: + k.cmd = "LANG=C free | grep Mem | awk '{print $7/$2 * 100.0}'"; + break; + } + if(k.cmd){ + exec(k.cmd,{encoding:'utf8',detached: true},function(err,d){ + if(s.isWin===true){ + d=(parseInt(d.split('=')[1])/(s.totalmem/1000))*100 + } + resolve(d) + s.onGetRamUsageExtensions.forEach(function(extender){ + extender(d) + }) + }) + }else{ + resolve(0) + } + }) } } + if(config.childNodes.mode !== 'child'){ + setInterval(async () => { + const cpu = await s.cpuUsage() + const ram = await s.ramUsage() + s.tx({ + f: 'os', + cpu: cpu, + ram: ram + },'CPU') + },10000) + } } diff --git a/libs/health/utils.js b/libs/health/utils.js new file mode 100644 index 00000000..98eecdfc --- /dev/null +++ b/libs/health/utils.js @@ -0,0 +1,66 @@ +// This file's contents were referenced from https://gist.github.com/sidwarkd/9578213 +const fs = require('fs'); +const calculateCPUPercentage = function(oldVals, newVals){ + var totalDiff = newVals.total - oldVals.total; + var activeDiff = newVals.active - oldVals.active; + return Math.ceil((activeDiff / totalDiff) * 100); +}; +function getValFromLine(line){ + var match = line.match(/[0-9]+/gi); + if(match !== null) + return parseInt(match[0]); + else + return null; +}; +const currentCPUInfo = { + total: 0, + active: 0 +} +const lastCPUInfo = { + total: 0, + active: 0 +} +exports.getCpuUsageOnLinux = () => { + lastCPUInfo.active = currentCPUInfo.active; + lastCPUInfo.idle = currentCPUInfo.idle; + lastCPUInfo.total = currentCPUInfo.total; + return new Promise((resolve,reject) => { + const getUsage = function(callback){ + fs.readFile("/proc/stat" ,'utf8', function(err, data){ + var lines = data.split('\n'); + var cpuTimes = lines[0].match(/[0-9]+/gi); + currentCPUInfo.total = 0; + currentCPUInfo.idle = parseInt(cpuTimes[3]) + parseInt(cpuTimes[4]); + for (var i = 0; i < cpuTimes.length; i++){ + currentCPUInfo.total += parseInt(cpuTimes[i]); + } + currentCPUInfo.active = currentCPUInfo.total - currentCPUInfo.idle + currentCPUInfo.percentUsed = calculateCPUPercentage(lastCPUInfo, currentCPUInfo); + callback(currentCPUInfo.percentUsed) + }) + } + getUsage(function(percentage){ + setTimeout(function(){ + getUsage(function(percentage){ + resolve(percentage); + }) + }, 3000) + }) + }) +} +exports.getRamUsageOnLinux = () => { + return new Promise((resolve,reject) => { + fs.readFile("/proc/meminfo", 'utf8', function(err, data){ + const lines = data.split('\n'); + const total = Math.floor(getValFromLine(lines[0]) / 1024); + const free = Math.floor(getValFromLine(lines[1]) / 1024); + const cached = Math.floor(getValFromLine(lines[4]) / 1024); + const used = total - free; + const percentUsed = Math.ceil(((used - cached) / total) * 100); + resolve({ + used: used, + percent: percentUsed, + }); + }) + }) +} diff --git a/libs/monitor.js b/libs/monitor.js index acddda84..7e154932 100644 --- a/libs/monitor.js +++ b/libs/monitor.js @@ -1,20 +1,22 @@ -var fs = require('fs'); -var events = require('events'); -var spawn = require('child_process').spawn; -var exec = require('child_process').exec; -var Mp4Frag = require('mp4frag'); -var onvif = require('node-onvif'); -var request = require('request'); -var connectionTester = require('connection-tester') -var SoundDetection = require('shinobi-sound-detection') -var async = require("async"); -var URL = require('url') +const fs = require('fs'); +const events = require('events'); +const spawn = require('child_process').spawn; +const exec = require('child_process').exec; +const Mp4Frag = require('mp4frag'); +const onvif = require('node-onvif'); +const treekill = require('tree-kill'); +const request = require('request'); +const connectionTester = require('connection-tester') +const SoundDetection = require('shinobi-sound-detection') +const async = require("async"); +const URL = require('url') +const { copyObject, createQueue } = require('./common.js') module.exports = function(s,config,lang){ - const startMonitorInQueue = async.queue(function(action, callback) { - setTimeout(function(){ - action(callback) - },2000) - }, 1) + const { cameraDestroy } = require('./monitor/utils.js')(s,config,lang) + const { + setPresetForCurrentPosition + } = require('./control/ptz.js')(s,config,lang) + const startMonitorInQueue = createQueue(1, 3) s.initiateMonitorObject = function(e){ if(!s.group[e.ke]){s.group[e.ke]={}}; if(!s.group[e.ke].activeMonitors){s.group[e.ke].activeMonitors={}} @@ -31,7 +33,10 @@ module.exports = function(s,config,lang){ // if(!s.group[e.ke].activeMonitors[e.mid].viewerConnection){s.group[e.ke].activeMonitors[e.mid].viewerConnection={}}; // if(!s.group[e.ke].activeMonitors[e.mid].viewerConnectionCount){s.group[e.ke].activeMonitors[e.mid].viewerConnectionCount=0}; if(!s.group[e.ke].activeMonitors[e.mid].parsedObjects){s.group[e.ke].activeMonitors[e.mid].parsedObjects={}}; + if(!s.group[e.ke].activeMonitors[e.mid].detector_motion_count){s.group[e.ke].activeMonitors[e.mid].detector_motion_count=[]}; + if(!s.group[e.ke].activeMonitors[e.mid].eventsCounted){s.group[e.ke].activeMonitors[e.mid].eventsCounted = {}}; if(!s.group[e.ke].activeMonitors[e.mid].isStarted){s.group[e.ke].activeMonitors[e.mid].isStarted = false}; + if(!s.group[e.ke].activeMonitors[e.mid].pipe4BufferPieces){s.group[e.ke].activeMonitors[e.mid].pipe4BufferPieces = []}; if(s.group[e.ke].activeMonitors[e.mid].delete){clearTimeout(s.group[e.ke].activeMonitors[e.mid].delete)} if(!s.group[e.ke].rawMonitorConfigurations){s.group[e.ke].rawMonitorConfigurations={}} s.onMonitorInitExtensions.forEach(function(extender){ @@ -44,26 +49,26 @@ module.exports = function(s,config,lang){ } s.getMonitorCpuUsage = function(e,callback){ if(s.group[e.ke].activeMonitors[e.mid] && s.group[e.ke].activeMonitors[e.mid].spawn){ - var getUsage = function(callback2){ + const getUsage = function(callback2){ s.readFile("/proc/" + s.group[e.ke].activeMonitors[e.mid].spawn.pid + "/stat", function(err, data){ if(!err){ - var elems = data.toString().split(' '); - var utime = parseInt(elems[13]); - var stime = parseInt(elems[14]); + const elems = data.toString().split(' '); + const utime = parseInt(elems[13]); + const stime = parseInt(elems[14]); callback2(utime + stime); }else{ - clearInterval(s.group[e.ke].activeMonitors[e.mid].getMonitorCpuUsage) + clearInterval(0) } }) } getUsage(function(startTime){ setTimeout(function(){ getUsage(function(endTime){ - var delta = endTime - startTime; - var percentage = 100 * (delta / 10000); + const delta = endTime - startTime; + const percentage = 100 * (delta / 10000); callback(percentage) - }); + }) }, 1000) }) }else{ @@ -100,129 +105,152 @@ module.exports = function(s,config,lang){ }); return x.ar; } - s.getRawSnapshotFromMonitor = function(monitor,options,callback){ - if(!callback){ - callback = options - var options = {flags: ''} - } - s.checkDetails(monitor) - var inputOptions = [] - var outputOptions = [] - var streamDir = s.dir.streams + monitor.ke + '/' + monitor.mid + '/' - var url = options.url - var secondsInward = options.secondsInward || '0' - if(secondsInward.length === 1)secondsInward = '0' + secondsInward - if(options.flags)outputOptions.push(options.flags) - const checkExists = function(streamDir,callback){ - s.fileStats(streamDir,function(err){ - var response = false - if(err){ - // s.debugLog(err) - }else{ - response = true + s.getStreamsDirectory = (monitor) => { + return s.dir.streams + monitor.ke + '/' + monitor.mid + '/' + } + s.getRawSnapshotFromMonitor = function(monitor,options){ + return new Promise((resolve,reject) => { + options = options instanceof Object ? options : {flags: ''} + s.checkDetails(monitor) + var inputOptions = [] + var outputOptions = [] + var streamDir = s.dir.streams + monitor.ke + '/' + monitor.mid + '/' + var url = options.url + var secondsInward = options.secondsInward || '0' + if(secondsInward.length === 1)secondsInward = '0' + secondsInward + if(options.flags)outputOptions.push(options.flags) + const checkExists = function(streamDir,callback){ + s.fileStats(streamDir,function(err){ + var response = false + if(err){ + // s.debugLog(err) + }else{ + response = true + } + callback(response) + }) + } + const noIconChecks = function(){ + const runExtraction = function(){ + var sendTempImage = function(){ + fs.readFile(temporaryImageFile,function(err,buffer){ + if(!err){ + resolve({ + screenShot: buffer, + isStaticFile: false + }) + } + fs.unlink(temporaryImageFile,function(){}) + }) + } + try{ + var snapBuffer = [] + var temporaryImageFile = streamDir + s.gid(5) + '.jpg' + var iconImageFile = streamDir + 'icon.jpg' + var ffmpegCmd = s.splitForFFPMEG(`-loglevel warning -re -probesize 100000 -analyzeduration 100000 ${inputOptions.join(' ')} -i "${url}" ${outputOptions.join(' ')} -f image2 -an -vf "fps=1" -vframes 1 "${temporaryImageFile}"`) + fs.writeFileSync(s.getStreamsDirectory(monitor) + 'snapCmd.txt',JSON.stringify({ + cmd: ffmpegCmd, + temporaryImageFile: temporaryImageFile, + iconImageFile: iconImageFile, + useIcon: options.useIcon, + rawMonitorConfig: s.group[monitor.ke].rawMonitorConfigurations[monitor.mid], + },null,3),'utf8') + var cameraCommandParams = [ + s.mainDirectory + '/libs/cameraThread/snapshot.js', + config.ffmpegDir, + s.group[monitor.ke].activeMonitors[monitor.id].sdir + 'snapCmd.txt' + ] + var snapProcess = spawn('node',cameraCommandParams,{detached: true}) + snapProcess.stderr.on('data',function(data){ + s.debugLog(data.toString()) + }) + snapProcess.on('close',function(data){ + clearTimeout(snapProcessTimeout) + sendTempImage() + }) + var snapProcessTimeout = setTimeout(function(){ + var pid = snapProcess.pid + if(s.isWin){ + spawn("taskkill", ["/pid", pid, '/t']) + }else{ + process.kill(-pid, 'SIGTERM') + } + setTimeout(function(){ + if(s.isWin === false){ + treekill(pid) + }else{ + snapProcess.kill() + } + },10000) + },30000) + }catch(err){ + console.log(err) + } } - callback(response) - }) - } - const noIconChecks = function(){ - const runExtraction = function(){ - try{ - var snapBuffer = [] - var temporaryImageFile = streamDir + s.gid(5) + '.jpg' - var iconImageFile = streamDir + 'icon.jpg' - var ffmpegCmd = `-loglevel quiet -re -probesize 1000000 -analyzeduration 1000000 ${inputOptions.join(' ')} -i "${url}" ${outputOptions.join(' ')} -vframes 1 "${temporaryImageFile}"` - var snapProcess = spawn(config.ffmpegDir,s.splitForFFPMEG(ffmpegCmd),{detached: true}) - snapProcess.stderr.on('data',function(data){ - console.log(data.toString()) - }) - snapProcess.on('close',function(data){ - clearTimeout(snapProcessTimeout) - fs.readFile(temporaryImageFile,function(err,buffer){ - if(buffer){ - if(options.useIcon === true){ - fs.writeFile(iconImageFile,buffer,function(){ - callback(buffer,false) + if(url){ + runExtraction() + }else{ + checkExists(streamDir + 's.jpg',function(success){ + if(success === false){ + checkExists(streamDir + 'detectorStream.m3u8',function(success){ + if(success === false){ + checkExists(streamDir + 's.m3u8',function(success){ + if(success === false){ + switch(monitor.type){ + case'h264': + switch(monitor.protocol){ + case'rtsp': + if( + monitor.details.rtsp_transport + && monitor.details.rtsp_transport !== '' + && monitor.details.rtsp_transport !== 'no' + ){ + inputOptions.push('-rtsp_transport ' + monitor.details.rtsp_transport) + } + break; + } + break; + } + url = s.buildMonitorUrl(monitor) + }else{ + outputOptions.push(`-ss 00:00:${secondsInward}`) + url = streamDir + 's.m3u8' + } + runExtraction() }) }else{ - callback(buffer,false) + outputOptions.push(`-ss 00:00:${secondsInward}`) + url = streamDir + 'detectorStream.m3u8' + runExtraction() } - }else{ - fs.readFile(config.defaultMjpeg,function(err,buffer){ - callback(buffer,false) + }) + }else{ + s.readFile(streamDir + 's.jpg',function(err,snapBuffer){ + resolve({ + screenShot: snapBuffer, + isStaticFile: true }) - } - fs.unlink(temporaryImageFile,function(){}) - }) - }) - var snapProcessTimeout = setTimeout(function(){ - snapProcess.stdin.setEncoding('utf8') - snapProcess.stdin.write('q') - snapProcess.kill() - },30000) - }catch(err){ - fs.readFile(config.defaultMjpeg,function(err,buffer){ - callback(buffer,false) + }) + } }) } } - if(url){ - runExtraction() - }else{ - checkExists(streamDir + 's.jpg',function(success){ + if(options.useIcon === true){ + checkExists(streamDir + 'icon.jpg',function(success){ if(success === false){ - checkExists(streamDir + 'detectorStream.m3u8',function(success){ - if(success === false){ - checkExists(streamDir + 's.m3u8',function(success){ - if(success === false){ - switch(monitor.type){ - case'h264': - switch(monitor.protocol){ - case'rtsp': - if( - monitor.details.rtsp_transport - && monitor.details.rtsp_transport !== '' - && monitor.details.rtsp_transport !== 'no' - ){ - inputOptions.push('-rtsp_transport ' + monitor.details.rtsp_transport) - } - break; - } - break; - } - url = s.buildMonitorUrl(monitor) - }else{ - outputOptions.push(`-ss 00:00:${secondsInward}`) - url = streamDir + 's.m3u8' - } - runExtraction() - }) - }else{ - outputOptions.push(`-ss 00:00:${secondsInward}`) - url = streamDir + 'detectorStream.m3u8' - runExtraction() - } - }) + noIconChecks() }else{ - s.readFile(streamDir + 's.jpg',function(err,snapBuffer){ - callback(snapBuffer,true) + var snapBuffer = fs.readFileSync(streamDir + 'icon.jpg') + resolve({ + screenShot: snapBuffer, + isStaticFile: false }) } }) + }else{ + noIconChecks() } - } - if(options.useIcon === true){ - checkExists(streamDir + 'icon.jpg',function(success){ - if(success === false){ - noIconChecks() - }else{ - s.readFile(streamDir + 'icon.jpg',function(err,snapBuffer){ - callback(snapBuffer,true) - }) - } - }) - }else{ - noIconChecks() - } + }) } s.mergeDetectorBufferChunks = function(monitor,callback){ var pathDir = s.dir.streams+monitor.ke+'/'+monitor.id+'/' @@ -347,72 +375,7 @@ module.exports = function(s,config,lang){ }) return items } - - s.cameraDestroy = function(x,e,p){ - if(s.group[e.ke]&&s.group[e.ke].activeMonitors[e.id]&&s.group[e.ke].activeMonitors[e.id].spawn !== undefined){ - if(s.group[e.ke].activeMonitors[e.id].spawn){ - s.group[e.ke].activeMonitors[e.id].allowStdinWrite = false - s.txToDashcamUsers({ - f : 'disable_stream', - ke : e.ke, - mid : e.id - },e.ke) - s.group[e.ke].activeMonitors[e.id].spawn.stdio[3].unpipe(); - // if(s.group[e.ke].activeMonitors[e.id].p2pStream){s.group[e.ke].activeMonitors[e.id].p2pStream.unpipe();} - if(s.group[e.ke].activeMonitors[e.id].p2p){s.group[e.ke].activeMonitors[e.id].p2p.unpipe();} - delete(s.group[e.ke].activeMonitors[e.id].p2pStream) - delete(s.group[e.ke].activeMonitors[e.id].p2p) - delete(s.group[e.ke].activeMonitors[e.id].pamDiff) - try{ - s.group[e.ke].activeMonitors[e.id].spawn.removeListener('end',s.group[e.ke].activeMonitors[e.id].spawn_exit); - s.group[e.ke].activeMonitors[e.id].spawn.removeListener('exit',s.group[e.ke].activeMonitors[e.id].spawn_exit); - delete(s.group[e.ke].activeMonitors[e.id].spawn_exit); - }catch(er){} - } - s.group[e.ke].activeMonitors[e.id].firstStreamChunk = {} - clearTimeout(s.group[e.ke].activeMonitors[e.id].recordingChecker); - delete(s.group[e.ke].activeMonitors[e.id].recordingChecker); - clearTimeout(s.group[e.ke].activeMonitors[e.id].streamChecker); - delete(s.group[e.ke].activeMonitors[e.id].streamChecker); - clearTimeout(s.group[e.ke].activeMonitors[e.id].checkSnap); - delete(s.group[e.ke].activeMonitors[e.id].checkSnap); - clearTimeout(s.group[e.ke].activeMonitors[e.id].watchdog_stop); - delete(s.group[e.ke].activeMonitors[e.id].watchdog_stop); - delete(s.group[e.ke].activeMonitors[e.id].lastJpegDetectorFrame); - delete(s.group[e.ke].activeMonitors[e.id].detectorFrameSaveBuffer); - clearTimeout(s.group[e.ke].activeMonitors[e.id].recordingSnapper); - clearInterval(s.group[e.ke].activeMonitors[e.id].getMonitorCpuUsage); - if(s.group[e.ke].activeMonitors[e.id].onChildNodeExit){ - s.group[e.ke].activeMonitors[e.id].onChildNodeExit() - } - if(s.group[e.ke].activeMonitors[e.id].mp4frag){ - var mp4FragChannels = Object.keys(s.group[e.ke].activeMonitors[e.id].mp4frag) - mp4FragChannels.forEach(function(channel){ - s.group[e.ke].activeMonitors[e.id].mp4frag[channel].removeAllListeners() - delete(s.group[e.ke].activeMonitors[e.id].mp4frag[channel]) - }) - } - if(config.childNodes.enabled === true && config.childNodes.mode === 'child' && config.childNodes.host){ - s.cx({f:'clearCameraFromActiveList',ke:e.ke,id:e.id}) - } - if(s.group[e.ke].activeMonitors[e.id].childNode){ - s.cx({f:'kill',d:s.cleanMonitorObject(e)},s.group[e.ke].activeMonitors[e.id].childNodeId) - }else{ - s.coSpawnClose(e) - if(!x||x===1){return}; - p=x.pid; - if(s.group[e.ke].rawMonitorConfigurations[e.id].type===('dashcam'||'socket'||'jpeg'||'pipe')){ - x.stdin.pause();setTimeout(function(){x.kill('SIGTERM');},500) - }else{ - try{ - x.stdin.setEncoding('utf8');x.stdin.write('q'); - }catch(er){} - } - setTimeout(function(){exec('kill -9 '+p,{detached: true})},1000) - } - } - } - s.cameraCheckObjectsInDetails = function(e){ + const checkObjectsInDetails = function(e){ //parse Objects (['detector_cascades','cords','detector_filters','input_map_choices']).forEach(function(v){ if(e.details && e.details[v]){ @@ -446,184 +409,7 @@ module.exports = function(s,config,lang){ } }); } - s.cameraControl = function(e,callback){ - s.checkDetails(e) - if(!s.group[e.ke]||!s.group[e.ke].activeMonitors[e.id]){return} - var monitorConfig = s.group[e.ke].rawMonitorConfigurations[e.id]; - if(monitorConfig.details.control!=="1"){s.userLog(e,{type:lang['Control Error'],msg:lang.ControlErrorText1});return} - if(!monitorConfig.details.control_base_url||monitorConfig.details.control_base_url===''){ - e.base = s.buildMonitorUrl(monitorConfig, true); - }else{ - e.base = monitorConfig.details.control_base_url; - } - if(!monitorConfig.details.control_url_stop_timeout || monitorConfig.details.control_url_stop_timeout === ''){ - monitorConfig.details.control_url_stop_timeout = 1000 - } - if(!monitorConfig.details.control_url_method||monitorConfig.details.control_url_method===''){monitorConfig.details.control_url_method="GET"} - var controlURL = e.base+monitorConfig.details['control_url_'+e.direction] - var controlURLOptions = s.cameraControlOptionsFromUrl(controlURL,monitorConfig) - if(monitorConfig.details.control_url_stop_timeout === '0' && monitorConfig.details.control_stop === '1' && s.group[e.ke].activeMonitors[e.id].ptzMoving === true){ - e.direction = 'stopMove' - s.group[e.ke].activeMonitors[e.id].ptzMoving = false - }else{ - s.group[e.ke].activeMonitors[e.id].ptzMoving = true - } - if(monitorConfig.details.control_url_method === 'ONVIF'){ - try{ - var move = function(device){ - var stopOptions = {ProfileToken : device.current_profile.token,'PanTilt': true,'Zoom': true} - switch(e.direction){ - case'center': -// device.services.ptz.gotoHomePosition() - msg = {type:'Center button inactive'} - s.userLog(e,msg) - callback(msg) - break; - case'stopMove': - msg = {type:'Control Trigger Ended'} - s.userLog(e,msg) - callback(msg) - device.services.ptz.stop(stopOptions).then((result) => { -// console.log(JSON.stringify(result['data'], null, ' ')); - }).catch((error) => { -// console.error(error); - }); - break; - default: - var controlOptions = { - ProfileToken : device.current_profile.token, - Velocity : {} - } - var onvifDirections = { - "left" : [-1.0,'x'], - "right" : [1.0,'x'], - "down" : [-1.0,'y'], - "up" : [1.0,'y'], - "zoom_in" : [1.0,'z'], - "zoom_out" : [-1.0,'z'] - } - var direction = onvifDirections[e.direction] - controlOptions.Velocity[direction[1]] = direction[0]; - (['x','y','z']).forEach(function(axis){ - if(!controlOptions.Velocity[axis]) - controlOptions.Velocity[axis] = 0 - }) - if(monitorConfig.details.control_stop=='1'){ - device.services.ptz.continuousMove(controlOptions).then(function(err){ - s.userLog(e,{type:'Control Trigger Started'}); - if(monitorConfig.details.control_url_stop_timeout !== '0'){ - setTimeout(function(){ - msg = {type:'Control Trigger Ended'} - s.userLog(e,msg) - callback(msg) - device.services.ptz.stop(stopOptions).then((result) => { -// console.log(JSON.stringify(result['data'], null, ' ')); - }).catch((error) => { - console.log(error); - }); - },monitorConfig.details.control_url_stop_timeout) - } - }).catch(function(err){ - console.log(err) - }); - }else{ - device.services.ptz.absoluteMove(controlOptions).then(function(err){ - msg = {type:'Control Triggered'} - s.userLog(e,msg); - callback(msg) - }).catch(function(err){ - console.log(err) - }); - } - break; - } - } - //create onvif connection - if(!s.group[e.ke].activeMonitors[e.id].onvifConnection || !s.group[e.ke].activeMonitors[e.id].onvifConnection.current_profile || !s.group[e.ke].activeMonitors[e.id].onvifConnection.current_profile.token){ - s.group[e.ke].activeMonitors[e.id].onvifConnection = new onvif.OnvifDevice({ - xaddr : 'http://' + controlURLOptions.host + ':' + controlURLOptions.port + '/onvif/device_service', - user : controlURLOptions.username, - pass : controlURLOptions.password - }) - s.group[e.ke].activeMonitors[e.id].onvifConnection.init().then((info) => { - move(s.group[e.ke].activeMonitors[e.id].onvifConnection) - }).catch(function(error){ - console.log(error) - s.userLog(e,{type:lang['Control Error'],msg:error}) - }) - }else{ - move(s.group[e.ke].activeMonitors[e.id].onvifConnection) - } - }catch(err){ - console.log(err) - msg = {type:lang['Control Error'],msg:{msg:lang.ControlErrorText2,error:err,options:controlURLOptions,direction:e.direction}} - s.userLog(e,msg) - callback(msg) - } - }else{ - var stopCamera = function(){ - var stopURL = e.base+monitorConfig.details['control_url_'+e.direction+'_stop'] - var options = s.cameraControlOptionsFromUrl(stopURL,monitorConfig) - var requestOptions = { - url : stopURL, - method : options.method, - auth : { - user : options.username, - pass : options.password - } - } - if(monitorConfig.details.control_digest_auth === '1'){ - requestOptions.sendImmediately = true - } - request(requestOptions,function(err,data){ - if(err){ - msg = {ok:false,type:'Control Error',msg:err} - }else{ - msg = {ok:true,type:'Control Trigger Ended'} - } - callback(msg) - s.userLog(e,msg); - }) - } - if(e.direction === 'stopMove'){ - stopCamera() - }else{ - var requestOptions = { - url: controlURL, - method: controlURLOptions.method, - auth: { - user: controlURLOptions.username, - pass: controlURLOptions.password - } - } - if(monitorConfig.details.control_digest_auth === '1'){ - requestOptions.sendImmediately = true - } - request(requestOptions,function(err,data){ - if(err){ - msg = {ok:false,type:'Control Error',msg:err}; - callback(msg) - s.userLog(e,msg); - return - } - if(monitorConfig.details.control_stop=='1'&&e.direction!=='center'){ - s.userLog(e,{type:'Control Triggered Started'}); - if(monitorConfig.details.control_url_stop_timeout > 0){ - setTimeout(function(){ - stopCamera() - },monitorConfig.details.control_url_stop_timeout) - } - }else{ - msg = {ok:true,type:'Control Triggered'}; - callback(msg) - s.userLog(e,msg); - } - }) - } - } - } s.cameraControlOptionsFromUrl = function(e,monitorConfig){ - s.checkDetails(e) URLobject = URL.parse(e) if(monitorConfig.details.control_url_method === 'ONVIF' && monitorConfig.details.control_base_url === ''){ if(monitorConfig.details.onvif_port === ''){ @@ -654,27 +440,27 @@ module.exports = function(s,config,lang){ } return options } - s.cameraSendSnapshot = function(e,options){ - if(!options)options = {} + s.cameraSendSnapshot = async (e,options) => { + options = Object.assign({ + flags: '-s 200x200' + },options || {}) s.checkDetails(e) - if(config.doSnapshot === true){ - if(e.mon.mode !== 'stop'){ + if(e.ke && config.doSnapshot === true){ + if(s.group[e.ke] && s.group[e.ke].rawMonitorConfigurations && s.group[e.ke].rawMonitorConfigurations[e.mid] && s.group[e.ke].rawMonitorConfigurations[e.mid].mode !== 'stop'){ var pathDir = s.dir.streams+e.ke+'/'+e.mid+'/' - s.getRawSnapshotFromMonitor(e.mon,Object.assign({ - flags: '-s 200x200' - },options),function(data,isStaticFile){ - if(data && (data[data.length-2] === 0xFF && data[data.length-1] === 0xD9)){ - s.tx({ - f: 'monitor_snapshot', - snapshot: data.toString('base64'), - snapshot_format: 'b64', - mid: e.mid, - ke: e.ke - },'GRP_'+e.ke) - }else{ - s.tx({f:'monitor_snapshot',snapshot:e.mon.name,snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke) - } - }) + const {screenShot, isStaticFile} = await s.getRawSnapshotFromMonitor(s.group[e.ke].rawMonitorConfigurations[e.mid],options) + if(screenShot && (screenShot[screenShot.length-2] === 0xFF && screenShot[screenShot.length-1] === 0xD9)){ + s.tx({ + f: 'monitor_snapshot', + snapshot: screenShot.toString('base64'), + snapshot_format: 'b64', + mid: e.mid, + ke: e.ke + },'GRP_'+e.ke) + }else{ + s.debugLog('Damaged Snapshot Data') + s.tx({f:'monitor_snapshot',snapshot:e.mon.name,snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke) + } }else{ s.tx({f:'monitor_snapshot',snapshot:'Disabled',snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke) } @@ -682,7 +468,30 @@ module.exports = function(s,config,lang){ s.tx({f:'monitor_snapshot',snapshot:e.mon.name,snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke) } } - var createRecordingDirectory = function(e,callback){ + s.getCameraSnapshot = async (e,options) => { + const getDefaultImage = async () => { + return await fs.promises.readFile(config.defaultMjpeg) + } + options = Object.assign({ + flags: '-s 200x200' + },options || {}) + if(e.ke && config.doSnapshot === true){ + if(s.group[e.ke] && s.group[e.ke].rawMonitorConfigurations && s.group[e.ke].rawMonitorConfigurations[e.mid] && s.group[e.ke].rawMonitorConfigurations[e.mid].mode !== 'stop'){ + var pathDir = s.dir.streams+e.ke+'/'+e.mid+'/' + const {screenShot, isStaticFile} = await s.getRawSnapshotFromMonitor(s.group[e.ke].rawMonitorConfigurations[e.mid],options) + if(screenShot && (screenShot[screenShot.length-2] === 0xFF && screenShot[screenShot.length-1] === 0xD9)){ + return screenShot + }else{ + return await getDefaultImage() + } + }else{ + return await getDefaultImage() + } + }else{ + return await getDefaultImage() + } + } + const createRecordingDirectory = function(e,callback){ var directory if(e.details && e.details.dir && e.details.dir !== '' && config.childNodes.mode !== 'child'){ //addStorage choice @@ -708,14 +517,14 @@ module.exports = function(s,config,lang){ }) } } - var createTimelapseDirectory = function(e,callback){ + const createTimelapseDirectory = function(e,callback){ var directory = s.getTimelapseFrameDirectory(e) fs.mkdir(directory,function(err){ s.handleFolderError(err) callback(err,directory) }) } - var createFileBinDirectory = function(e,callback){ + const createFileBinDirectory = function(e,callback){ var directory = s.dir.fileBin + e.ke + '/' fs.mkdir(directory,function(err){ s.handleFolderError(err) @@ -726,7 +535,7 @@ module.exports = function(s,config,lang){ }) }) } - var createStreamDirectory = function(e,callback){ + const createStreamDirectory = function(e,callback){ callback = callback || function(){} var directory = s.dir.streams + e.ke + '/' fs.mkdir(directory,function(err){ @@ -744,13 +553,17 @@ module.exports = function(s,config,lang){ }) }) } - var createCameraFolders = function(e,callback){ + const createCameraFolders = function(e,callback){ //set the recording directory + var activeMonitor = s.group[e.ke].activeMonitors[e.id] createStreamDirectory(e,function(err,directory){ + activeMonitor.sdir = directory e.sdir = directory createRecordingDirectory(e,function(err,directory){ + activeMonitor.dir = directory e.dir = directory createTimelapseDirectory(e,function(err,directory){ + activeMonitor.dirTimelapse = directory e.dirTimelapse = directory createFileBinDirectory(e,function(err){ if(callback)callback() @@ -759,6 +572,23 @@ module.exports = function(s,config,lang){ }) }) } + const forceMonitorRestart = (monitor,restartMessage) => { + const monitorConfig = Object.assign(s.group[monitor.ke].rawMonitorConfigurations[monitor.mid],{}) + s.sendMonitorStatus({ + id: monitor.mid, + ke: monitor.ke, + status: lang.Restarting + }) + launchMonitorProcesses(monitorConfig) + s.userLog({ + ke: monitor.ke, + mid: monitor.mid, + },restartMessage) + s.orphanedVideoCheck({ + ke: monitor.ke, + mid: monitor.mid, + },2,null,true) + } s.stripAuthFromHost = function(e){ var host = e.host.split('@'); if(host[1]){ @@ -770,7 +600,7 @@ module.exports = function(s,config,lang){ } return host } - s.resetRecordingCheck = function(e){ + const resetRecordingCheck = function(e){ clearTimeout(s.group[e.ke].activeMonitors[e.id].recordingChecker) var cutoff = e.cutoff + 0 if(e.type === 'dashcam'){ @@ -778,129 +608,57 @@ module.exports = function(s,config,lang){ } s.group[e.ke].activeMonitors[e.id].recordingChecker = setTimeout(function(){ if(s.group[e.ke].activeMonitors[e.id].isStarted === true && s.group[e.ke].rawMonitorConfigurations[e.id].mode === 'record'){ - s.launchMonitorProcesses(s.cleanMonitorObject(e)); - s.sendMonitorStatus({id:e.id,ke:e.ke,status:lang.Restarting}); - s.userLog(e,{type:lang['Camera is not recording'],msg:{msg:lang['Restarting Process']}}); - s.orphanedVideoCheck(e,2,null,true) + forceMonitorRestart({ + ke: e.ke, + mid: e.id, + },{ + type: lang['Camera is not recording'], + msg: { + msg: lang['Restarting Process'] + } + }) } },60000 * cutoff * 1.3); } - s.resetStreamCheck = function(e){ + const resetStreamCheck = function(e){ clearTimeout(s.group[e.ke].activeMonitors[e.id].streamChecker) s.group[e.ke].activeMonitors[e.id].streamChecker = setTimeout(function(){ if(s.group[e.ke].activeMonitors[e.id] && s.group[e.ke].activeMonitors[e.id].isStarted === true){ - s.launchMonitorProcesses(s.cleanMonitorObject(e)); - s.userLog(e,{type:lang['Camera is not streaming'],msg:{msg:lang['Restarting Process']}}); - s.orphanedVideoCheck(e,2,null,true) + forceMonitorRestart({ + ke: e.ke, + mid: e.id, + },{ + type: lang['Camera is not streaming'], + msg: { + msg: lang['Restarting Process'] + } + }) } },60000*1); } - s.cameraPullJpegStream = function(e){ - if(!e.details.sfps||e.details.sfps===''){ - e.details.sfps = 1 - } - var capture_fps = parseFloat(e.details.sfps); - if(isNaN(capture_fps)){capture_fps = 1} - if(s.group[e.ke].activeMonitors[e.id].spawn){ - s.group[e.ke].activeMonitors[e.id].spawn.stdin.on('error',function(err){ - if(err&&e.details.loglevel!=='quiet'){ - s.userLog(e,{type:'STDIN ERROR',msg:err}); - } + const onDetectorJpegOutputAlone = (e,d) => { + if(s.isAtleatOneDetectorPluginConnected){ + s.ocvTx({ + f: 'frame', + mon: s.group[e.ke].rawMonitorConfigurations[e.id].details, + ke: e.ke, + id: e.id, + time: s.formattedTime(), + frame: d }) - }else{ - if(e.functionMode === 'record'){ - s.userLog(e,{type:lang.FFmpegCantStart,msg:lang.FFmpegCantStartText}); - return + } + } + const onDetectorJpegOutputSecondary = (e,buffer) => { + if(s.isAtleatOneDetectorPluginConnected){ + const theArray = s.group[e.ke].activeMonitors[e.id].pipe4BufferPieces + theArray.push(buffer) + if(buffer[buffer.length-2] === 0xFF && buffer[buffer.length-1] === 0xD9){ + s.group[e.ke].activeMonitors[e.id].lastJpegDetectorFrame = Buffer.concat(theArray) + s.group[e.ke].activeMonitors[e.id].pipe4BufferPieces = [] } } - e.captureOne = function(f){ - s.group[e.ke].activeMonitors[e.id].recordingSnapRequest = request({ - url: e.url, - method: 'GET', - encoding: null, - timeout: 15000 - },function(err,data){ - if(err){ - return; - } - }).on('data',function(d){ - if(!e.buffer0){ - e.buffer0 = [d] - }else{ - e.buffer0.push(d) - } - if((d[d.length-2] === 0xFF && d[d.length-1] === 0xD9)){ - e.buffer0 = Buffer.concat(e.buffer0); - if(s.group[e.ke].activeMonitors[e.id].spawn&&s.group[e.ke].activeMonitors[e.id].spawn.stdin){ - s.group[e.ke].activeMonitors[e.id].spawn.stdin.write(e.buffer0); - } - if(s.group[e.ke].activeMonitors[e.id].isStarted === true){ - s.group[e.ke].activeMonitors[e.id].recordingSnapper = setTimeout(function(){ - e.captureOne() - },1000/capture_fps) - } - e.buffer0 = null - } - if(!e.timeOut){ - e.timeOut = setTimeout(function(){ - e.errorCount = 0; - delete(e.timeOut) - },3000) - } - }).on('error', function(err){ - ++e.errorCount - clearTimeout(e.timeOut) - delete(e.timeOut) - if(e.details.loglevel !== 'quiet'){ - s.userLog(e,{ - type: lang['JPEG Error'], - msg: { - msg: lang.JPEGErrorText, - info: err - } - }); - switch(err.code){ - case'ESOCKETTIMEDOUT': - case'ETIMEDOUT': - ++s.group[e.ke].activeMonitors[e.id].errorSocketTimeoutCount - if( - e.details.fatal_max !== 0 && - s.group[e.ke].activeMonitors[e.id].errorSocketTimeoutCount > e.details.fatal_max - ){ - s.userLog(e,{type:lang['Fatal Maximum Reached'],msg:{code:'ESOCKETTIMEDOUT',msg:lang.FatalMaximumReachedText}}); - s.camera('stop',e) - }else{ - s.userLog(e,{type:lang['Restarting Process'],msg:{code:'ESOCKETTIMEDOUT',msg:lang.FatalMaximumReachedText}}); - s.camera('restart',e) - } - return; - break; - } - } - if(e.details.fatal_max !== 0 && e.errorCount > e.details.fatal_max){ - clearTimeout(s.group[e.ke].activeMonitors[e.id].recordingSnapper) - s.launchMonitorProcesses(s.cleanMonitorObject(e)) - } - }) - } - e.captureOne() } - var onDetectorJpegOutputAlone = function(e,d){ - s.ocvTx({ - f: 'frame', - mon: s.group[e.ke].rawMonitorConfigurations[e.id].details, - ke: e.ke, - id: e.id, - time: s.formattedTime(), - frame: d - }) - } - var onDetectorJpegOutputSecondary = function(e,d){ - s.group[e.ke].activeMonitors[e.id].lastJpegDetectorFrame = d - } - s.onMonitorDetectorDataOutputAlone = onDetectorJpegOutputAlone - s.onMonitorDetectorDataOutputSecondary = onDetectorJpegOutputSecondary - s.createCameraFfmpegProcess = function(e){ + const createCameraFfmpegProcess = (e) => { //launch ffmpeg (main) s.tx({ f: 'monitor_starting', @@ -908,7 +666,14 @@ module.exports = function(s,config,lang){ mid: e.id, time: s.formattedTime() },'GRP_'+e.ke) - s.group[e.ke].activeMonitors[e.id].spawn = s.ffmpeg(e) + try{ + s.group[e.ke].activeMonitors[e.id].spawn = s.ffmpeg(e) + }catch(err){ + console.log('failed to launch, try again') + setTimeout(() => { + s.group[e.ke].activeMonitors[e.id].spawn = s.ffmpeg(e) + },3000) + } s.sendMonitorStatus({id:e.id,ke:e.ke,status:e.wantedStatus}); //on unexpected exit restart s.group[e.ke].activeMonitors[e.id].spawn_exit = function(){ @@ -916,7 +681,7 @@ module.exports = function(s,config,lang){ if(e.details.loglevel!=='quiet'){ s.userLog(e,{type:lang['Process Unexpected Exit'],msg:{msg:lang['Process Crashed for Monitor'],cmd:s.group[e.ke].activeMonitors[e.id].ffmpeg}}); } - s.fatalCameraError(e,'Process Unexpected Exit'); + fatalError(e,'Process Unexpected Exit'); s.orphanedVideoCheck(e,2,null,true) s.onMonitorUnexpectedExitExtensions.forEach(function(extender){ extender(Object.assign(s.group[e.ke].rawMonitorConfigurations[e.id],{}),e) @@ -926,7 +691,7 @@ module.exports = function(s,config,lang){ s.group[e.ke].activeMonitors[e.id].spawn.on('end',s.group[e.ke].activeMonitors[e.id].spawn_exit) s.group[e.ke].activeMonitors[e.id].spawn.on('exit',s.group[e.ke].activeMonitors[e.id].spawn_exit) s.group[e.ke].activeMonitors[e.id].spawn.on('error',function(er){ - s.userLog(e,{type:'Spawn Error',msg:er});s.fatalCameraError(e,'Spawn Error') + s.userLog(e,{type:'Spawn Error',msg:er});fatalError(e,'Spawn Error') }) s.userLog(e,{type:lang['Process Started'],msg:{cmd:s.group[e.ke].activeMonitors[e.id].ffmpeg}}) if(s.isWin === false){ @@ -942,13 +707,14 @@ module.exports = function(s,config,lang){ },'MON_STREAM_'+e.ke+e.id) }) } + clearInterval(s.group[e.ke].activeMonitors[e.id].getMonitorCpuUsage) s.group[e.ke].activeMonitors[e.id].getMonitorCpuUsage = setInterval(function(){ if(e.details.skip_ping !== '1'){ connectionTester.test(strippedHost,e.port,2000,function(err,response){ if(response.success){ sendProcessCpuUsage() }else{ - s.launchMonitorProcesses(e) + launchMonitorProcesses(e) } }) }else{ @@ -957,17 +723,58 @@ module.exports = function(s,config,lang){ },1000 * 60) } } - s.createCameraStreamHandlers = function(e){ + const createEventCounter = function(monitor){ + if(monitor.details.detector_obj_count === '1'){ + const activeMonitor = s.group[monitor.ke].activeMonitors[monitor.id] + activeMonitor.eventsCountStartTime = new Date() + clearInterval(activeMonitor.objectCountIntervals) + activeMonitor.objectCountIntervals = setInterval(() => { + const eventsCounted = activeMonitor.eventsCounted || {} + const countsToSave = Object.assign(eventsCounted,{}) + activeMonitor.eventsCounted = {} + const groupKey = monitor.ke + const monitorId = monitor.id + const startTime = new Date(activeMonitor.eventsCountStartTime + 0) + const endTime = new Date() + const countedKeys = Object.keys(countsToSave) + activeMonitor.eventsCountStartTime = new Date() + if(countedKeys.length > 0)countedKeys.forEach((tag) => { + const tagInfo = countsToSave[tag] + const count = Object.keys(tagInfo.count) + const times = tagInfo.times + const realTag = tagInfo.tag + s.knexQuery({ + action: "insert", + table: "Events Counts", + insert: { + ke: groupKey, + mid: monitorId, + details: JSON.stringify({ + times: times, + count: count, + }), + time: startTime, + end: endTime, + count: count.length, + tag: realTag + } + }) + }) + },60000) //every minute + } + } + const createCameraStreamHandlers = function(e){ s.group[e.ke].activeMonitors[e.id].spawn.stdio[5].on('data',function(data){ - s.resetStreamCheck(e) + resetStreamCheck(e) }) //emitter for mjpeg if(!e.details.stream_mjpeg_clients||e.details.stream_mjpeg_clients===''||isNaN(e.details.stream_mjpeg_clients)===false){e.details.stream_mjpeg_clients=20;}else{e.details.stream_mjpeg_clients=parseInt(e.details.stream_mjpeg_clients)} s.group[e.ke].activeMonitors[e.id].emitter = new events.EventEmitter().setMaxListeners(e.details.stream_mjpeg_clients); - if(e.type==='jpeg'){ - s.cameraPullJpegStream(e) - } if(e.details.detector_audio === '1'){ + if(s.group[e.ke].activeMonitors[e.id].audioDetector){ + s.group[e.ke].activeMonitors[e.id].audioDetector.stop() + delete(s.group[e.ke].activeMonitors[e.id].audioDetector) + } var triggerLevel var triggerLevelMax if(e.details.detector_audio_min_db && e.details.detector_audio_min_db !== ''){ @@ -1005,15 +812,16 @@ module.exports = function(s,config,lang){ }) s.group[e.ke].activeMonitors[e.id].audioDetector = audioDetector audioDetector.start() - s.group[e.ke].activeMonitors[e.id].spawn.stdio[6].pipe(audioDetector.streamDecoder) + s.group[e.ke].activeMonitors[e.id].spawn.stdio[6].pipe(audioDetector.streamDecoder,{ end: false }) } if(e.details.record_timelapse === '1'){ + var timelapseRecordingDirectory = s.getTimelapseFrameDirectory(e) s.group[e.ke].activeMonitors[e.id].spawn.stdio[7].on('data',function(data){ var fileStream = s.group[e.ke].activeMonitors[e.id].recordTimelapseWriter if(!fileStream){ var currentDate = s.formattedTime(null,'YYYY-MM-DD') var filename = s.formattedTime() + '.jpg' - var location = s.getTimelapseFrameDirectory(e) + currentDate + '/' + var location = timelapseRecordingDirectory + currentDate + '/' if(!fs.existsSync(location)){ fs.mkdirSync(location) } @@ -1035,54 +843,73 @@ module.exports = function(s,config,lang){ s.ocvTx({f:'init_monitor',id:e.id,ke:e.ke}) //frames from motion detect if(e.details.detector_pam === '1'){ - s.createPamDiffEngine(e) - s.group[e.ke].activeMonitors[e.id].spawn.stdio[3].pipe(s.group[e.ke].activeMonitors[e.id].p2p).pipe(s.group[e.ke].activeMonitors[e.id].pamDiff) + // s.group[e.ke].activeMonitors[e.id].spawn.stdio[3].pipe(s.group[e.ke].activeMonitors[e.id].p2p).pipe(s.group[e.ke].activeMonitors[e.id].pamDiff) + s.group[e.ke].activeMonitors[e.id].spawn.stdio[3].on('data',function(buf){ + try{ + buf.toString().split('}{').forEach((object,n)=>{ + var theJson = object + if(object.substr(object.length - 1) !== '}')theJson += '}' + if(object.substr(0,1) !== '{')theJson = '{' + theJson + var data = JSON.parse(theJson) + s.triggerEvent(data) + }) + }catch(err){ + console.log('There was an error parsing a detector event') + console.log(err) + } + }) if(e.details.detector_use_detect_object === '1'){ s.group[e.ke].activeMonitors[e.id].spawn.stdio[4].on('data',function(data){ - s.onMonitorDetectorDataOutputSecondary(e,data) + onDetectorJpegOutputSecondary(e,data) }) } - }else if(s.isAtleatOneDetectorPluginConnected){ - s.group[e.ke].activeMonitors[e.id].spawn.stdio[3].on('data',function(data){ - s.onMonitorDetectorDataOutputAlone(e,data) + }else if(e.details.detector_use_detect_object === '1' && e.details.detector_send_frames !== '1'){ + s.group[e.ke].activeMonitors[e.id].spawn.stdio[4].on('data',function(data){ + onDetectorJpegOutputSecondary(e,data) + }) + }else{ + s.group[e.ke].activeMonitors[e.id].spawn.stdio[4].on('data',function(data){ + onDetectorJpegOutputAlone(e,data) }) } } //frames to stream + var frameToStreamPrimary switch(e.details.stream_type){ case'mp4': - s.group[e.ke].activeMonitors[e.id].mp4frag['MAIN'] = new Mp4Frag() + delete(s.group[e.ke].activeMonitors[e.id].mp4frag['MAIN']) + if(!s.group[e.ke].activeMonitors[e.id].mp4frag['MAIN'])s.group[e.ke].activeMonitors[e.id].mp4frag['MAIN'] = new Mp4Frag() s.group[e.ke].activeMonitors[e.id].mp4frag['MAIN'].on('error',function(error){ s.userLog(e,{type:lang['Mp4Frag'],msg:{error:error}}) }) - s.group[e.ke].activeMonitors[e.id].spawn.stdio[1].pipe(s.group[e.ke].activeMonitors[e.id].mp4frag['MAIN']) + s.group[e.ke].activeMonitors[e.id].spawn.stdio[1].pipe(s.group[e.ke].activeMonitors[e.id].mp4frag['MAIN'],{ end: false }) break; case'flv': - e.frameToStream = function(d){ + frameToStreamPrimary = function(d){ if(!s.group[e.ke].activeMonitors[e.id].firstStreamChunk['MAIN'])s.group[e.ke].activeMonitors[e.id].firstStreamChunk['MAIN'] = d; - e.frameToStream = function(d){ - s.resetStreamCheck(e) + frameToStreamPrimary = function(d){ + resetStreamCheck(e) s.group[e.ke].activeMonitors[e.id].emitter.emit('data',d) } - e.frameToStream(d) + frameToStreamPrimary(d) } break; case'mjpeg': - e.frameToStream = function(d){ - s.resetStreamCheck(e) + frameToStreamPrimary = function(d){ + resetStreamCheck(e) s.group[e.ke].activeMonitors[e.id].emitter.emit('data',d) } break; case'h265': - e.frameToStream = function(d){ - s.resetStreamCheck(e) + frameToStreamPrimary = function(d){ + resetStreamCheck(e) s.group[e.ke].activeMonitors[e.id].emitter.emit('data',d) } break; case'b64':case undefined:case null:case'': var buffer - e.frameToStream = function(d){ - s.resetStreamCheck(e) + frameToStreamPrimary = function(d){ + resetStreamCheck(e) if(!buffer){ buffer=[d] }else{ @@ -1095,11 +922,11 @@ module.exports = function(s,config,lang){ } break; } - if(e.frameToStream){ + if(frameToStreamPrimary){ if(e.coProcessor === true && e.details.stream_type === ('b64'||'mjpeg')){ }else{ - s.group[e.ke].activeMonitors[e.id].spawn.stdout.on('data',e.frameToStream) + s.group[e.ke].activeMonitors[e.id].spawn.stdout.on('data',frameToStreamPrimary) } } if(e.details.stream_channels && e.details.stream_channels !== ''){ @@ -1108,47 +935,85 @@ module.exports = function(s,config,lang){ if(!s.group[e.ke].activeMonitors[e.id].emitterChannel[pipeNumber]){ s.group[e.ke].activeMonitors[e.id].emitterChannel[pipeNumber] = new events.EventEmitter().setMaxListeners(0); } - var frameToStream + var frameToStreamAdded switch(channel.stream_type){ case'mp4': - s.group[e.ke].activeMonitors[e.id].mp4frag[pipeNumber] = new Mp4Frag(); - s.group[e.ke].activeMonitors[e.id].spawn.stdio[pipeNumber].pipe(s.group[e.ke].activeMonitors[e.id].mp4frag[pipeNumber]) + delete(s.group[e.ke].activeMonitors[e.id].mp4frag[pipeNumber]) + if(!s.group[e.ke].activeMonitors[e.id].mp4frag[pipeNumber])s.group[e.ke].activeMonitors[e.id].mp4frag[pipeNumber] = new Mp4Frag(); + s.group[e.ke].activeMonitors[e.id].spawn.stdio[pipeNumber].pipe(s.group[e.ke].activeMonitors[e.id].mp4frag[pipeNumber],{ end: false }) break; case'mjpeg': - frameToStream = function(d){ + frameToStreamAdded = function(d){ s.group[e.ke].activeMonitors[e.id].emitterChannel[pipeNumber].emit('data',d) } break; case'flv': - frameToStream = function(d){ + frameToStreamAdded = function(d){ if(!s.group[e.ke].activeMonitors[e.id].firstStreamChunk[pipeNumber])s.group[e.ke].activeMonitors[e.id].firstStreamChunk[pipeNumber] = d; - frameToStream = function(d){ + frameToStreamAdded = function(d){ s.group[e.ke].activeMonitors[e.id].emitterChannel[pipeNumber].emit('data',d) } - frameToStream(d) + frameToStreamAdded(d) } break; case'h264': - frameToStream = function(d){ + frameToStreamAdded = function(d){ s.group[e.ke].activeMonitors[e.id].emitterChannel[pipeNumber].emit('data',d) } break; } - if(frameToStream){ - s.group[e.ke].activeMonitors[e.id].spawn.stdio[pipeNumber].on('data',frameToStream) + if(frameToStreamAdded){ + s.group[e.ke].activeMonitors[e.id].spawn.stdio[pipeNumber].on('data',frameToStreamAdded) } } e.details.stream_channels.forEach(createStreamEmitter) } } - s.cameraFilterFfmpegLog = function(e){ + const catchNewSegmentNames = function(e){ + var checkLog = function(d,x){return d.indexOf(x)>-1} + s.group[e.ke].activeMonitors[e.id].spawn.stdio[8].on('data',function(d){ + d=d.toString(); + if(/T[0-9][0-9]-[0-9][0-9]-[0-9][0-9]./.test(d)){ + var filename = d.split('.')[0].split(' [')[0].trim()+'.'+e.ext + s.insertCompletedVideo(e,{ + file: filename, + events: s.group[e.ke].activeMonitors[e.id].detector_motion_count + },function(err){ + s.userLog(e,{type:lang['Video Finished'],msg:{filename:d}}) + if( + e.details.detector === '1' && + s.group[e.ke].activeMonitors[e.id].isStarted === true && + e.details && + e.details.detector_record_method === 'del'&& + e.details.detector_delete_motionless_videos === '1'&& + s.group[e.ke].activeMonitors[e.id].detector_motion_count.length === 0 + ){ + if(e.details.loglevel !== 'quiet'){ + s.userLog(e,{type:lang['Delete Motionless Video'],msg:filename}) + } + s.deleteVideo({ + filename : filename, + ke : e.ke, + id : e.id + }) + } + s.group[e.ke].activeMonitors[e.id].detector_motion_count = [] + }) + resetRecordingCheck(e) + } + }) + } + const cameraFilterFfmpegLog = function(e){ var checkLog = function(d,x){return d.indexOf(x)>-1} s.group[e.ke].activeMonitors[e.id].spawn.stderr.on('data',function(d){ d=d.toString(); switch(true){ case checkLog(d,'No space left on device'): s.checkUserPurgeLock(e.ke) - s.purgeDiskForGroup(e) + s.purgeDiskForGroup(e.ke) + break; + case checkLog(d,'error parsing AU headers'): + s.userLog(e,{type:lang['Error While Decoding'],msg:lang.ErrorWhileDecodingTextAudio}); break; case checkLog(d,'error while decoding'): s.userLog(e,{type:lang['Error While Decoding'],msg:lang.ErrorWhileDecodingText}); @@ -1173,43 +1038,15 @@ module.exports = function(s,config,lang){ //restart setTimeout(function(){ s.userLog(e,{type:lang['Connection timed out'],msg:lang['Retrying...']}); - s.fatalCameraError(e,'Connection timed out'); + fatalError(e,'Connection timed out'); },1000) break; - case checkLog(d,'Immediate exit requested'): + // case checkLog(d,'Immediate exit requested'): case checkLog(d,'mjpeg_decode_dc'): case checkLog(d,'bad vlc'): case checkLog(d,'error dc'): case checkLog(d,'No route to host'): - s.launchMonitorProcesses(e) - break; - case /T[0-9][0-9]-[0-9][0-9]-[0-9][0-9]./.test(d): - var filename = d.split('.')[0].split(' [')[0].trim()+'.'+e.ext - s.insertCompletedVideo(e,{ - file : filename - },function(err){ - s.userLog(e,{type:lang['Video Finished'],msg:{filename:d}}) - if( - e.details.detector === '1' && - s.group[e.ke].activeMonitors[e.id].isStarted === true && - e.details && - e.details.detector_record_method === 'del'&& - e.details.detector_delete_motionless_videos === '1'&& - s.group[e.ke].activeMonitors[e.id].detector_motion_count === 0 - ){ - if(e.details.loglevel !== 'quiet'){ - s.userLog(e,{type:lang['Delete Motionless Video'],msg:filename}) - } - s.deleteVideo({ - filename : filename, - ke : e.ke, - id : e.id - }) - } - s.group[e.ke].activeMonitors[e.id].detector_motion_count = 0 - }) - s.resetRecordingCheck(e) - return; + launchMonitorProcesses(e) break; } s.userLog(e,{type:"FFMPEG STDERR",msg:d}) @@ -1247,22 +1084,23 @@ module.exports = function(s,config,lang){ },detector_notrigger_timeout) } //set master based process launcher - s.launchMonitorProcesses = function(e){ + const launchMonitorProcesses = function(e){ + const activeMonitor = s.group[e.ke].activeMonitors[e.id] // e = monitor object + clearTimeout(activeMonitor.resetFatalErrorCountTimer) + activeMonitor.resetFatalErrorCountTimer = setTimeout(()=>{ + activeMonitor.errorFatalCount = 0 + },1000 * 60) //create host string without username and password var strippedHost = s.stripAuthFromHost(e) var doOnThisMachine = function(callback){ createCameraFolders(e,function(){ - s.group[e.ke].activeMonitors[e.id].allowStdinWrite = false - s.txToDashcamUsers({ - f : 'disable_stream', - ke : e.ke, - mid : e.id - },e.ke) + activeMonitor.allowStdinWrite = false if(e.details.detector_trigger === '1'){ - s.group[e.ke].activeMonitors[e.id].motion_lock = setTimeout(function(){ - clearTimeout(s.group[e.ke].activeMonitors[e.id].motion_lock) - delete(s.group[e.ke].activeMonitors[e.id].motion_lock) + clearTimeout(activeMonitor.motion_lock) + activeMonitor.motion_lock = setTimeout(function(){ + clearTimeout(activeMonitor.motion_lock) + delete(activeMonitor.motion_lock) },15000) } //start "no motion" checker @@ -1271,15 +1109,15 @@ module.exports = function(s,config,lang){ } if(e.details.snap === '1'){ var resetSnapCheck = function(){ - clearTimeout(s.group[e.ke].activeMonitors[e.id].checkSnap) - s.group[e.ke].activeMonitors[e.id].checkSnap = setTimeout(function(){ - if(s.group[e.ke].activeMonitors[e.id].isStarted === true){ + clearTimeout(activeMonitor.checkSnap) + activeMonitor.checkSnap = setTimeout(function(){ + if(activeMonitor.isStarted === true){ s.fileStats(e.sdir+'s.jpg',function(err,snap){ var notStreaming = function(){ if(e.coProcessor === true){ s.coSpawnLauncher(e) }else{ - s.launchMonitorProcesses(e) + launchMonitorProcesses(e) } s.userLog(e,{type:lang['Camera is not streaming'],msg:{msg:lang['Restarting Process']}}) s.orphanedVideoCheck(e,2,null,true) @@ -1302,11 +1140,13 @@ module.exports = function(s,config,lang){ resetSnapCheck() } if(config.childNodes.mode !== 'child' && s.platform!=='darwin' && (e.functionMode === 'record' || (e.functionMode === 'start'&&e.details.detector_record_method==='sip'))){ - //check if ffmpeg is recording - s.group[e.ke].activeMonitors[e.id].fswatch = fs.watch(e.dir, {encoding : 'utf8'}, (event, filename) => { + if(activeMonitor.fswatch && activeMonitor.fswatch.close){ + activeMonitor.fswatch.close() + } + activeMonitor.fswatch = fs.watch(e.dir, {encoding : 'utf8'}, (event, filename) => { switch(event){ case'change': - s.resetRecordingCheck(e) + resetRecordingCheck(e) break; } }); @@ -1323,54 +1163,66 @@ module.exports = function(s,config,lang){ e.details.snap === '1' ) ){ - s.group[e.ke].activeMonitors[e.id].fswatchStream = fs.watch(e.sdir, {encoding : 'utf8'}, () => { - s.resetStreamCheck(e) + if(activeMonitor.fswatchStream && activeMonitor.fswatchStream.close){ + activeMonitor.fswatchStream.close() + } + activeMonitor.fswatchStream = fs.watch(activeMonitor.sdir, {encoding : 'utf8'}, () => { + resetStreamCheck(e) }) } s.cameraSendSnapshot({mid:e.id,ke:e.ke,mon:e},{useIcon: true}) //check host to see if has password and user in it - clearTimeout(s.group[e.ke].activeMonitors[e.id].recordingChecker) - if(s.group[e.ke].activeMonitors[e.id].isStarted === true){ - e.errorCount = 0; - s.group[e.ke].activeMonitors[e.id].errorSocketTimeoutCount = 0; - s.cameraDestroy(s.group[e.ke].activeMonitors[e.id].spawn,e) + clearTimeout(activeMonitor.recordingChecker) + if(activeMonitor.isStarted === true){ + try{ + cameraDestroy(e) + }catch(err){ + + } startVideoProcessor = function(err,o){ if(o.success === true){ - s.group[e.ke].activeMonitors[e.id].isRecording = true - s.createCameraFfmpegProcess(e) - s.createCameraStreamHandlers(e) - if(e.type === 'dashcam'){ - setTimeout(function(){ - s.group[e.ke].activeMonitors[e.id].allowStdinWrite = true - s.txToDashcamUsers({ - f : 'enable_stream', - ke : e.ke, - mid : e.id - },e.ke) - },30000) + activeMonitor.isRecording = true + try{ + createCameraFfmpegProcess(e) + createCameraStreamHandlers(e) + createEventCounter(e) + if(e.type === 'dashcam' || e.type === 'socket'){ + setTimeout(function(){ + activeMonitor.allowStdinWrite = true + s.txToDashcamUsers({ + f : 'enable_stream', + ke : e.ke, + mid : e.id + },e.ke) + },30000) + } + if( + e.functionMode === 'record' || + e.type === 'mjpeg' || + e.type === 'h264' || + e.type === 'local' + ){ + catchNewSegmentNames(e) + cameraFilterFfmpegLog(e) + } + if(e.coProcessor === true){ + setTimeout(function(){ + s.coSpawnLauncher(e) + },6000) + } + s.onMonitorStartExtensions.forEach(function(extender){ + extender(Object.assign(s.group[e.ke].rawMonitorConfigurations[e.id],{}),e) + }) + }catch(err){ + console.log('Failed to Load',e.id,e.ke) + console.log(err) } - if( - e.functionMode === 'record' || - e.type === 'mjpeg' || - e.type === 'h264' || - e.type === 'local' - ){ - s.cameraFilterFfmpegLog(e) - } - if(e.coProcessor === true){ - setTimeout(function(){ - s.coSpawnLauncher(e) - },6000) - } - s.onMonitorStartExtensions.forEach(function(extender){ - extender(Object.assign(s.group[e.ke].rawMonitorConfigurations[e.id],{}),e) - }) }else{ s.onMonitorPingFailedExtensions.forEach(function(extender){ extender(Object.assign(s.group[e.ke].rawMonitorConfigurations[e.id],{}),e) }) s.userLog(e,{type:lang["Ping Failed"],msg:lang.skipPingText1}); - s.fatalCameraError(e,"Ping Failed");return; + fatalError(e,"Ping Failed");return; } } if( @@ -1380,12 +1232,16 @@ module.exports = function(s,config,lang){ e.type !== 'local' && e.details.skip_ping !== '1' ){ - connectionTester.test(strippedHost,e.port,2000,startVideoProcessor); + try{ + connectionTester.test(strippedHost,e.port,2000,startVideoProcessor); + }catch(err){ + startVideoProcessor(null,{success:true}) + } }else{ startVideoProcessor(null,{success:true}) } }else{ - s.cameraDestroy(s.group[e.ke].activeMonitors[e.id].spawn,e) + cameraDestroy(e) } if(callback)callback() }) @@ -1399,7 +1255,7 @@ module.exports = function(s,config,lang){ mode : e.functionMode, //data, options d : s.group[e.ke].rawMonitorConfigurations[e.id] - },s.group[e.ke].activeMonitors[e.id].childNodeId) + },activeMonitor.childNodeId) } if( e.type !== 'socket' && @@ -1416,7 +1272,7 @@ module.exports = function(s,config,lang){ extender(Object.assign(s.group[e.ke].rawMonitorConfigurations[e.id],{}),e) }) s.userLog(e,{type:lang["Ping Failed"],msg:lang.skipPingText1}); - s.fatalCameraError(e,"Ping Failed");return; + fatalError(e,"Ping Failed");return; } }) }else{ @@ -1446,9 +1302,9 @@ module.exports = function(s,config,lang){ if(nodeWithLowestActiveCameras)selectNode(nodeWithLowestActiveCameras) if(e.childNodeFound === true){ s.childNodes[e.childNodeSelected].activeCameras[e.ke+e.id] = copiedMonitorObject - s.group[e.ke].activeMonitors[e.id].childNode = e.childNodeSelected - s.group[e.ke].activeMonitors[e.id].childNodeId = s.childNodes[e.childNodeSelected].cnid; - s.cx({f:'sync',sync:s.group[e.ke].rawMonitorConfigurations[e.id],ke:e.ke,mid:e.id},s.group[e.ke].activeMonitors[e.id].childNodeId); + activeMonitor.childNode = e.childNodeSelected + activeMonitor.childNodeId = s.childNodes[e.childNodeSelected].cnid; + s.cx({f:'sync',sync:s.group[e.ke].rawMonitorConfigurations[e.id],ke:e.ke,mid:e.id},activeMonitor.childNodeId); doOnChildMachine() }else{ startMonitorInQueue.push(doOnThisMachine,function(){}) @@ -1464,46 +1320,47 @@ module.exports = function(s,config,lang){ console.log(err) } } - s.fatalCameraError = function(e,errorMessage){ - clearTimeout(s.group[e.ke].activeMonitors[e.id].err_fatal_timeout); - ++e.errorFatalCount; - if(s.group[e.ke].activeMonitors[e.id].isStarted === true){ - s.group[e.ke].activeMonitors[e.id].err_fatal_timeout = setTimeout(function(){ - if(e.details.fatal_max !== 0 && e.errorFatalCount > e.details.fatal_max){ + const fatalError = function(e,errorMessage){ + const activeMonitor = s.group[e.ke].activeMonitors[e.id] + clearTimeout(activeMonitor.err_fatal_timeout); + ++activeMonitor.errorFatalCount; + if(activeMonitor.isStarted === true){ + activeMonitor.err_fatal_timeout = setTimeout(function(){ + if(e.details.fatal_max !== 0 && activeMonitor.errorFatalCount > e.details.fatal_max){ s.camera('stop',{id:e.id,ke:e.ke}) }else{ - s.launchMonitorProcesses(s.cleanMonitorObject(e)) + launchMonitorProcesses(s.cleanMonitorObject(e)) }; },5000); }else{ - s.cameraDestroy(s.group[e.ke].activeMonitors[e.id].spawn,e) + cameraDestroy(e) } s.sendMonitorStatus({id:e.id,ke:e.ke,status:lang.Died}) s.onMonitorDiedExtensions.forEach(function(extender){ extender(Object.assign(s.group[e.ke].rawMonitorConfigurations[e.id],{}),e) }) } - s.isWatchCountable = function(d){ - try{ - var variableMethodsToAllow = [ - 'mp4ws', //Poseidon over Websocket - 'flvws', - 'h265ws', - ]; - var indefiniteIgnore = [ - 'mjpeg', - 'h264', - ]; - var monConfig = s.group[d.ke].rawMonitorConfigurations[d.id] - if( - variableMethodsToAllow.indexOf(monConfig.details.stream_type + monConfig.details.stream_flv_type) > -1 && - indefiniteIgnore.indexOf(monConfig.details.stream_type) === -1 - ){ - return true - } - }catch(err){} - return false - } + // s.isWatchCountable = function(d){ + // try{ + // var variableMethodsToAllow = [ + // 'mp4ws', //Poseidon over Websocket + // 'flvws', + // 'h265ws', + // ]; + // var indefiniteIgnore = [ + // 'mjpeg', + // 'h264', + // ]; + // var monConfig = s.group[d.ke].rawMonitorConfigurations[d.id] + // if( + // variableMethodsToAllow.indexOf(monConfig.details.stream_type + monConfig.details.stream_flv_type) > -1 && + // indefiniteIgnore.indexOf(monConfig.details.stream_type) === -1 + // ){ + // return true + // } + // }catch(err){} + // return false + // } s.addOrEditMonitor = function(form,callback,user){ var endData = { ok: false @@ -1515,10 +1372,17 @@ module.exports = function(s,config,lang){ } form.mid = form.mid.replace(/[^\w\s]/gi,'').replace(/ /g,'') form = s.cleanMonitorObjectForDatabase(form) - s.sqlQuery('SELECT * FROM Monitors WHERE ke=? AND mid=?',[form.ke,form.mid],function(er,r){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Monitors", + where: [ + ['ke','=',form.ke], + ['mid','=',form.mid], + ] + },(err,r) => { var affectMonitor = false - var monitorQuery = [] - var monitorQueryValues = [] + var monitorQuery = {} var txData = { f: 'monitor_edit', mid: form.mid, @@ -1536,19 +1400,23 @@ module.exports = function(s,config,lang){ form[v] !== false && form[v] !== `false` ){ - monitorQuery.push(v+'=?') if(form[v] instanceof Object){ form[v] = s.s(form[v]) } - monitorQueryValues.push(form[v]) + monitorQuery[v] = form[v] } }) - monitorQuery = monitorQuery.join(',') - monitorQueryValues.push(form.ke) - monitorQueryValues.push(form.mid) s.userLog(form,{type:'Monitor Updated',msg:'by user : '+user.uid}) endData.msg = user.lang['Monitor Updated by user']+' : '+user.uid - s.sqlQuery('UPDATE Monitors SET '+monitorQuery+' WHERE ke=? AND mid=?',monitorQueryValues) + s.knexQuery({ + action: "update", + table: "Monitors", + update: monitorQuery, + where: [ + ['ke','=',form.ke], + ['mid','=',form.mid], + ] + }) affectMonitor = true }else if( !s.group[form.ke].init.max_camera || @@ -1556,22 +1424,21 @@ module.exports = function(s,config,lang){ Object.keys(s.group[form.ke].activeMonitors).length <= parseInt(s.group[form.ke].init.max_camera) ){ txData.new = true - monitorQueryInsertValues = [] Object.keys(form).forEach(function(v){ if(form[v] && form[v] !== ''){ - monitorQuery.push(v) - monitorQueryInsertValues.push('?') if(form[v] instanceof Object){ form[v] = s.s(form[v]) } - monitorQueryValues.push(form[v]) + monitorQuery[v] = form[v] } }) - monitorQuery = monitorQuery.join(',') - monitorQueryInsertValues = monitorQueryInsertValues.join(',') s.userLog(form,{type:'Monitor Added',msg:'by user : '+user.uid}) endData.msg = user.lang['Monitor Added by user']+' : '+user.uid - s.sqlQuery('INSERT INTO Monitors ('+monitorQuery+') VALUES ('+monitorQueryInsertValues+')',monitorQueryValues) + s.knexQuery({ + action: "insert", + table: "Monitors", + insert: monitorQuery + }) affectMonitor = true }else{ txData.f = 'monitor_edit_failed' @@ -1586,9 +1453,9 @@ module.exports = function(s,config,lang){ if(form.mode === 'stop'){ s.camera('stop',form) }else{ - s.camera('stop',form) + s.camera('stop',Object.assign(s.group[form.ke].rawMonitorConfigurations[form.mid])) setTimeout(function(){ - s.camera(form.mode,form) + s.camera(form.mode,Object.assign(s.group[form.ke].rawMonitorConfigurations[form.mid])) },5000) } s.tx(txData,'STR_'+form.ke) @@ -1608,7 +1475,7 @@ module.exports = function(s,config,lang){ e.functionMode = x if(!e.mode){e.mode = x} s.checkDetails(e) - s.cameraCheckObjectsInDetails(e) + checkObjectsInDetails(e) s.initiateMonitorObject({ke:e.ke,mid:e.id}) switch(e.functionMode){ case'watch_on'://live streamers - join @@ -1657,14 +1524,9 @@ module.exports = function(s,config,lang){ if(s.group[e.ke].activeMonitors[e.id].fswatchStream){s.group[e.ke].activeMonitors[e.id].fswatchStream.close();delete(s.group[e.ke].activeMonitors[e.id].fswatchStream)} if(s.group[e.ke].activeMonitors[e.id].last_frame){delete(s.group[e.ke].activeMonitors[e.id].last_frame)} if(s.group[e.ke].activeMonitors[e.id].isStarted !== true){return} - s.cameraDestroy(s.group[e.ke].activeMonitors[e.id].spawn,e) - if(e.neglectTriggerTimer === 1){ - delete(e.neglectTriggerTimer); - }else{ - clearTimeout(s.group[e.ke].activeMonitors[e.id].trigger_timer) - delete(s.group[e.ke].activeMonitors[e.id].trigger_timer) - } - clearInterval(s.group[e.ke].activeMonitors[e.id].running); + cameraDestroy(e) + clearTimeout(s.group[e.ke].activeMonitors[e.id].trigger_timer) + delete(s.group[e.ke].activeMonitors[e.id].trigger_timer) clearInterval(s.group[e.ke].activeMonitors[e.id].detector_notrigger_timeout) clearTimeout(s.group[e.ke].activeMonitors[e.id].err_fatal_timeout); s.group[e.ke].activeMonitors[e.id].isStarted = false @@ -1696,27 +1558,27 @@ module.exports = function(s,config,lang){ break; case'start':case'record'://watch or record monitor url s.initiateMonitorObject({ke:e.ke,mid:e.id}) + const activeMonitor = s.group[e.ke].activeMonitors[e.id] if(!s.group[e.ke].rawMonitorConfigurations[e.id]){s.group[e.ke].rawMonitorConfigurations[e.id]=s.cleanMonitorObject(e);} - e.url = s.buildMonitorUrl(e); - if(s.group[e.ke].activeMonitors[e.id].isStarted === true){ + if(activeMonitor.isStarted === true){ //stop action, monitor already started or recording return } //lock this function s.sendMonitorStatus({id:e.id,ke:e.ke,status:lang.Starting}); - s.group[e.ke].activeMonitors[e.id].isStarted = true + activeMonitor.isStarted = true if(e.details && e.details.dir && e.details.dir !== ''){ - s.group[e.ke].activeMonitors[e.id].addStorageId = e.details.dir + activeMonitor.addStorageId = e.details.dir }else{ - s.group[e.ke].activeMonitors[e.id].addStorageId = null + activeMonitor.addStorageId = null } //set recording status e.wantedStatus = lang.Watching if(e.functionMode === 'record'){ e.wantedStatus = lang.Recording - s.group[e.ke].activeMonitors[e.id].isRecording = true + activeMonitor.isRecording = true }else{ - s.group[e.ke].activeMonitors[e.mid].isRecording = false + activeMonitor.isRecording = false } //set up fatal error handler if(e.details.fatal_max === ''){ @@ -1724,13 +1586,59 @@ module.exports = function(s,config,lang){ }else{ e.details.fatal_max = parseFloat(e.details.fatal_max) } - e.errorFatalCount = 0; + activeMonitor.errorFatalCount = 0; //cutoff time and recording check interval if(!e.details.cutoff||e.details.cutoff===''){e.cutoff=15}else{e.cutoff=parseFloat(e.details.cutoff)}; if(isNaN(e.cutoff)===true){e.cutoff=15} //start drawing files - delete(s.group[e.ke].activeMonitors[e.id].childNode) - s.launchMonitorProcesses(e) + delete(activeMonitor.childNode) + //validate port + if(!e.port){ + switch(e.protocol){ + case'http': + e.port = '80' + break; + case'rtmps': + case'https': + e.port = '443' + break; + case'rtmp': + e.port = '1935' + break; + case'rtsp': + e.port = '554' + break; + } + } + if(e.details.detector_ptz_follow === '1'){ + setTimeout(() => { + setPresetForCurrentPosition({ + ke: e.ke, + id: e.id + },(endData) => { + if(endData.ok === false){ + setTimeout(() => { + setPresetForCurrentPosition({ + ke: e.ke, + id: e.id + },(endData) => { + if(endData.ok === false){ + setTimeout(() => { + setPresetForCurrentPosition({ + ke: e.ke, + id: e.id + },(endData) => { + console.log(endData) + }) + },5000) + } + }) + },5000) + } + }) + },5000) + } + launchMonitorProcesses(e) break; default: console.log(x) @@ -1747,15 +1655,24 @@ module.exports = function(s,config,lang){ if(notFound === false){ var sqlQuery = 'SELECT * FROM Monitors WHERE ke=? AND ' var monitorQuery = [] - var sqlQueryValues = [groupKey] var monitorPresets = {} preset.details.monitors.forEach(function(monitor){ - monitorQuery.push('mid=?') - sqlQueryValues.push(monitor.mid) + const whereConditions = {} + if(monitorQuery.length === 0){ + whereConditions.ke = groupKey + monitorQuery.push(['ke','=',groupKey]) + }else{ + monitorQuery.push(['or','ke','=',groupKey]) + } + monitorQuery.push(['mid','=',monitor.mid]) monitorPresets[monitor.mid] = monitor }) - sqlQuery += '('+monitorQuery.join(' OR ')+')' - s.sqlQuery(sqlQuery,sqlQueryValues,function(err,monitors){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Monitors", + where: monitorQuery + },function(err,monitors){ if(monitors && monitors[0]){ monitors.forEach(function(monitor){ s.checkDetails(monitor) @@ -1812,6 +1729,40 @@ module.exports = function(s,config,lang){ }) return cameras } + s.getMonitorRestrictions = (permissions,monitorId) => { + const monitorRestrictions = [] + if( + !monitorId && + permissions.sub && + permissions.monitors && + permissions.allmonitors !== '1' + ){ + try{ + permissions.monitors = s.parseJSON(permissions.monitors) + permissions.monitors.forEach(function(v,n){ + if(n === 0){ + monitorRestrictions.push(['mid','=',v]) + }else{ + monitorRestrictions.push(['or','mid','=',v]) + } + }) + }catch(er){ + } + }else if( + monitorId && ( + !permissions.sub || + permissions.allmonitors !== '0' || + permissions.monitors.indexOf(monitorId) >- 1 + ) + ){ + monitorRestrictions.push(['mid','=',monitorId]) + }else if( + !monitorId && + permissions.sub && + permissions.allmonitors !== '0' + ){} + return monitorRestrictions + } // s.checkViewerConnectionsForMonitor = function(monitorObject){ // var monitorConfig = s.group[monitorObject.ke].rawMonitorConfigurations[monitorObject.mid] // if(monitorConfig.mode === 'start'){ diff --git a/libs/monitor/utils.js b/libs/monitor/utils.js new file mode 100644 index 00000000..fa8e01c9 --- /dev/null +++ b/libs/monitor/utils.js @@ -0,0 +1,89 @@ +module.exports = (s,config,lang) => { + const cameraDestroy = function(e,p){ + if( + s.group[e.ke] && + s.group[e.ke].activeMonitors[e.id] && + s.group[e.ke].activeMonitors[e.id].spawn !== undefined + ){ + const activeMonitor = s.group[e.ke].activeMonitors[e.id]; + const proc = s.group[e.ke].activeMonitors[e.id].spawn; + if(proc){ + activeMonitor.allowStdinWrite = false + s.txToDashcamUsers({ + f : 'disable_stream', + ke : e.ke, + mid : e.id + },e.ke) + // if(activeMonitor.p2pStream){activeMonitor.p2pStream.unpipe();} + try{ + proc.removeListener('end',activeMonitor.spawn_exit); + proc.removeListener('exit',activeMonitor.spawn_exit); + delete(activeMonitor.spawn_exit); + }catch(er){ + + } + } + if(activeMonitor.audioDetector){ + activeMonitor.audioDetector.stop() + delete(activeMonitor.audioDetector) + } + activeMonitor.firstStreamChunk = {} + clearTimeout(activeMonitor.recordingChecker); + delete(activeMonitor.recordingChecker); + clearTimeout(activeMonitor.streamChecker); + delete(activeMonitor.streamChecker); + clearTimeout(activeMonitor.checkSnap); + delete(activeMonitor.checkSnap); + clearTimeout(activeMonitor.watchdog_stop); + delete(activeMonitor.watchdog_stop); + delete(activeMonitor.lastJpegDetectorFrame); + delete(activeMonitor.detectorFrameSaveBuffer); + clearTimeout(activeMonitor.recordingSnapper); + clearInterval(activeMonitor.getMonitorCpuUsage); + clearInterval(activeMonitor.objectCountIntervals); + delete(activeMonitor.onvifConnection) + if(activeMonitor.onChildNodeExit){ + activeMonitor.onChildNodeExit() + } + activeMonitor.spawn.stdio.forEach(function(stdio){ + try{ + stdio.unpipe() + }catch(err){ + console.log(err) + } + }) + if(activeMonitor.mp4frag){ + var mp4FragChannels = Object.keys(activeMonitor.mp4frag) + mp4FragChannels.forEach(function(channel){ + activeMonitor.mp4frag[channel].removeAllListeners() + delete(activeMonitor.mp4frag[channel]) + }) + } + if(config.childNodes.enabled === true && config.childNodes.mode === 'child' && config.childNodes.host){ + s.cx({f:'clearCameraFromActiveList',ke:e.ke,id:e.id}) + } + if(activeMonitor.childNode){ + s.cx({f:'kill',d:s.cleanMonitorObject(e)},activeMonitor.childNodeId) + }else{ + s.coSpawnClose(e) + if(proc && proc.kill){ + if(s.isWin){ + spawn("taskkill", ["/pid", proc.pid, '/t']) + }else{ + proc.kill('SIGTERM') + } + setTimeout(function(){ + try{ + proc.kill() + }catch(err){ + s.debugLog(err) + } + },1000) + } + } + } + } + return { + cameraDestroy: cameraDestroy + } +} diff --git a/libs/notification.js b/libs/notification.js index 2d131e7e..d94b9d9f 100644 --- a/libs/notification.js +++ b/libs/notification.js @@ -1,6 +1,16 @@ var fs = require("fs") var Discord = require("discord.js") +var template = require("./notifications/emailTemplate.js") module.exports = function(s,config,lang){ + const checkEmail = (email) => { + if(email.toLowerCase().indexOf('@shinobi') > -1 && !config.allowSpammingViaEmail){ + console.log('CHANGE YOUR ACCOUNT EMAIL!') + console.log(email + ' IS NOT ALLOWED TO BE USED') + console.log('YOU CANNOT EMAIL TO THIS ADDRESS') + return 'cannot@email.com' + } + return email + } //discord bot if(config.discordBot === true){ try{ @@ -11,7 +21,7 @@ module.exports = function(s,config,lang){ s.userLog({ke:groupKey,mid:'$USER'},{type:lang.DiscordFailedText,msg:lang.DiscordNotEnabledText}) return } - var sendBody = Object.assign({ + const sendBody = Object.assign({ color: 3447003, title: 'Alert from Shinobi', description: "", @@ -22,7 +32,7 @@ module.exports = function(s,config,lang){ text: "Shinobi Systems" } },data) - var discordChannel = bot.channels.get(s.group[groupKey].init.discordbot_channel) + const discordChannel = bot.channels.cache.get(s.group[groupKey].init.discordbot_channel) if(discordChannel && discordChannel.send){ discordChannel.send({ embed: sendBody, @@ -44,10 +54,10 @@ module.exports = function(s,config,lang){ }) } } - var onEventTriggerBeforeFilterForDiscord = function(d,filter){ + const onEventTriggerBeforeFilterForDiscord = function(d,filter){ filter.discord = true } - var onEventTriggerForDiscord = function(d,filter){ + const onEventTriggerForDiscord = async (d,filter) => { // d = event object //discord bot if(filter.discord && s.group[d.ke].discordBot && d.mon.details.detector_discordbot === '1' && !s.group[d.ke].activeMonitors[d.id].detector_discordbot){ @@ -59,28 +69,11 @@ module.exports = function(s,config,lang){ } //lock mailer so you don't get emailed on EVERY trigger event. s.group[d.ke].activeMonitors[d.id].detector_discordbot = setTimeout(function(){ - //unlock so you can mail again. clearTimeout(s.group[d.ke].activeMonitors[d.id].detector_discordbot); delete(s.group[d.ke].activeMonitors[d.id].detector_discordbot); },detector_discordbot_timeout) - var files = [] - var sendAlert = function(){ - s.discordMsg({ - author: { - name: s.group[d.ke].rawMonitorConfigurations[d.id].name, - icon_url: config.iconURL - }, - title: lang.Event+' - '+d.screenshotName, - description: lang.EventText1+' '+d.currentTimestamp, - fields: [], - timestamp: d.currentTime, - footer: { - icon_url: config.iconURL, - text: "Shinobi Systems" - } - },files,d.ke) - } if(d.mon.details.detector_discordbot_send_video === '1'){ + // change to function that captures on going video capture, waits, grabs new video file, slices portion (max for transmission) and prepares for delivery s.mergeDetectorBufferChunks(d,function(mergedFilepath,filename){ s.discordMsg({ author: { @@ -102,21 +95,34 @@ module.exports = function(s,config,lang){ ],d.ke) }) } - s.getRawSnapshotFromMonitor(d.mon,{ + const {screenShot, isStaticFile} = await s.getRawSnapshotFromMonitor(d.mon,{ secondsInward: d.mon.details.snap_seconds_inward - },function(data){ - if(data[data.length - 2] === 0xFF && data[data.length - 1] === 0xD9){ - d.screenshotBuffer = data - files.push({ - attachment: d.screenshotBuffer, - name: d.screenshotName+'.jpg' - }) - } - sendAlert() }) + if(screenShot[screenShot.length - 2] === 0xFF && screenShot[screenShot.length - 1] === 0xD9){ + d.screenshotBuffer = screenShot + s.discordMsg({ + author: { + name: s.group[d.ke].rawMonitorConfigurations[d.id].name, + icon_url: config.iconURL + }, + title: lang.Event+' - '+d.screenshotName, + description: lang.EventText1+' '+d.currentTimestamp, + fields: [], + timestamp: d.currentTime, + footer: { + icon_url: config.iconURL, + text: "Shinobi Systems" + } + },[ + { + attachment: screenShot, + name: d.screenshotName+'.jpg' + } + ],d.ke) + } } } - var onTwoFactorAuthCodeNotificationForDiscord = function(r){ + const onTwoFactorAuthCodeNotificationForDiscord = function(r){ // r = user if(r.details.factor_discord === '1'){ s.discordMsg({ @@ -135,13 +141,13 @@ module.exports = function(s,config,lang){ },[],r.ke) } } - var loadDiscordBotForUser = function(user){ - ar=JSON.parse(user.details); + const loadDiscordBotForUser = function(user){ + const userDetails = s.parseJSON(user.details); //discordbot if(!s.group[user.ke].discordBot && config.discordBot === true && - ar.discordbot === '1' && - ar.discordbot_token !== '' + userDetails.discordbot === '1' && + userDetails.discordbot_token !== '' ){ s.group[user.ke].discordBot = new Discord.Client() s.group[user.ke].discordBot.on('ready', () => { @@ -153,16 +159,16 @@ module.exports = function(s,config,lang){ msg: s.group[user.ke].discordBot.user.tag }) }) - s.group[user.ke].discordBot.login(ar.discordbot_token) + s.group[user.ke].discordBot.login(userDetails.discordbot_token) } } - var unloadDiscordBotForUser = function(user){ + const unloadDiscordBotForUser = function(user){ if(s.group[user.ke].discordBot && s.group[user.ke].discordBot.destroy){ s.group[user.ke].discordBot.destroy() delete(s.group[user.ke].discordBot) } } - var onDetectorNoTriggerTimeoutForDiscord = function(e){ + const onDetectorNoTriggerTimeoutForDiscord = function(e){ //e = monitor object var currentTime = new Date() if(e.details.detector_notrigger_discord === '1'){ @@ -204,14 +210,22 @@ module.exports = function(s,config,lang){ if(config.mail.from === undefined){config.mail.from = '"ShinobiCCTV" '} s.nodemailer = require('nodemailer').createTransport(config.mail); } - var onDetectorNoTriggerTimeoutForEmail = function(e){ + const onDetectorNoTriggerTimeoutForEmail = function(e){ //e = monitor object if(config.mail && e.details.detector_notrigger_mail === '1'){ - s.sqlQuery('SELECT mail FROM Users WHERE ke=? AND details NOT LIKE ?',[e.ke,'%"sub"%'],function(err,r){ + s.knexQuery({ + action: "select", + columns: "mail", + table: "Users", + where: [ + ['ke','=',e.ke], + ['details','NOT LIKE','%"sub"%'], + ] + },(err,r) => { r = r[0] var mailOptions = { from: config.mail.from, // sender address - to: r.mail, // list of receivers + to: checkEmail(r.mail), // list of receivers subject: lang.NoMotionEmailText1+' '+e.name+' ('+e.id+')', // Subject line html: ''+lang.NoMotionEmailText2+' ' + (e.details.detector_notrigger_timeout || 10) + ' '+lang.minutes+'.', } @@ -228,16 +242,15 @@ module.exports = function(s,config,lang){ }) } } - var onTwoFactorAuthCodeNotificationForEmail = function(r){ + const onTwoFactorAuthCodeNotificationForEmail = function(r){ // r = user object if(r.details.factor_mail !== '0'){ - var mailOptions = { + s.nodemailer.sendMail({ from: config.mail.from, - to: r.mail, + to: checkEmail(r.mail), subject: r.lang['2-Factor Authentication'], html: r.lang['Enter this code to proceed']+' '+s.factorAuth[r.ke][r.uid].key+'. '+r.lang.FactorAuthText1, - }; - s.nodemailer.sendMail(mailOptions, (error, info) => { + }, (error, info) => { if (error) { s.systemLog(r.lang.MailError,error) return @@ -245,14 +258,14 @@ module.exports = function(s,config,lang){ }) } } - var onFilterEventForEmail = function(x,d){ + const onFilterEventForEmail = function(x,d){ // x = filter function // d = filter event object if(x === 'email'){ if(d.videos && d.videos.length > 0){ d.mailOptions = { from: config.mail.from, // sender address - to: d.mail, // list of receivers + to: checkEmail(d.mail), subject: lang['Filter Matches']+' : '+d.name, // Subject line html: lang.FilterMatchesText1+' '+d.videos.length+' '+lang.FilterMatchesText2, }; @@ -274,13 +287,25 @@ module.exports = function(s,config,lang){ } } } - var onEventTriggerBeforeFilterForEmail = function(d,filter){ - filter.mail = true + const onEventTriggerBeforeFilterForEmail = function(d,filter){ + if(d.mon.details.detector_mail === '1'){ + filter.mail = true + }else{ + filter.mail = false + } } - var onEventTriggerForEmail = function(d,filter){ - if(filter.mail && config.mail && !s.group[d.ke].activeMonitors[d.id].detector_mail && d.mon.details.detector_mail === '1'){ - s.sqlQuery('SELECT mail FROM Users WHERE ke=? AND details NOT LIKE ?',[d.ke,'%"sub"%'],function(err,r){ - r=r[0]; + const onEventTriggerForEmail = async (d,filter) => { + if(filter.mail && config.mail && !s.group[d.ke].activeMonitors[d.id].detector_mail){ + s.knexQuery({ + action: "select", + columns: "mail", + table: "Users", + where: [ + ['ke','=',d.ke], + ['details','NOT LIKE','%"sub"%'], + ] + },async (err,r) => { + r = r[0]; var detector_mail_timeout if(!d.mon.details.detector_mail_timeout||d.mon.details.detector_mail_timeout===''){ detector_mail_timeout = 1000*60*10; @@ -288,24 +313,35 @@ module.exports = function(s,config,lang){ detector_mail_timeout = parseFloat(d.mon.details.detector_mail_timeout)*1000*60; } //lock mailer so you don't get emailed on EVERY trigger event. - s.group[d.ke].activeMonitors[d.id].detector_mail=setTimeout(function(){ + s.group[d.ke].activeMonitors[d.id].detector_mail = setTimeout(function(){ //unlock so you can mail again. clearTimeout(s.group[d.ke].activeMonitors[d.id].detector_mail); delete(s.group[d.ke].activeMonitors[d.id].detector_mail); },detector_mail_timeout); - var files = [] - var mailOptions = { - from: config.mail.from, // sender address - to: r.mail, // list of receivers - subject: lang.Event+' - '+d.screenshotName, // Subject line - html: ''+lang.EventText1+' '+d.currentTimestamp+'.', - attachments: files - } - var sendMail = function(){ - Object.keys(d.details).forEach(function(v,n){ - mailOptions.html+='
'+v+' : '+d.details[v]+'
' + const sendMail = function(files){ + const infoRows = [] + Object.keys(d.details).forEach(function(key){ + var value = d.details[key] + var text = value + if(value instanceof Object){ + text = JSON.stringify(value,null,3) + } + infoRows.push(template.createRow({ + title: key, + text: text + })) }) - s.nodemailer.sendMail(mailOptions, (error, info) => { + s.nodemailer.sendMail({ + from: config.mail.from, + to: checkEmail(r.mail), + subject: lang.Event+' - '+d.screenshotName, + html: template.createFramework({ + title: lang.EventText1 + ' ' + d.currentTimestamp, + subtitle: 'Shinobi Event', + body: infoRows.join(''), + }), + attachments: files || [] + }, (error, info) => { if (error) { s.systemLog(lang.MailError,error) return false; @@ -313,12 +349,13 @@ module.exports = function(s,config,lang){ }) } if(d.mon.details.detector_mail_send_video === '1'){ + // change to function that captures on going video capture, waits, grabs new video file, slices portion (max for transmission) and prepares for delivery s.mergeDetectorBufferChunks(d,function(mergedFilepath,filename){ fs.readFile(mergedFilepath,function(err,buffer){ if(buffer){ s.nodemailer.sendMail({ from: config.mail.from, - to: r.mail, + to: checkEmail(r.mail), subject: filename, html: '', attachments: [ @@ -337,24 +374,18 @@ module.exports = function(s,config,lang){ }) }) } - if(d.screenshotBuffer){ - files.push({ + if(!d.screenshotBuffer){ + const {screenShot, isStaticFile} = await s.getRawSnapshotFromMonitor(d.mon,{ + secondsInward: d.mon.details.snap_seconds_inward + }) + d.screenshotBuffer = screenShot + } + sendMail([ + { filename: d.screenshotName + '.jpg', content: d.screenshotBuffer - }) - sendMail() - }else{ - s.getRawSnapshotFromMonitor(d.mon,{ - secondsInward: d.mon.details.snap_seconds_inward - },function(data){ - d.screenshotBuffer = data - files.push({ - filename: d.screenshotName + '.jpg', - content: data - }) - sendMail() - }) - } + } + ]) }) } } diff --git a/libs/notifications/emailTemplate.js b/libs/notifications/emailTemplate.js new file mode 100644 index 00000000..dd18d892 --- /dev/null +++ b/libs/notifications/emailTemplate.js @@ -0,0 +1,250 @@ +// Example of how to generate HTML for an email. +// createFramework({ +// title: 'Password Reset', +// subtitle: 'If you did not make this request please change your password.', +// body: [ +// createRow({ +// title: 'Customer', +// text: `${customer.email} — ${customer.id}` +// }), +// createRow({ +// btn: { +// text: 'Confirm Password Reset', +// href: `https://licenses.shinobi.video/forgot/reset?code=${newCode}` +// } +// }), +// createRow({ +// title: 'Reset Code', +// text: newCode +// }), +// ].join(''), +// }) +module.exports = { + createRow : (options) => { + const trFillers = ` + +
 
+ + + + +
 
+ + ` + if(options.btn){ + return ` + + + + + + + + + + + + ${trFillers} + +
+
 
+
+
 
+
+ + + + + + +
+ + + ${options.btn.text} + + +
+
+
 
+
+ + ` + } + return ` + + + + + + + + + + + + ${trFillers} + +
+
 
+
+
 
+
+ + + + + + +
+ ${options.title} +
+ + + + + + +
+
 
+
+ + + + + + +
+ ${options.text} +
+
+
 
+
+ + ` + }, + createFramework : (options) => { + return `
+ + + + + + + + + + + +
+
 
+
+ ${options.title} + +
 
+
+
 
+
+ + + + + + + + + + + +
+
 
+
${options.subtitle} +
 
+
+
 
+
+ + + + + + + + + + +
+
 
+
+
 
+
+ + + ${options.body} + + + + +
+
 
+
+
+ + +
 
+ + + + +
 
+ + + + + + + + + + + + + + + + + + +
+
 
+
+
 
+
+
 
+
+
 
+
+
 
+
+ ${options.footerText ? ` + + + + + + + + + + +
+
 
+
+ ${options.footerText} + +
 
+
+
 
+
` : ''} +
` + } +} diff --git a/libs/plugins.js b/libs/plugins.js index 3898afa5..70ef175d 100644 --- a/libs/plugins.js +++ b/libs/plugins.js @@ -14,11 +14,14 @@ module.exports = function(s,config,lang,io){ case's.tx': s.tx(d.data,d.to) break; + case'log': + s.systemLog('PLUGIN : '+d.plug+' : ',d) + break; case's.sqlQuery': s.sqlQuery(d.query,d.values) break; - case'log': - s.systemLog('PLUGIN : '+d.plug+' : ',d) + case's.knexQuery': + s.knexQuery(d.options) break; } } @@ -72,10 +75,84 @@ module.exports = function(s,config,lang,io){ s.debugLog(`resetDetectorPluginArray : ${JSON.stringify(pluginArray)}`) s.detectorPluginArray = pluginArray } - s.sendToAllDetectors = function(data){ - s.detectorPluginArray.forEach(function(name){ - s.connectedPlugins[name].tx(data) - }) + var currentPluginCpuUsage = {} + var currentPluginGpuUsage = {} + var currentPluginFrameProcessingCount = {} + var pluginHandlersSet = {} + if(config.detectorPluginsCluster){ + if(config.clusterUseBasicFrameCount === undefined)config.clusterUseBasicFrameCount = true; + if(config.clusterUseBasicFrameCount){ + // overAllProcessingCount + var getPluginWithLowestUtilization = () => { + var selectedPluginServer = null + var lowestUsed = 1000 + s.detectorPluginArray.forEach((pluginName) => { + const processCount = currentPluginFrameProcessingCount[pluginName] || 0 + if(processCount < lowestUsed){ + selectedPluginServer = pluginName + lowestUsed = processCount + } + }) + if(selectedPluginServer){ + return s.connectedPlugins[selectedPluginServer] + }else{ + return {tx: () => {}} + } + } + }else{ + + if(config.clusterBasedOnGpu){ + var getPluginWithLowestUtilization = () => { + var selectedPluginServer = null + var lowestUsed = 1000 + s.detectorPluginArray.forEach((pluginName) => { + var overAllPercent = 0 + var gpus = currentPluginGpuUsage[pluginName] + gpus.forEach((gpu) => { + console.log(gpu) + const percent = gpu.utilization + overAllPercent += percent + }) + if((overAllPercent / gpus.length) < lowestUsed){ + selectedPluginServer = pluginName + lowestUsed = overAllPercent + } + }) + if(selectedPluginServer){ + return s.connectedPlugins[selectedPluginServer] + }else{ + return {tx: () => {}} + } + } + }else{ + var getPluginWithLowestUtilization = () => { + var selectedPluginServer = null + var lowestUsed = 1000 + s.detectorPluginArray.forEach((pluginName) => { + const percent = currentPluginCpuUsage[pluginName] + if(percent < lowestUsed){ + selectedPluginServer = pluginName + lowestUsed = percent + } + }) + if(selectedPluginServer){ + return s.connectedPlugins[selectedPluginServer] + }else{ + return {tx: () => {}} + } + } + } + } + s.debugLog(`Detector Plugins running in Cluster Mode`) + s.sendToAllDetectors = function(data){ + getPluginWithLowestUtilization().tx(data) + } + }else{ + s.sendToAllDetectors = function(data){ + s.detectorPluginArray.forEach(function(name){ + s.connectedPlugins[name].tx(data) + }) + } } s.sendDetectorInfoToClient = function(data,txFunction){ s.detectorPluginArray.forEach(function(name){ @@ -212,6 +289,7 @@ module.exports = function(s,config,lang,io){ if(cn.ocv && s.ocv){ s.tx({f:'detector_unplugged',plug:s.ocv.plug},'CPU') delete(s.ocv); + delete(pluginHandlersSet[pluginName]) } } var onSocketAuthentication = function(r,cn,d,tx){ @@ -223,17 +301,32 @@ module.exports = function(s,config,lang,io){ tx({f:'detector_plugged',plug:s.ocv.plug,notice:s.ocv.notice}) } } + var addCpuUsageHandler = (cn,pluginName) => { + if(pluginHandlersSet[pluginName])return; + pluginHandlersSet[pluginName] = true + cn.on('cpuUsage',function(percent){ + currentPluginCpuUsage[pluginName] = percent + }) + cn.on('gpuUsage',function(gpus){ + currentPluginGpuUsage[pluginName] = gpus + }) + cn.on('processCount',function(count){ + currentPluginFrameProcessingCount[pluginName] = count + }) + } var onWebSocketConnection = function(cn){ cn.on('ocv',function(d){ if(!cn.pluginEngine && d.f === 'init'){ if(config.pluginKeys[d.plug] === d.pluginKey){ s.pluginInitiatorSuccess("client",d,cn) + if(config.detectorPluginsCluster)addCpuUsageHandler(cn,d.plug) }else{ s.pluginInitiatorFail("client",d,cn) } }else{ if(config.pluginKeys[d.plug] === d.pluginKey){ s.pluginEventController(d) + if(config.detectorPluginsCluster)addCpuUsageHandler(cn,d.plug) }else{ cn.disconnect() } diff --git a/libs/process.js b/libs/process.js index fc2550bf..71ad2345 100644 --- a/libs/process.js +++ b/libs/process.js @@ -31,7 +31,7 @@ module.exports = function(process,__dirname){ //UTC Offset utcOffset : require('moment')().utcOffset(), //directory path for this file - mainDirectory : __dirname + mainDirectory : process.cwd() } s.packageJson = packageJson if(packageJson.mainDirectory){ diff --git a/libs/scanners.js b/libs/scanners.js new file mode 100644 index 00000000..d99a0210 --- /dev/null +++ b/libs/scanners.js @@ -0,0 +1,173 @@ +var os = require('os'); +var exec = require('child_process').exec; +var onvif = require("node-onvif"); +module.exports = function(s,config,lang,app,io){ + const activeProbes = {} + const runFFprobe = (url,auth,callback) => { + var endData = {ok: false} + if(!url){ + endData.error = 'Missing URL' + callback(endData) + return + } + if(activeProbes[auth]){ + endData.error = 'Account is already probing' + callback(endData) + return + } + activeProbes[auth] = 1 + const probeCommand = s.splitForFFPMEG(`-v quiet -print_format json -show_format -show_streams -i "${url}"`).join(' ') + exec('ffprobe ' + probeCommand,function(err,stdout,stderr){ + delete(activeProbes[auth]) + if(err){ + endData.error = (err) + }else{ + endData.ok = true + endData.result = s.parseJSON(stdout) + } + endData.probe = probeCommand + callback(endData) + }) + } + const runOnvifScanner = (options,foundCameraCallback) => { + var ip = options.ip.replace(/ /g,'') + var ports = options.port.replace(/ /g,'') + if(options.ip === ''){ + var interfaces = os.networkInterfaces() + var addresses = [] + for (var k in interfaces) { + for (var k2 in interfaces[k]) { + var address = interfaces[k][k2] + if (address.family === 'IPv4' && !address.internal) { + addresses.push(address.address) + } + } + } + const addressRange = [] + addresses.forEach(function(address){ + if(address.indexOf('0.0.0')>-1){return false} + var addressPrefix = address.split('.') + delete(addressPrefix[3]); + addressPrefix = addressPrefix.join('.') + addressRange.push(`${addressPrefix}1-${addressPrefix}254`) + }) + ip = addressRange.join(',') + } + if(ports === ''){ + ports = '80,8080,8000,7575,8081,9080,8090,8999,8899' + } + if(ports.indexOf('-') > -1){ + ports = ports.split('-') + var portRangeStart = ports[0] + var portRangeEnd = ports[1] + ports = s.portRange(portRangeStart,portRangeEnd); + }else{ + ports = ports.split(',') + } + var ipList = options.ipList + var onvifUsername = options.user || '' + var onvifPassword = options.pass || '' + ip.split(',').forEach(function(addressRange){ + var ipRangeStart = addressRange[0] + var ipRangeEnd = addressRange[1] + if(addressRange.indexOf('-')>-1){ + addressRange = addressRange.split('-'); + ipRangeStart = addressRange[0] + ipRangeEnd = addressRange[1] + }else{ + ipRangeStart = addressRange + ipRangeEnd = addressRange + } + if(!ipList){ + ipList = s.ipRange(ipRangeStart,ipRangeEnd); + }else{ + ipList = ipList.concat(s.ipRange(ipRangeStart,ipRangeEnd)) + } + }) + var hitList = [] + ipList.forEach((ipEntry,n) => { + ports.forEach((portEntry,nn) => { + hitList.push({ + xaddr : 'http://' + ipEntry + ':' + portEntry + '/onvif/device_service', + user : onvifUsername, + pass : onvifPassword, + ip: ipEntry, + port: portEntry, + }) + }) + }) + var responseList = [] + hitList.forEach(async (camera) => { + try{ + var device = new onvif.OnvifDevice(camera) + var info = await device.init() + var date = await device.services.device.getSystemDateAndTime() + var stream = await device.services.media.getStreamUri({ + ProfileToken : device.current_profile.token, + Protocol : 'RTSP' + }) + var cameraResponse = { + ip: camera.ip, + port: camera.port, + info: info, + date: date, + uri: stream.data.GetStreamUriResponse.MediaUri.Uri + } + responseList.push(cameraResponse) + if(foundCameraCallback)foundCameraCallback(Object.assign(cameraResponse,{f: 'onvif'})) + }catch(err){ + const searchError = (find) => { + return s.stringContains(find,err.message,true) + } + var foundDevice = false + var errorMessage = '' + switch(true){ + //ONVIF camera found but denied access + case searchError('400'): //Bad Request - Sender not Authorized + foundDevice = true + errorMessage = lang.ONVIFErr400 + break; + case searchError('405'): //Method Not Allowed + foundDevice = true + errorMessage = lang.ONVIFErr405 + break; + //Webserver exists but undetermined if IP Camera + case searchError('404'): //Not Found + foundDevice = true + errorMessage = lang.ONVIFErr404 + break; + } + if(foundDevice && foundCameraCallback)foundCameraCallback({ + f: 'onvif', + ff: 'failed_capture', + ip: camera.ip, + port: camera.port, + error: errorMessage + }); + s.debugLog(err) + } + }) + return responseList + } + const onWebSocketConnection = async (cn) => { + const tx = function(z){if(!z.ke){z.ke=cn.ke;};cn.emit('f',z);} + cn.on('f',(d) => { + switch(d.f){ + case'onvif': + runOnvifScanner(d,tx) + break; + } + }) + } + s.onWebSocketConnection(onWebSocketConnection) + /** + * API : FFprobe + */ + app.get(config.webPaths.apiPrefix+':auth/probe/:ke',function (req,res){ + s.auth(req.params,function(user){ + runFFprobe(req.query.url,req.params.auth,(endData) => { + s.closeJsonResponse(res,endData) + }) + },res,req); + }) +} diff --git a/libs/scheduler.js b/libs/scheduler.js index cbcc4fe1..861f906d 100644 --- a/libs/scheduler.js +++ b/libs/scheduler.js @@ -3,7 +3,11 @@ module.exports = function(s,config,lang,app,io){ //Get all Schedules s.getAllSchedules = function(callback){ s.schedules = {} - s.sqlQuery('SELECT * FROM Schedules',function(err,rows){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Schedules" + },(err,rows) => { rows.forEach(function(schedule){ s.updateSchedule(schedule) }) @@ -114,10 +118,11 @@ module.exports = function(s,config,lang,app,io){ var scheduleNames = Object.keys(s.schedules[key]) scheduleNames.forEach(function(name){ var schedule = s.schedules[key][name] - if(!schedule.active && schedule.enabled === 1 && schedule.start && schedule.details.monitorStates){ + if(schedule.enabled === 1 && schedule.start && schedule.details.monitorStates){ var timePasses = checkTimeAgainstSchedule(schedule) var daysPasses = checkDaysAgainstSchedule(schedule) - if(timePasses && daysPasses){ + var passed = timePasses && daysPasses + if(passed && !schedule.active){ schedule.active = true var monitorStates = schedule.details.monitorStates monitorStates.forEach(function(stateName){ @@ -131,7 +136,7 @@ module.exports = function(s,config,lang,app,io){ // console.log(endData) }) }) - }else{ + }else if(!passed && schedule.active){ schedule.active = false } } @@ -141,7 +146,16 @@ module.exports = function(s,config,lang,app,io){ // s.findSchedule = function(groupKey,name,callback){ //presetQueryVals = [ke, type, name] - s.sqlQuery("SELECT * FROM Schedules WHERE ke=? AND name=? LIMIT 1",[groupKey,name],function(err,schedules){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Schedules", + where: [ + ['ke','=',groupKey], + ['name','=',name], + ], + limit: 1 + },function(err,schedules) { var schedule var notFound = false if(schedules && schedules[0]){ @@ -184,22 +198,24 @@ module.exports = function(s,config,lang,app,io){ s.closeJsonResponse(res,endData) return } - var theQuery = "SELECT * FROM Schedules WHERE ke=?" - var theQueryValues = [req.params.ke] + var whereQuery = [ + ['ke','=',req.params.ke] + ] if(req.params.name){ - theQuery += ' AND name=?' - theQueryValues.push(req.params.name) + whereQuery.push(['name','=',req.params.name]) } - s.sqlQuery(theQuery,theQueryValues,function(err,schedules){ - if(schedules && schedules[0]){ - endData.ok = true - schedules.forEach(function(schedule){ - s.checkDetails(schedule) - }) - endData.schedules = schedules - }else{ - endData.msg = user.lang['Not Found'] - } + s.knexQuery({ + action: "select", + columns: "*", + table: "Schedules", + where: whereQuery, + },function(err,schedules) { + endData.ok = true + schedules = schedules || [] + schedules.forEach(function(schedule){ + s.checkDetails(schedule) + }) + endData.schedules = schedules s.closeJsonResponse(res,endData) }) }) @@ -243,7 +259,11 @@ module.exports = function(s,config,lang,app,io){ end: form.end, enabled: form.enabled } - s.sqlQuery('INSERT INTO Schedules ('+Object.keys(insertData).join(',')+') VALUES (?,?,?,?,?,?)',Object.values(insertData)) + s.knexQuery({ + action: "insert", + table: "Schedules", + insert: insertData + }) s.tx({ f: 'add_schedule', insertData: insertData, @@ -256,14 +276,23 @@ module.exports = function(s,config,lang,app,io){ details: s.stringJSON(form.details), start: form.start, end: form.end, - enabled: form.enabled, - ke: req.params.ke, - name: req.params.name + enabled: form.enabled } - s.sqlQuery('UPDATE Schedules SET details=?,start=?,end=?,enabled=? WHERE ke=? AND name=?',Object.values(insertData)) + s.knexQuery({ + action: "update", + table: "Schedules", + update: insertData, + where: [ + ['ke','=',req.params.ke], + ['name','=',req.params.name], + ] + }) s.tx({ f: 'edit_schedule', - insertData: insertData, + insertData: Object.assign(insertData,{ + ke: req.params.ke, + name: req.params.name, + }), ke: req.params.ke, name: req.params.name },'GRP_'+req.params.ke) @@ -286,7 +315,14 @@ module.exports = function(s,config,lang,app,io){ endData.msg = user.lang['Schedule Configuration Not Found'] s.closeJsonResponse(res,endData) }else{ - s.sqlQuery('DELETE FROM Schedules WHERE ke=? AND name=?',[req.params.ke,req.params.name],function(err){ + s.knexQuery({ + action: "delete", + table: "Schedules", + where : { + ke: req.params.ke, + name: req.params.name, + } + },function(err){ if(!err){ endData.msg = lang["Deleted Schedule Configuration"] endData.ok = true diff --git a/libs/shinobiHub.js b/libs/shinobiHub.js new file mode 100644 index 00000000..f809c939 --- /dev/null +++ b/libs/shinobiHub.js @@ -0,0 +1,154 @@ +var fs = require('fs') +var request = require('request') +module.exports = function(s,config,lang,app,io){ + if(config.shinobiHubEndpoint === undefined){config.shinobiHubEndpoint = `https://hub.shinobi.video/`}else{config.shinobiHubEndpoint = s.checkCorrectPathEnding(config.shinobiHubEndpoint)} + var stripUsernameAndPassword = function(string,username,password){ + if(username)string = string.split(username).join('_USERNAME_') + if(password)string = string.split(password).join('_PASSWORD_') + return string + } + var validatePostConfiguration = function(fields){ + var response = {ok: true} + var fieldsJson + if(!fields.json)fields.json = '{}' + try{ + fieldsJson = JSON.parse(fields.json) + }catch(err){ + response.ok = false + response.msg = ('Configuration is not JSON format.') + } + if(!fields.details)fields.details = '{}' + try{ + fieldsDetails = JSON.parse(fields.details) + }catch(err){ + fieldsDetails = {} + } + if( + fields.name === '' || + fields.brand === '' || + fields.json === '' + ){ + response.ok = false + response.msg = ('The form is incomplete.') + } + if(!fields.description)fields.description = '' + if(!fieldsJson.mid){ + response.ok = false + response.msg = ('The monitor configuration is incomplete.') + } + var monitorDetails = s.parseJSON(fieldsJson.details) + fieldsJson.details = monitorDetails || {} + + fieldsJson.details.auto_host = stripUsernameAndPassword(fieldsJson.details.auto_host,fieldsJson.details.muser,fieldsJson.details.mpass) + fieldsJson.path = stripUsernameAndPassword(fieldsJson.path,fieldsJson.details.muser,fieldsJson.details.mpass) + fieldsJson.details.muser = '_USERNAME_' + fieldsJson.details.mpass = '_PASSWORD_' + + response.json = JSON.stringify(fieldsJson) + response.details = JSON.stringify(fieldsDetails) + return response + } + const uploadConfiguration = (shinobiHubApiKey,type,monitorConfig,callback) => { + var validated = validatePostConfiguration({ + json: JSON.stringify(monitorConfig) + }) + if(validated.ok === true){ + request.post({ + url: `${config.shinobiHubEndpoint}api/${shinobiHubApiKey}/postConfiguration`, + form: { + "type": type, + "brand": monitorConfig.ke, + "name": monitorConfig.name, + "description": "Backup at " + (new Date()), + "json": validated.json, + "details": JSON.stringify({ + // maybe ip address? + }) + } + }, function(err,httpResponse,body){ + callback(err,s.parseJSON(body) || {ok: false}) + }) + }else{ + callback(new Error(validated.msg),{ok: false}) + } + } + const onMonitorSave = async (monitorConfig,form) => { + if(config.shinobiHubAutoBackup === true && config.shinobiHubApiKey){ + uploadConfiguration(config.shinobiHubApiKey,'cam',monitorConfig,() => { + // s.userLog({ke:monitorConfig.ke,mid:'$USER'},{type:lang['Websocket Connected'],msg:{for:lang['Superuser'],id:cn.mail,ip:cn.ip}}) + }) + } + if(s.group[monitorConfig.ke] && s.group[monitorConfig.ke].init.shinobihub === '1'){ + uploadConfiguration(s.group[monitorConfig.ke].init.shinobihub_key,'cam',monitorConfig,() => { + // s.userLog({ke:monitorConfig.ke,mid:'$USER'},{type:lang['Websocket Connected'],msg:{for:lang['Superuser'],id:cn.mail,ip:cn.ip}}) + }) + } + } + app.get([ + config.webPaths.apiPrefix + ':auth/getShinobiHubConfigurations/:ke/:type', + config.webPaths.apiPrefix + ':auth/getShinobiHubConfigurations/:ke/:type/:id' + ],function (req,res){ + s.auth(req.params,function(user){ + //query defaults : rowLimit=5, skipOver=0, explore=0 + res.setHeader('Content-Type', 'application/json'); + var shinobiHubApiKey = s.group[req.params.ke].init.shinobihub_key + if(shinobiHubApiKey){ + var queryString = [] + if(req.query){ + Object.keys(req.query).forEach((key) => { + var value = req.query[key] + queryString.push(key + '=' + value) + }) + } + request(`${config.shinobiHubEndpoint}api/${shinobiHubApiKey}/getConfiguration/${req.params.type}${req.params.id ? '/' + req.params.id : ''}${queryString.length > 0 ? '?' + queryString.join('&') : ''}`).pipe(res) + }else{ + s.closeJsonResponse(res,{ + ok: false, + msg: user.lang['No API Key'] + }) + } + },res,req) + }) + app.get([ + config.webPaths.apiPrefix + ':auth/backupMonitorsAllToShinobHub/:ke' + ],function (req,res){ + s.auth(req.params,function(user){ + //query defaults : rowLimit=5, skipOver=0, explore=0 + res.setHeader('Content-Type', 'application/json'); + var shinobiHubApiKey = s.group[req.params.ke].init.shinobihub_key + if(shinobiHubApiKey){ + if(!s.group[req.params.ke].uploadingAllMonitorsToShinobiHub){ + s.group[req.params.ke].uploadingAllMonitorsToShinobiHub = true + var current = 0; + var monitorConfigs = s.group[req.params.ke].rawMonitorConfigurations + var monitorIds = Object.keys(monitorConfigs) + var doOneUpload = () => { + if(!monitorIds[current]){ + s.group[req.params.ke].uploadingAllMonitorsToShinobiHub = false + s.closeJsonResponse(res,{ + ok: true, + }) + return; + }; + uploadConfiguration(s.group[req.params.ke].init.shinobihub_key,'cam',Object.assign(monitorConfigs[monitorIds[current]],{}),() => { + ++current + doOneUpload() + }) + } + doOneUpload() + }else{ + s.closeJsonResponse(res,{ + ok: false, + msg: lang['Already Processing'] + }) + } + }else{ + s.closeJsonResponse(res,{ + ok: false, + msg: user.lang['No API Key'] + }) + } + },res,req) + }) + s.onMonitorSave(onMonitorSave) +} diff --git a/libs/socketio.js b/libs/socketio.js index 5039a86c..3eaf58a9 100644 --- a/libs/socketio.js +++ b/libs/socketio.js @@ -1,14 +1,23 @@ -var os = require('os'); var moment = require('moment'); var execSync = require('child_process').execSync; var exec = require('child_process').exec; var spawn = require('child_process').spawn; var jsonfile = require("jsonfile"); -var onvif = require("node-onvif"); module.exports = function(s,config,lang,io){ + const { + ptzControl + } = require('./control/ptz.js')(s,config,lang) s.clientSocketConnection = {} //send data to socket client function - s.tx = function(z,y,x){if(x){return x.broadcast.to(y).emit('f',z)};io.to(y).emit('f',z);} + s.tx = function(z,y,x){ + s.onWebsocketMessageSendExtensions.forEach(function(extender){ + extender(z,y,x) + }) + if(x){ + return x.broadcast.to(y).emit('f',z) + }; + io.to(y).emit('f',z); + } s.txToDashcamUsers = function(data,groupKey){ if(s.group[groupKey] && s.group[groupKey].dashcamUsers){ Object.keys(s.group[groupKey].dashcamUsers).forEach(function(auth){ @@ -45,6 +54,87 @@ module.exports = function(s,config,lang,io){ } } + const streamConnectionAuthentication = (options,ipAddress) => { + return new Promise( (resolve,reject) => { + var isInternal = false + if(ipAddress.indexOf('localhost') > -1 || ipAddress.indexOf('127.0.0.1') > -1){ + isInternal = true + } + const baseWheres = [ + ['ke','=',options.ke], + ['uid','=',options.uid], + ] + s.knexQuery({ + action: "select", + columns: "ke,uid,auth,mail,details", + table: "Users", + where: baseWheres.concat(!isInternal ? [['auth','=',options.auth]] : []) + },(err,r) => { + if(r&&r[0]){ + resolve(r) + }else{ + s.knexQuery({ + action: "select", + columns: "*", + table: "API", + where: baseWheres.concat(!isInternal ? [['code','=',options.auth]] : []) + },(err,r) => { + if(r && r[0]){ + r = r[0] + r.details = JSON.parse(r.details) + if(r.details.auth_socket === '1'){ + s.knexQuery({ + action: "select", + columns: "ke,uid,auth,mail,details", + table: "Users", + where: [ + ['ke','=',options.ke], + ['uid','=',options.uid], + ] + },(err,r) => { + if(r && r[0]){ + resolve(r) + }else{ + reject('User not found') + } + }) + }else{ + reject('Permissions for this key do not allow authentication with Websocket') + } + }else{ + reject('Not an API key') + } + }) + } + }) + }) + } + + const validatedAndBindAuthenticationToSocketConnection = (cn,d,removeListenerOnDisconnect) => { + if(!d.channel)d.channel = 'MAIN'; + cn.ke = d.ke, + cn.uid = d.uid, + cn.auth = d.auth; + cn.channel = d.channel; + cn.removeListenerOnDisconnect = removeListenerOnDisconnect; + cn.socketVideoStream = d.id; + } + + const createStreamEmitter = (d,cn) => { + var Emitter,chunkChannel + if(!d.channel){ + Emitter = s.group[d.ke].activeMonitors[d.id].emitter + chunkChannel = 'MAIN' + }else{ + Emitter = s.group[d.ke].activeMonitors[d.id].emitterChannel[parseInt(d.channel)+config.pipeAddition] + chunkChannel = parseInt(d.channel)+config.pipeAddition + } + if(!Emitter){ + cn.disconnect();return; + } + return Emitter + } + ////socket controller io.on('connection', function (cn) { var tx; @@ -58,30 +148,14 @@ module.exports = function(s,config,lang,io){ return new Date().toISOString(); } var tx=function(z){cn.emit('data',z);} - d.failed=function(msg){ + const onFail = (msg) => { tx({f:'stop_reconnect',msg:msg,token_used:d.auth,ke:d.ke}); cn.disconnect(); } - d.success=function(r){ - r=r[0]; - var Emitter,chunkChannel - if(!d.channel){ - Emitter = s.group[d.ke].activeMonitors[d.id].emitter - chunkChannel = 'MAIN' - }else{ - Emitter = s.group[d.ke].activeMonitors[d.id].emitterChannel[parseInt(d.channel)+config.pipeAddition] - chunkChannel = parseInt(d.channel)+config.pipeAddition - } - if(!Emitter){ - cn.disconnect();return; - } - if(!d.channel)d.channel = 'MAIN'; - cn.ke=d.ke, - cn.uid=d.uid, - cn.auth=d.auth; - cn.channel=d.channel; - cn.removeListenerOnDisconnect=true; - cn.socketVideoStream=d.id; + const onSuccess = (r) => { + r = r[0]; + const Emitter = createStreamEmitter(d,cn) + validatedAndBindAuthenticationToSocketConnection(cn,d,true) var contentWriter cn.closeSocketVideoStream = function(){ Emitter.removeListener('data', contentWriter); @@ -92,33 +166,9 @@ module.exports = function(s,config,lang,io){ } //check if auth key is user's temporary session key if(s.group[d.ke]&&s.group[d.ke].users&&s.group[d.ke].users[d.auth]){ - d.success(s.group[d.ke].users[d.auth]); + onSuccess(s.group[d.ke].users[d.auth]); }else{ - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND auth=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - d.success(r) - }else{ - s.sqlQuery('SELECT * FROM API WHERE ke=? AND code=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - r=r[0] - r.details=JSON.parse(r.details) - if(r.details.auth_socket==='1'){ - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND uid=?',[r.ke,r.uid],function(err,r) { - if(r&&r[0]){ - d.success(r) - }else{ - d.failed('User not found') - } - }) - }else{ - d.failed('Permissions for this key do not allow authentication with Websocket') - } - }else{ - d.failed('Not an API key') - } - }) - } - }) + streamConnectionAuthentication(d,cn.ip).then(onSuccess).catch(onFail) } }) //unique Base64 socket stream @@ -131,30 +181,14 @@ module.exports = function(s,config,lang,io){ return new Date().toISOString(); } var tx=function(z){cn.emit('data',z);} - d.failed=function(msg){ + const onFail = (msg) => { tx({f:'stop_reconnect',msg:msg,token_used:d.auth,ke:d.ke}); cn.disconnect(); } - d.success=function(r){ - r=r[0]; - var Emitter,chunkChannel - if(!d.channel){ - Emitter = s.group[d.ke].activeMonitors[d.id].emitter - chunkChannel = 'MAIN' - }else{ - Emitter = s.group[d.ke].activeMonitors[d.id].emitterChannel[parseInt(d.channel)+config.pipeAddition] - chunkChannel = parseInt(d.channel)+config.pipeAddition - } - if(!Emitter){ - cn.disconnect();return; - } - if(!d.channel)d.channel = 'MAIN'; - cn.ke=d.ke, - cn.uid=d.uid, - cn.auth=d.auth; - cn.channel=d.channel; - cn.removeListenerOnDisconnect=true; - cn.socketVideoStream=d.id; + const onSuccess = (r) => { + r = r[0]; + const Emitter = createStreamEmitter(d,cn) + validatedAndBindAuthenticationToSocketConnection(cn,d,true) var contentWriter cn.closeSocketVideoStream = function(){ Emitter.removeListener('data', contentWriter); @@ -165,33 +199,9 @@ module.exports = function(s,config,lang,io){ } //check if auth key is user's temporary session key if(s.group[d.ke]&&s.group[d.ke].users&&s.group[d.ke].users[d.auth]){ - d.success(s.group[d.ke].users[d.auth]); + onSuccess(s.group[d.ke].users[d.auth]); }else{ - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND auth=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - d.success(r) - }else{ - s.sqlQuery('SELECT * FROM API WHERE ke=? AND code=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - r=r[0] - r.details=JSON.parse(r.details) - if(r.details.auth_socket==='1'){ - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND uid=?',[r.ke,r.uid],function(err,r) { - if(r&&r[0]){ - d.success(r) - }else{ - d.failed('User not found') - } - }) - }else{ - d.failed('Permissions for this key do not allow authentication with Websocket') - } - }else{ - d.failed('Not an API key') - } - }) - } - }) + streamConnectionAuthentication(d,cn.ip).then(onSuccess).catch(onFail) } }) //unique FLV socket stream @@ -204,30 +214,14 @@ module.exports = function(s,config,lang,io){ return new Date().toISOString(); } var tx=function(z){cn.emit('data',z);} - d.failed=function(msg){ + const onFail = (msg) => { tx({f:'stop_reconnect',msg:msg,token_used:d.auth,ke:d.ke}); cn.disconnect(); } - d.success=function(r){ + const onSuccess = (r) => { r=r[0]; - var Emitter,chunkChannel - if(!d.channel){ - Emitter = s.group[d.ke].activeMonitors[d.id].emitter - chunkChannel = 'MAIN' - }else{ - Emitter = s.group[d.ke].activeMonitors[d.id].emitterChannel[parseInt(d.channel)+config.pipeAddition] - chunkChannel = parseInt(d.channel)+config.pipeAddition - } - if(!Emitter){ - cn.disconnect();return; - } - if(!d.channel)d.channel = 'MAIN'; - cn.ke=d.ke, - cn.uid=d.uid, - cn.auth=d.auth; - cn.channel=d.channel; - cn.removeListenerOnDisconnect=true; - cn.socketVideoStream=d.id; + const Emitter = createStreamEmitter(d,cn) + validatedAndBindAuthenticationToSocketConnection(cn,d,true) var contentWriter cn.closeSocketVideoStream = function(){ Emitter.removeListener('data', contentWriter); @@ -238,37 +232,13 @@ module.exports = function(s,config,lang,io){ }) } if(s.group[d.ke] && s.group[d.ke].users && s.group[d.ke].users[d.auth]){ - d.success(s.group[d.ke].users[d.auth]); + onSuccess(s.group[d.ke].users[d.auth]); }else{ - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND auth=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - d.success(r) - }else{ - s.sqlQuery('SELECT * FROM API WHERE ke=? AND code=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - r=r[0] - r.details=JSON.parse(r.details) - if(r.details.auth_socket==='1'){ - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND uid=?',[r.ke,r.uid],function(err,r) { - if(r&&r[0]){ - d.success(r) - }else{ - d.failed('User not found') - } - }) - }else{ - d.failed('Permissions for this key do not allow authentication with Websocket') - } - }else{ - d.failed('Not an API key') - } - }) - } - }) + streamConnectionAuthentication(d,cn.ip).then(onSuccess).catch(onFail) } }) //unique MP4 socket stream - cn.on('MP4',function(d){ + cn.on('MP4',function(d, cb){ if(!s.group[d.ke]||!s.group[d.ke].activeMonitors||!s.group[d.ke].activeMonitors[d.id]){ cn.disconnect();return; } @@ -277,29 +247,13 @@ module.exports = function(s,config,lang,io){ return new Date().toISOString(); } var tx=function(z){cn.emit('data',z);} - d.failed=function(msg){ + const onFail = (msg) => { tx({f:'stop_reconnect',msg:msg,token_used:d.auth,ke:d.ke}); cn.disconnect(); } - d.success=function(r){ - r=r[0]; - var Emitter,chunkChannel - if(!d.channel){ - Emitter = s.group[d.ke].activeMonitors[d.id].emitter - chunkChannel = 'MAIN' - }else{ - Emitter = s.group[d.ke].activeMonitors[d.id].emitterChannel[parseInt(d.channel)+config.pipeAddition] - chunkChannel = parseInt(d.channel)+config.pipeAddition - } - if(!Emitter){ - cn.disconnect();return; - } - if(!d.channel)d.channel = 'MAIN'; - cn.ke=d.ke, - cn.uid=d.uid, - cn.auth=d.auth; - cn.channel=d.channel; - cn.socketVideoStream=d.id; + const onSuccess = (r) => { + r = r[0]; + validatedAndBindAuthenticationToSocketConnection(cn,d) var mp4frag = s.group[d.ke].activeMonitors[d.id].mp4frag[d.channel]; var onInitialized = () => { cn.emit('mime', mp4frag.mime); @@ -315,6 +269,7 @@ module.exports = function(s,config,lang,io){ mp4frag.removeListener('initialized', onInitialized) } } + if (cb) cb(null, true); cn.on('MP4Command',function(msg){ switch (msg) { case 'mime' ://client is requesting mime @@ -358,33 +313,9 @@ module.exports = function(s,config,lang,io){ }) } if(s.group[d.ke]&&s.group[d.ke].users&&s.group[d.ke].users[d.auth]){ - d.success(s.group[d.ke].users[d.auth]); + onSuccess(s.group[d.ke].users[d.auth]); }else{ - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND auth=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - d.success(r) - }else{ - s.sqlQuery('SELECT * FROM API WHERE ke=? AND code=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - r=r[0] - r.details=JSON.parse(r.details) - if(r.details.auth_socket==='1'){ - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND uid=?',[r.ke,r.uid],function(err,r) { - if(r&&r[0]){ - d.success(r) - }else{ - d.failed('User not found') - } - }) - }else{ - d.failed('Permissions for this key do not allow authentication with Websocket') - } - }else{ - d.failed('Not an API key') - } - }) - } - }) + streamConnectionAuthentication(d,cn.ip).then(onSuccess).catch(onFail) } }) //main socket control functions @@ -392,9 +323,12 @@ module.exports = function(s,config,lang,io){ if(!cn.ke&&d.f==='init'){//socket login cn.ip=cn.request.connection.remoteAddress; tx=function(z){if(!z.ke){z.ke=cn.ke;};cn.emit('f',z);} - d.failed=function(){tx({ok:false,msg:'Not Authorized',token_used:d.auth,ke:d.ke});cn.disconnect();} - d.success=function(r){ - r=r[0];cn.join('GRP_'+d.ke);cn.join('CPU'); + const onFail = (msg) => { + tx({ok:false,msg:'Not Authorized',token_used:d.auth,ke:d.ke});cn.disconnect(); + } + const onSuccess = (r) => { + r = r[0]; + cn.join('GRP_'+d.ke);cn.join('CPU'); cn.ke=d.ke, cn.uid=d.uid, cn.auth=d.auth; @@ -423,9 +357,17 @@ module.exports = function(s,config,lang,io){ } tx({f:'users_online',users:s.group[d.ke].users}) s.tx({f:'user_status_change',ke:d.ke,uid:cn.uid,status:1,user:s.group[d.ke].users[d.auth]},'GRP_'+d.ke) - s.sendDiskUsedAmountToClients(d) + s.sendDiskUsedAmountToClients(d.ke) s.loadGroupApps(d) - s.sqlQuery('SELECT * FROM API WHERE ke=? AND uid=?',[d.ke,d.uid],function(err,rrr) { + s.knexQuery({ + action: "select", + columns: "*", + table: "API", + where: [ + ['ke','=',d.ke], + ['uid','=',d.uid], + ] + },function(err,rrr) { tx({ f:'init_success', users:s.group[d.ke].vid, @@ -437,46 +379,24 @@ module.exports = function(s,config,lang,io){ } }) try{ - s.sqlQuery('SELECT * FROM Monitors WHERE ke=?', [d.ke], function(err,r) { - if(r && r[0]){ - r.forEach(function(monitor){ - s.cameraSendSnapshot({mid:monitor.mid,ke:monitor.ke,mon:monitor},{useIcon: true}) - }) - } + Object.values(s.group[d.ke].rawMonitorConfigurations).forEach((monitor) => { + s.cameraSendSnapshot({ + mid: monitor.mid, + ke: monitor.ke, + mon: monitor + },{ + useIcon: true + }) }) }catch(err){ - console.log(err) + s.debugLog(err) } }) s.onSocketAuthenticationExtensions.forEach(function(extender){ extender(r,cn,d,tx) }) } - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND auth=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - d.success(r) - }else{ - s.sqlQuery('SELECT * FROM API WHERE ke=? AND code=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - r=r[0] - r.details=JSON.parse(r.details) - if(r.details.auth_socket==='1'){ - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND uid=?',[r.ke,r.uid],function(err,r) { - if(r&&r[0]){ - d.success(r) - }else{ - d.failed() - } - }) - }else{ - d.failed() - } - }else{ - d.failed() - } - }) - } - }) + streamConnectionAuthentication(d,cn.ip).then(onSuccess).catch(onFail) return; } if((d.id||d.uid||d.mid)&&cn.ke){ @@ -484,83 +404,79 @@ module.exports = function(s,config,lang,io){ switch(d.f){ case'monitorOrder': if(d.monitorOrder && d.monitorOrder instanceof Object){ - s.sqlQuery('SELECT details FROM Users WHERE uid=? AND ke=?',[cn.uid,cn.ke],function(err,r){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Users", + where: [ + ['ke','=',cn.ke], + ['uid','=',cn.uid] + ] + },(err,r) => { if(r && r[0]){ details = JSON.parse(r[0].details) details.monitorOrder = d.monitorOrder - s.sqlQuery('UPDATE Users SET details=? WHERE uid=? AND ke=?',[s.s(details),cn.uid,cn.ke]) + s.knexQuery({ + action: "update", + table: "Users", + update: { + details: s.s(details) + }, + where: [ + ['ke','=',cn.ke], + ['uid','=',cn.uid] + ] + }) } }) } break; case'monitorListOrder': if(d.monitorListOrder && d.monitorListOrder instanceof Object){ - s.sqlQuery('SELECT details FROM Users WHERE uid=? AND ke=?',[cn.uid,cn.ke],function(err,r){ + s.knexQuery({ + action: "select", + columns: "details", + table: "Users", + where: [ + ['ke','=',cn.ke], + ['uid','=',cn.uid], + ] + },(err,r) => { if(r && r[0]){ details = JSON.parse(r[0].details) details.monitorListOrder = d.monitorListOrder - s.sqlQuery('UPDATE Users SET details=? WHERE uid=? AND ke=?',[s.s(details),cn.uid,cn.ke]) + s.knexQuery({ + action: "update", + table: "Users", + update: { + details: s.s(details) + }, + where: [ + ['ke','=',cn.ke], + ['uid','=',cn.uid], + ] + }) } }) } break; - case'update': - if(!config.updateKey){ - tx({error:lang.updateKeyText1}); - return; - } - if(d.key===config.updateKey){ - exec('chmod +x '+s.mainDirectory+'/UPDATE.sh&&'+s.mainDirectory+'/UPDATE.sh',{detached: true}) - }else{ - tx({error:lang.updateKeyText2}); - } - break; - case'cron': - if(s.group[cn.ke]&&s.group[cn.ke].users[cn.auth].details&&!s.group[cn.ke].users[cn.auth].details.sub){ - s.tx({f:d.ff},s.cron.id) - } - break; - case'api': - switch(d.ff){ - case'delete': - d.set=[],d.ar=[]; - d.form.ke=cn.ke;d.form.uid=cn.uid;delete(d.form.ip); - if(!d.form.code){tx({f:'form_incomplete',form:'APIs',uid:cn.uid});return} - d.for=Object.keys(d.form); - d.for.forEach(function(v){ - d.set.push(v+'=?'),d.ar.push(d.form[v]); - }); - s.sqlQuery('DELETE FROM API WHERE '+d.set.join(' AND '),d.ar,function(err,r){ - if(!err){ - tx({f:'api_key_deleted',form:d.form,uid:cn.uid}); - delete(s.api[d.form.code]); - }else{ - s.systemLog('API Delete Error : '+e.ke+' : '+' : '+e.mid,err) - } - }) - break; - case'add': - d.set=[],d.qu=[],d.ar=[]; - d.form.ke=cn.ke,d.form.uid=cn.uid,d.form.code=s.gid(30); - d.for=Object.keys(d.form); - d.for.forEach(function(v){ - d.set.push(v),d.qu.push('?'),d.ar.push(d.form[v]); - }); - s.sqlQuery('INSERT INTO API ('+d.set.join(',')+') VALUES ('+d.qu.join(',')+')',d.ar,function(err,r){ - d.form.time=s.formattedTime(new Date,'YYYY-DD-MM HH:mm:ss'); - if(!err){tx({f:'api_key_added',form:d.form,uid:cn.uid});}else{s.systemLog(err)} - }); - break; - } - break; case'settings': switch(d.ff){ case'filters': switch(d.fff){ case'save':case'delete': - s.sqlQuery('SELECT details FROM Users WHERE ke=? AND uid=?',[d.ke,d.uid],function(err,r){ - if(r&&r[0]){ - r=r[0]; + s.knexQuery({ + action: "select", + columns: "details", + table: "Users", + where: [ + ['ke','=',cn.ke], + ['uid','=',cn.uid], + ], + limit: 1 + },(err,r) => { + if(r && r[0]){ + r = r[0]; d.d=JSON.parse(r.details); if(d.form.id===''){d.form.id=s.gid(5)} if(!d.d.filters)d.d.filters={}; @@ -570,9 +486,19 @@ module.exports = function(s,config,lang,io){ }else{ delete(d.d.filters[d.form.id]); } - s.sqlQuery('UPDATE Users SET details=? WHERE ke=? AND uid=?',[JSON.stringify(d.d),d.ke,d.uid],function(err,r){ + s.knexQuery({ + action: "update", + table: "Users", + update: { + details: JSON.stringify(d.d) + }, + where: [ + ['ke','=',cn.ke], + ['uid','=',cn.uid], + ] + },(err) => { tx({f:'filters_change',uid:d.uid,ke:d.ke,filters:d.d.filters}); - }); + }) } }) break; @@ -600,48 +526,52 @@ module.exports = function(s,config,lang,io){ if(!d.eventEndDate&&d.endDate){ d.eventEndDate = s.stringToSqlTime(d.endDate) } - var monitorQuery = '' - var monitorValues = [] + var monitorRestrictions = [] var permissions = s.group[d.ke].users[cn.auth].details; if(!d.mid){ - if(permissions.sub&&permissions.monitors&&permissions.allmonitors!=='1'){ - try{permissions.monitors=JSON.parse(permissions.monitors);}catch(er){} - var or = []; - permissions.monitors.forEach(function(v,n){ - or.push('mid=?'); - monitorValues.push(v) - }) - monitorQuery += ' AND ('+or.join(' OR ')+')' - } - }else if(!permissions.sub||permissions.allmonitors!=='0'||permissions.monitors.indexOf(d.mid)>-1){ - monitorQuery += ' and mid=?'; - monitorValues.push(d.mid) - } - var getEvents = function(callback){ - var eventQuery = 'SELECT * FROM Events WHERE ke=?'; - var eventQueryValues = [cn.ke]; - if(d.eventStartDate&&d.eventStartDate!==''){ - if(d.eventEndDate&&d.eventEndDate!==''){ - eventQuery+=' AND `time` >= ? AND `time` <= ?'; - eventQueryValues.push(d.eventStartDate) - eventQueryValues.push(d.eventEndDate) - }else{ - eventQuery+=' AND `time` >= ?'; - eventQueryValues.push(d.eventStartDate) + if(permissions.sub && permissions.monitors && permissions.allmonitors !== '1'){ + try{ + permissions.monitors = JSON.parse(permissions.monitors); + permissions.monitors.forEach(function(v,n){ + if(n === 0){ + monitorRestrictions.push(['mid','=',v]) + }else{ + monitorRestrictions.push(['or','mid','=',v]) + } + }) + }catch(er){ + console.log(er) } } - if(monitorValues.length>0){ - eventQuery += monitorQuery; - eventQueryValues = eventQueryValues.concat(monitorValues); + }else if(!permissions.sub||permissions.allmonitors!=='0'||permissions.monitors.indexOf(d.mid)>-1){ + monitorRestrictions.push(['mid','=',d.mid]) + } + var getEvents = function(callback){ + var eventWhereQuery = [ + ['ke','=',cn.ke], + ] + if(d.eventStartDate&&d.eventStartDate!==''){ + if(d.eventEndDate&&d.eventEndDate!==''){ + eventWhereQuery.push(['time','>=',d.eventStartDate]) + eventWhereQuery.push(['time','<=',d.eventEndDate]) + }else{ + eventWhereQuery.push(['time','>=',d.eventStartDate]) + } } - eventQuery+=' ORDER BY `time` DESC LIMIT '+d.eventLimit+''; - s.sqlQuery(eventQuery,eventQueryValues,function(err,r){ + if(monitorRestrictions.length > 0){ + eventWhereQuery.push(monitorRestrictions) + } + s.knexQuery({ + action: "select", + columns: "*", + table: "Events", + where: eventWhereQuery, + orderBy: ['time','desc'], + limit: d.eventLimit + },(err,r) => { if(err){ - console.log(eventQuery) - console.error('LINE 2428',err) - setTimeout(function(){ - getEvents(callback) - },2000) + console.error(err) + callback([]) }else{ if(!r){r=[]} r.forEach(function(v,n){ @@ -653,7 +583,6 @@ module.exports = function(s,config,lang,io){ } if(!d.videoLimit&&d.limit){ d.videoLimit=d.limit - eventQuery.push() } if(!d.videoStartDate&&d.startDate){ d.videoStartDate = s.stringToSqlTime(d.startDate) @@ -662,9 +591,10 @@ module.exports = function(s,config,lang,io){ d.videoEndDate = s.stringToSqlTime(d.endDate) } var getVideos = function(callback){ - var videoQuery='SELECT * FROM Videos WHERE ke=?'; - var videoQueryValues=[cn.ke]; - if(d.videoStartDate||d.videoEndDate){ + var videoWhereQuery = [ + ['ke','=',cn.ke], + ] + if(d.videoStartDate || d.videoEndDate){ if(!d.videoStartDateOperator||d.videoStartDateOperator==''){ d.videoStartDateOperator='>=' } @@ -672,38 +602,33 @@ module.exports = function(s,config,lang,io){ d.videoEndDateOperator='<=' } switch(true){ - case(d.videoStartDate&&d.videoStartDate!==''&&d.videoEndDate&&d.videoEndDate!==''): - videoQuery+=' AND `time` '+d.videoStartDateOperator+' ? AND `end` '+d.videoEndDateOperator+' ?'; - videoQueryValues.push(d.videoStartDate) - videoQueryValues.push(d.videoEndDate) + case(d.videoStartDate && d.videoStartDate !== '' && d.videoEndDate && d.videoEndDate !== ''): + videoWhereQuery.push(['time',d.videoStartDateOperator,d.videoStartDate]) + videoWhereQuery.push(['end',d.videoEndDateOperator,d.videoEndDate]) break; - case(d.videoStartDate&&d.videoStartDate!==''): - videoQuery+=' AND `time` '+d.videoStartDateOperator+' ?'; - videoQueryValues.push(d.videoStartDate) + case(d.videoStartDate && d.videoStartDate !== ''): + videoWhereQuery.push(['time',d.videoStartDateOperator,d.videoStartDate]) break; - case(d.videoEndDate&&d.videoEndDate!==''): - videoQuery+=' AND `end` '+d.videoEndDateOperator+' ?'; - videoQueryValues.push(d.videoEndDate) + case(d.videoEndDate && d.videoEndDate !== ''): + videoWhereQuery.push(['end',d.videoEndDateOperator,d.videoEndDate]) break; } } - if(monitorValues.length>0){ - videoQuery += monitorQuery; - videoQueryValues = videoQueryValues.concat(monitorValues); + if(monitorRestrictions.length > 0){ + videoWhereQuery.push(monitorRestrictions) } - videoQuery+=' ORDER BY `time` DESC'; - if(!d.videoLimit||d.videoLimit==''){ - d.videoLimit='100' - } - if(d.videoLimit!=='0'){ - videoQuery+=' LIMIT '+d.videoLimit - } - s.sqlQuery(videoQuery,videoQueryValues,function(err,r){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Videos", + where: videoWhereQuery, + orderBy: ['time','desc'], + limit: d.videoLimit || '100' + },(err,r) => { if(err){ - console.log(videoQuery) - console.error('LINE 2416',err) + console.error(err) setTimeout(function(){ - getVideos(callback) + callback({total:0,limit:d.videoLimit,videos:[]}) },2000) }else{ s.buildVideoLinks(r,{ @@ -727,8 +652,9 @@ module.exports = function(s,config,lang,io){ } break; case'control': - s.cameraControl(d,function(resp){ - tx({f:'control',response:resp}) + ptzControl(d,function(msg){ + s.userLog(d,msg) + tx({f:'control',response:msg}) }) break; case'jpeg_off': @@ -770,7 +696,16 @@ module.exports = function(s,config,lang,io){ tx({f:'monitor_watch_off',ke:d.ke,id:d.id,cnid:cn.id}) break; case'start':case'stop': - s.sqlQuery('SELECT * FROM Monitors WHERE ke=? AND mid=?',[cn.ke,d.id],function(err,r) { + s.knexQuery({ + action: "select", + columns: "*", + table: "Monitors", + where: [ + ['ke','=',cn.ke], + ['mid','=',d.id], + ], + limit: 1 + },(err,r) => { if(r && r[0]){ r = r[0] s.camera(d.ff,{type:r.type,url:s.buildMonitorUrl(r),id:d.id,mode:d.ff,ke:cn.ke}); @@ -779,134 +714,6 @@ module.exports = function(s,config,lang,io){ break; } break; - // case'video': - // switch(d.ff){ - // case'fix': - // s.video('fix',d) - // break; - // } - // break; - case'ffprobe': - if(s.group[cn.ke].users[cn.auth]){ - switch(d.ff){ - case'stop': - exec('kill -9 '+s.group[cn.ke].users[cn.auth].ffprobe.pid,{detatched: true}) - break; - default: - if(s.group[cn.ke].users[cn.auth].ffprobe){ - return - } - s.group[cn.ke].users[cn.auth].ffprobe=1; - tx({f:'ffprobe_start'}) - exec('ffprobe '+('-v quiet -print_format json -show_format -show_streams '+d.query),function(err,data){ - tx({f:'ffprobe_data',data:data.toString('utf8')}) - delete(s.group[cn.ke].users[cn.auth].ffprobe) - tx({f:'ffprobe_stop'}) - }) - //auto kill in 30 seconds - setTimeout(function(){ - exec('kill -9 '+d.pid,{detached: true}) - },30000) - break; - } - } - break; - case'onvif': - d.ip=d.ip.replace(/ /g,''); - d.port=d.port.replace(/ /g,''); - if(d.ip===''){ - var interfaces = os.networkInterfaces(); - var addresses = []; - for (var k in interfaces) { - for (var k2 in interfaces[k]) { - var address = interfaces[k][k2]; - if (address.family === 'IPv4' && !address.internal) { - addresses.push(address.address); - } - } - } - d.arr=[] - addresses.forEach(function(v){ - if(v.indexOf('0.0.0')>-1){return false} - v=v.split('.'); - delete(v[3]); - v=v.join('.'); - d.arr.push(v+'1-'+v+'254') - }) - d.ip=d.arr.join(',') - } - if(d.port===''){ - d.port='80,8080,8000,7575,8081,554' - } - d.ip.split(',').forEach(function(v){ - if(v.indexOf('-')>-1){ - v=v.split('-'); - d.IP_RANGE_START = v[0], - d.IP_RANGE_END = v[1]; - }else{ - d.IP_RANGE_START = v; - d.IP_RANGE_END = v; - } - if(!d.IP_LIST){ - d.IP_LIST = s.ipRange(d.IP_RANGE_START,d.IP_RANGE_END); - }else{ - d.IP_LIST=d.IP_LIST.concat(s.ipRange(d.IP_RANGE_START,d.IP_RANGE_END)) - } - //check port - if(d.port.indexOf('-')>-1){ - d.port=d.port.split('-'); - d.PORT_RANGE_START = d.port[0]; - d.PORT_RANGE_END = d.port[1]; - d.PORT_LIST = s.portRange(d.PORT_RANGE_START,d.PORT_RANGE_END); - }else{ - d.PORT_LIST=d.port.split(',') - } - //check user name and pass - d.USERNAME=''; - if(d.user){ - d.USERNAME = d.user - } - d.PASSWORD=''; - if(d.pass){ - d.PASSWORD = d.pass - } - }) - d.cams=[] - d.IP_LIST.forEach(function(ip_entry,n) { - d.PORT_LIST.forEach(function(port_entry,nn) { - var device = new onvif.OnvifDevice({ - xaddr : 'http://' + ip_entry + ':' + port_entry + '/onvif/device_service', - user : d.USERNAME, - pass : d.PASSWORD - }) - device.init().then((info) => { - var data = { - f : 'onvif', - ip : ip_entry, - port : port_entry, - info : info - } - device.services.device.getSystemDateAndTime().then((date) => { - data.date = date - device.services.media.getStreamUri({ - ProfileToken : device.current_profile.token, - Protocol : 'RTSP' - }).then((stream) => { - data.uri = stream.data.GetStreamUriResponse.MediaUri.Uri - tx(data) - }).catch((error) => { - // console.log(error) - }); - }).catch((error) => { - // console.log(error) - }); - }).catch(function(error){ - // console.log(error) - }) - }); - }); - // tx({f:'onvif_end'}) - break; } }catch(er){ s.systemLog('ERROR CATCH 1',er) @@ -941,15 +748,19 @@ module.exports = function(s,config,lang,io){ case'logs': switch(d.ff){ case'delete': - //config.webPaths.superApiPrefix+':auth/logs/delete' - s.sqlQuery('DELETE FROM Logs WHERE ke=?',[d.ke]) + s.knexQuery({ + action: "delete", + table: "Logs", + where: { + ke: d.ke, + } + }) break; } break; case'system': switch(d.ff){ case'update': - //config.webPaths.superApiPrefix+':auth/update' s.ffmpegKill() s.systemLog('Shinobi ordered to update',{ by:cn.mail, @@ -988,145 +799,25 @@ module.exports = function(s,config,lang,io){ break; } break; - case'accounts': - switch(d.ff){ - case'saveSuper': - var currentSuperUserList = jsonfile.readFileSync(s.location.super) - var currentSuperUser = {} - var currentSuperUserPosition = -1 - //find this user in current list - currentSuperUserList.forEach(function(user,pos){ - if(user.mail === cn.mail){ - currentSuperUser = user - currentSuperUserPosition = pos - } - }) - var logDetails = { - by : cn.mail, - ip : cn.ip - } - //check if pass and pass_again match, if not remove password - if(d.form.pass !== '' && d.form.pass === d.form.pass_again){ - d.form.pass = s.createHash(d.form.pass) - }else{ - delete(d.form.pass) - } - //delete pass_again from object - delete(d.form.pass_again) - //set new values - currentSuperUser = Object.assign(currentSuperUser,d.form) - //reset email and log change of email - if(d.form.mail !== cn.mail){ - logDetails.newEmail = d.form.mail - logDetails.oldEmail = cn.mail + '' - cn.mail = d.form.mail - } - //log this change - s.systemLog('super.json Modified',logDetails) - //modify or add account in temporary master list - if(currentSuperUserList[currentSuperUserPosition]){ - currentSuperUserList[currentSuperUserPosition] = currentSuperUser - }else{ - currentSuperUserList.push(currentSuperUser) - } - //update master list in system - jsonfile.writeFile(s.location.super,currentSuperUserList,{spaces: 2},function(){ - s.tx({f:'save_preferences'},cn.id) - }) - break; - case'register': - if(d.form.mail!==''&&d.form.pass!==''){ - if(d.form.pass===d.form.password_again){ - s.sqlQuery('SELECT * FROM Users WHERE mail=?',[d.form.mail],function(err,r) { - if(r&&r[0]){ - //found address already exists - d.msg=lang['Email address is in use.']; - s.tx({f:'error',ff:'account_register',msg:d.msg},cn.id) - }else{ - //create new - //user id - d.form.uid=s.gid(); - //check to see if custom key set - if(!d.form.ke||d.form.ke===''){ - d.form.ke=s.gid() - }else{ - d.form.ke = d.form.ke.replace(/[`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, '') - } - //write user to db - s.sqlQuery('INSERT INTO Users (ke,uid,mail,pass,details) VALUES (?,?,?,?,?)',[d.form.ke,d.form.uid,d.form.mail,s.createHash(d.form.pass),d.form.details]) - s.tx({f:'add_account',details:d.form.details,ke:d.form.ke,uid:d.form.uid,mail:d.form.mail},'$'); - //init user - s.loadGroup(d.form) - } - }) - }else{ - d.msg=lang["Passwords Don't Match"]; - } - }else{ - d.msg=lang['Fields cannot be empty']; - } - if(d.msg){ - s.tx({f:'error',ff:'account_register',msg:d.msg},cn.id) - } - break; - case'edit': - s.sqlQuery('SELECT * FROM Users WHERE mail=?',[d.account.mail],function(err,r) { - if(r && r[0]){ - r = r[0] - var details = JSON.parse(r.details) - if(d.form.pass&&d.form.pass!==''){ - if(d.form.pass===d.form.password_again){ - d.form.pass=s.createHash(d.form.pass); - }else{ - s.tx({f:'error',ff:'edit_account',msg:lang["Passwords Don't Match"]},cn.id) - return - } - }else{ - delete(d.form.pass); - } - delete(d.form.password_again); - d.keys=Object.keys(d.form); - d.set=[]; - d.values=[]; - d.keys.forEach(function(v,n){ - if(d.set==='ke'||d.set==='password_again'||!d.form[v]){return} - d.set.push(v+'=?') - if(v === 'details'){ - d.form[v] = JSON.stringify(Object.assign(details,JSON.parse(d.form[v]))) - } - d.values.push(d.form[v]) - }) - d.values.push(d.account.mail) - s.sqlQuery('UPDATE Users SET '+d.set.join(',')+' WHERE mail=?',d.values,function(err,r) { - if(err){ - console.log(err) - s.tx({f:'error',ff:'edit_account',msg:lang.AccountEditText1},cn.id) - return - } - s.tx({f:'edit_account',form:d.form,ke:d.account.ke,uid:d.account.uid},'$'); - delete(s.group[d.account.ke].init); - s.loadGroupApps(d.account) - }) - } - }) - break; - case'delete': - s.sqlQuery('DELETE FROM Users WHERE uid=? AND ke=? AND mail=?',[d.account.uid,d.account.ke,d.account.mail]) - s.sqlQuery('DELETE FROM API WHERE uid=? AND ke=?',[d.account.uid,d.account.ke]) - s.tx({f:'delete_account',ke:d.account.ke,uid:d.account.uid,mail:d.account.mail},'$'); - break; - } - break; } } } }) // admin page socket functions cn.on('a',function(d){ - if(!cn.init&&d.f=='init'){ - s.sqlQuery('SELECT * FROM Users WHERE auth=? AND uid=?',[d.auth,d.uid],function(err,r){ - if(r&&r[0]){ - r=r[0]; + if(!cn.init && d.f == 'init'){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Users", + where: [ + ['auth','=',d.auth], + ['uid','=',d.uid], + ], + limit: 1 + },(err,r) => { + if(r && r[0]){ + r = r[0]; if(!s.group[d.ke]){s.group[d.ke]={users:{}}} if(!s.group[d.ke].users[d.auth]){s.group[d.ke].users[d.auth]={cnid:cn.id,uid:d.uid,ke:d.ke,auth:d.auth}} try{s.group[d.ke].users[d.auth].details=JSON.parse(r.details)}catch(er){} @@ -1136,65 +827,35 @@ module.exports = function(s,config,lang,io){ cn.auth=d.auth; cn.init='admin'; }else{ - cn.disconnect(); + cn.disconnect() } }) }else{ - s.auth({auth:d.auth,ke:d.ke,id:d.id,ip:cn.request.connection.remoteAddress},function(user){ - if(!user.details.sub){ - switch(d.f){ - case'accounts': - switch(d.ff){ - case'edit': - d.keys=Object.keys(d.form); - d.condition=[]; - d.value=[]; - d.keys.forEach(function(v){ - d.condition.push(v+'=?') - d.value.push(d.form[v]) - }) - d.value=d.value.concat([d.ke,d.$uid]) - s.sqlQuery("UPDATE Users SET "+d.condition.join(',')+" WHERE ke=? AND uid=?",d.value) - s.tx({f:'edit_sub_account',ke:d.ke,uid:d.$uid,mail:d.mail,form:d.form},'ADM_'+d.ke); - s.sqlQuery("SELECT * FROM API WHERE ke=? AND uid=?",[d.ke,d.$uid],function(err,rows){ - if(rows && rows[0]){ - rows.forEach(function(row){ - delete(s.api[row.code]) - }) - } - }) - break; - case'delete': - s.sqlQuery('DELETE FROM Users WHERE uid=? AND ke=? AND mail=?',[d.$uid,d.ke,d.mail]) - s.sqlQuery("SELECT * FROM API WHERE ke=? AND uid=?",[d.ke,d.$uid],function(err,rows){ - if(rows && rows[0]){ - rows.forEach(function(row){ - delete(s.api[row.code]) - }) - s.sqlQuery('DELETE FROM API WHERE uid=? AND ke=?',[d.$uid,d.ke]) - } - }) - s.tx({f:'delete_sub_account',ke:d.ke,uid:d.$uid,mail:d.mail},'ADM_'+d.ke); - break; - } - break; - } - } - }) + cn.disconnect() } }) //functions for webcam recorder cn.on('r',function(d){ if(!cn.ke&&d.f==='init'){ - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND auth=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - r=r[0] + s.knexQuery({ + action: "select", + columns: "ke,uid,auth,mail,details", + table: "Users", + where: [ + ['ke','=',d.ke], + ['auth','=',d.auth], + ['uid','=',d.uid], + ], + limit: 1 + },(err,r) => { + if(r && r[0]){ + r = r[0] cn.ke=d.ke,cn.uid=d.uid,cn.auth=d.auth; if(!s.group[d.ke])s.group[d.ke]={}; if(!s.group[d.ke].users)s.group[d.ke].users={}; if(!s.group[d.ke].dashcamUsers)s.group[d.ke].dashcamUsers={}; s.group[d.ke].users[d.auth]={ - cnid:cn.id, + cnid: cn.id, ke : d.ke, uid:r.uid, mail:r.mail, @@ -1225,6 +886,9 @@ module.exports = function(s,config,lang,io){ if(s.group[d.ke] && s.group[d.ke].activeMonitors[d.mid]){ if(s.group[d.ke].activeMonitors[d.mid].allowStdinWrite === true){ switch(d.f){ + case'monitor_b64': + console.log(d) + break; case'monitor_chunk': if(s.group[d.ke].activeMonitors[d.mid].isStarted !== true || !s.group[d.ke].activeMonitors[d.mid].spawn || !s.group[d.ke].activeMonitors[d.mid].spawn.stdin){ s.tx({error:'Not Started'},cn.id); @@ -1248,6 +912,13 @@ module.exports = function(s,config,lang,io){ } } }) + cn.on('gps',(d) => { + s.tx({ + f: 'gps', + gps: d.data, + mid: d.mid + },`MON_STREAM_${cn.ke}${d.mid}`) + }) //embed functions cn.on('e', function (d) { tx=function(z){if(!z.ke){z.ke=cn.ke;};cn.emit('f',z);} diff --git a/libs/sql.js b/libs/sql.js index 68a9846e..65cdaba1 100644 --- a/libs/sql.js +++ b/libs/sql.js @@ -61,6 +61,222 @@ module.exports = function(s,config){ .raw(data.query,data.values) .asCallback(callback) }, 4); + const cleanSqlWhereObject = (where) => { + const newWhere = {} + Object.keys(where).forEach((key) => { + if(key !== '__separator'){ + const value = where[key] + newWhere[key] = value + } + }) + return newWhere + } + const processSimpleWhereCondition = (dbQuery,where,didOne) => { + var whereIsArray = where instanceof Array; + if(where[0] === 'or' || where.__separator === 'or'){ + if(whereIsArray){ + where.shift() + dbQuery.orWhere(...where) + }else{ + where = cleanSqlWhereObject(where) + dbQuery.orWhere(where) + } + }else if(!didOne){ + didOne = true + whereIsArray ? dbQuery.where(...where) : dbQuery.where(where) + }else{ + whereIsArray ? dbQuery.andWhere(...where) : dbQuery.andWhere(where) + } + } + const processWhereCondition = (dbQuery,where,didOne) => { + var whereIsArray = where instanceof Array; + if(!where[0])return; + if(where[0] && where[0] instanceof Array){ + dbQuery.where(function() { + var _this = this + var didOneInsideGroup = false + where.forEach((whereInsideGroup) => { + processWhereCondition(_this,whereInsideGroup,didOneInsideGroup) + }) + }) + }else if(where[0] && where[0] instanceof Object){ + dbQuery.where(function() { + var _this = this + var didOneInsideGroup = false + where.forEach((whereInsideGroup) => { + processSimpleWhereCondition(_this,whereInsideGroup,didOneInsideGroup) + }) + }) + }else{ + processSimpleWhereCondition(dbQuery,where,didOne) + } + } + const knexError = (dbQuery,options,err) => { + console.error('knexError----------------------------------- START') + if(config.debugLogVerbose && config.debugLog === true){ + s.debugLog('s.knexQuery QUERY',JSON.stringify(options,null,3)) + s.debugLog('STACK TRACE, NOT AN ',new Error()) + } + console.error(err) + console.error(dbQuery.toString()) + console.error('knexError----------------------------------- END') + } + const knexQuery = (options,callback) => { + try{ + if(!s.databaseEngine)return// console.log('Database Not Set'); + // options = { + // action: "", + // columns: "", + // table: "" + // } + var dbQuery + switch(options.action){ + case'select': + options.columns = options.columns.indexOf(',') === -1 ? [options.columns] : options.columns.split(','); + dbQuery = s.databaseEngine.select(...options.columns).from(options.table) + break; + case'count': + options.columns = options.columns.indexOf(',') === -1 ? [options.columns] : options.columns.split(','); + dbQuery = s.databaseEngine(options.table) + dbQuery.count(options.columns) + break; + case'update': + dbQuery = s.databaseEngine(options.table).update(options.update) + break; + case'delete': + dbQuery = s.databaseEngine(options.table) + break; + case'insert': + dbQuery = s.databaseEngine(options.table).insert(options.insert) + break; + } + if(options.where instanceof Array){ + var didOne = false; + options.where.forEach((where) => { + processWhereCondition(dbQuery,where,didOne) + }) + }else if(options.where instanceof Object){ + dbQuery.where(options.where) + } + if(options.action === 'delete'){ + dbQuery.del() + } + if(options.orderBy){ + dbQuery.orderBy(...options.orderBy) + } + if(options.groupBy){ + dbQuery.groupBy(options.groupBy) + } + if(options.limit){ + if(`${options.limit}`.indexOf(',') === -1){ + dbQuery.limit(options.limit) + }else{ + const limitParts = `${options.limit}`.split(',') + dbQuery.limit(limitParts[1]).offset(limitParts[0]) + } + } + if(config.debugLog === true){ + console.log(dbQuery.toString()) + } + if(callback || options.update || options.insert || options.action === 'delete'){ + dbQuery.asCallback(function(err,r) { + if(err){ + knexError(dbQuery,options,err) + } + if(callback)callback(err,r) + if(config.debugLogVerbose && config.debugLog === true){ + s.debugLog('s.knexQuery QUERY',JSON.stringify(options,null,3)) + s.debugLog('s.knexQuery RESPONSE',JSON.stringify(r,null,3)) + s.debugLog('STACK TRACE, NOT AN ',new Error()) + } + }) + } + return dbQuery + }catch(err){ + if(callback)callback(err,[]) + knexError(dbQuery,options,err) + } + } + const getDatabaseRows = function(options,callback){ + //current cant handle `end` time + var whereQuery = [ + ['ke','=',options.groupKey], + ] + const monitorRestrictions = options.monitorRestrictions + var frameLimit = options.limit + const endIsStartTo = options.endIsStartTo + const chosenDate = options.date + const startDate = options.startDate ? s.stringToSqlTime(options.startDate) : null + const endDate = options.endDate ? s.stringToSqlTime(options.endDate) : null + const startOperator = options.startOperator || '>=' + const endOperator = options.endOperator || '<=' + const rowType = options.rowType || 'rows' + if(chosenDate){ + if(chosenDate.indexOf('-') === -1 && !isNaN(chosenDate)){ + chosenDate = parseInt(chosenDate) + } + var selectedDate = chosenDate + if(typeof chosenDate === 'string' && chosenDate.indexOf('.') > -1){ + selectedDate = chosenDate.split('.')[0] + } + selectedDate = new Date(selectedDate) + var utcSelectedDate = new Date(selectedDate.getTime() + selectedDate.getTimezoneOffset() * 60000) + startDate = moment(utcSelectedDate).format('YYYY-MM-DD HH:mm:ss') + var dayAfter = utcSelectedDate + dayAfter.setDate(dayAfter.getDate() + 1) + endDate = moment(dayAfter).format('YYYY-MM-DD HH:mm:ss') + } + if(startDate){ + if(endDate){ + whereQuery.push(['time',startOperator,startDate]) + whereQuery.push([endIsStartTo ? 'time' : 'end',endOperator,endDate]) + }else{ + whereQuery.push(['time',startOperator,startDate]) + } + } + if(monitorRestrictions && monitorRestrictions.length > 0){ + whereQuery.push(monitorRestrictions) + } + if(options.archived){ + whereQuery.push(['details','LIKE',`%"archived":"1"%`]) + } + if(options.filename){ + whereQuery.push(['filename','=',options.filename]) + frameLimit = "1"; + } + options.orderBy = options.orderBy ? options.orderBy : ['time','desc'] + if(options.count)options.groupBy = options.groupBy ? options.groupBy : options.orderBy[0] + knexQuery({ + action: options.count ? "count" : "select", + columns: options.columns || "*", + table: options.table, + where: whereQuery, + orderBy: options.orderBy, + groupBy: options.groupBy, + limit: frameLimit || '500' + },(err,r) => { + if(err){ + callback({ + ok: false, + total: 0, + limit: frameLimit, + [rowType]: [] + }) + }else{ + r.forEach(function(file){ + file.details = s.parseJSON(file.details) + }) + callback({ + ok: true, + total: r.length, + limit: frameLimit, + [rowType]: r + }) + } + }) + } + s.knexQuery = knexQuery + s.getDatabaseRows = getDatabaseRows s.sqlQuery = function(query,values,onMoveOn,hideLog){ if(!values){values=[]} if(typeof values === 'function'){ @@ -109,55 +325,209 @@ module.exports = function(s,config){ var mySQLtail = '' if(config.databaseType === 'mysql'){ mySQLtail = ' ENGINE=InnoDB DEFAULT CHARSET=utf8' + //add Presets table and modernize + var createPresetsTableQuery = 'CREATE TABLE IF NOT EXISTS `Presets` ( `ke` varchar(50) DEFAULT NULL, `name` text, `details` text, `type` varchar(50) DEFAULT NULL)' + s.sqlQuery( createPresetsTableQuery + mySQLtail + ';',[],function(err){ + if(err)console.error(err) + if(config.databaseType === 'sqlite3'){ + var aQuery = "ALTER TABLE Presets RENAME TO _Presets_old;" + aQuery += createPresetsTableQuery + aQuery += "INSERT INTO Presets (`ke`, `name`, `details`, `type`) SELECT `ke`, `name`, `details`, `type` FROM _Presets_old;COMMIT;DROP TABLE _Presets_old;" + }else{ + s.sqlQuery('ALTER TABLE `Presets` CHANGE COLUMN `type` `type` VARCHAR(50) NULL DEFAULT NULL AFTER `details`;',[],function(err){ + if(err)console.error(err) + },true) + } + },true) + //add Schedules table, will remove in future + s.sqlQuery("CREATE TABLE IF NOT EXISTS `Schedules` (`ke` varchar(50) DEFAULT NULL,`name` text,`details` text,`start` varchar(10) DEFAULT NULL,`end` varchar(10) DEFAULT NULL,`enabled` int(1) NOT NULL DEFAULT '1')" + mySQLtail + ';',[],function(err){ + if(err)console.error(err) + },true) + //add Timelapses and Timelapse Frames tables, will remove in future + s.sqlQuery("CREATE TABLE IF NOT EXISTS `Timelapses` (`ke` varchar(50) NOT NULL,`mid` varchar(50) NOT NULL,`details` longtext,`date` date NOT NULL,`time` timestamp NOT NULL,`end` timestamp NOT NULL,`size` int(11)NOT NULL)" + mySQLtail + ';',[],function(err){ + if(err)console.error(err) + },true) + s.sqlQuery("CREATE TABLE IF NOT EXISTS `Timelapse Frames` (`ke` varchar(50) NOT NULL,`mid` varchar(50) NOT NULL,`details` longtext,`filename` varchar(50) NOT NULL,`time` timestamp NULL DEFAULT NULL,`size` int(11) NOT NULL)" + mySQLtail + ';',[],function(err){ + if(err)console.error(err) + },true) + //Add index to Videos table + s.sqlQuery('CREATE INDEX `videos_index` ON Videos(`time`);',[],function(err){ + if(err && err.code !== 'ER_DUP_KEYNAME'){ + console.error(err) + } + },true) + //Add index to Events table + s.sqlQuery('CREATE INDEX `events_index` ON Events(`ke`, `mid`, `time`);',[],function(err){ + if(err && err.code !== 'ER_DUP_KEYNAME'){ + console.error(err) + } + },true) + //Add index to Logs table + s.sqlQuery('CREATE INDEX `logs_index` ON Logs(`ke`, `mid`, `time`);',[],function(err){ + if(err && err.code !== 'ER_DUP_KEYNAME'){ + console.error(err) + } + },true) + //Add index to Monitors table + s.sqlQuery('CREATE INDEX `monitors_index` ON Monitors(`ke`, `mode`, `type`, `ext`);',[],function(err){ + if(err && err.code !== 'ER_DUP_KEYNAME'){ + console.error(err) + } + },true) + //Add index to Timelapse Frames table + s.sqlQuery('CREATE INDEX `timelapseframes_index` ON `Timelapse Frames`(`ke`, `mid`, `time`);',[],function(err){ + if(err && err.code !== 'ER_DUP_KEYNAME'){ + console.error(err) + } + },true) + //add Cloud Videos table, will remove in future + s.sqlQuery('CREATE TABLE IF NOT EXISTS `Cloud Videos` (`mid` varchar(50) NOT NULL,`ke` varchar(50) DEFAULT NULL,`href` text NOT NULL,`size` float DEFAULT NULL,`time` timestamp NULL DEFAULT NULL,`end` timestamp NULL DEFAULT NULL,`status` int(1) DEFAULT \'0\',`details` text)' + mySQLtail + ';',[],function(err){ + if(err)console.error(err) + },true) + //add Events Counts table, will remove in future + s.sqlQuery('CREATE TABLE IF NOT EXISTS `Events Counts` (`ke` varchar(50) NOT NULL,`mid` varchar(50) NOT NULL,`details` longtext NOT NULL,`time` timestamp NOT NULL DEFAULT current_timestamp(),`end` timestamp NOT NULL DEFAULT current_timestamp(),`count` int(10) NOT NULL DEFAULT 1,`tag` varchar(30) DEFAULT NULL)' + mySQLtail + ';',[],function(err){ + if(err && err.code !== 'ER_TABLE_EXISTS_ERROR'){ + console.error(err) + } + s.sqlQuery('ALTER TABLE `Events Counts` ADD COLUMN `time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `details`;',[],function(err){ + // console.error(err) + },true) + },true) + //add Cloud Timelapse Frames table, will remove in future + s.sqlQuery('CREATE TABLE IF NOT EXISTS `Cloud Timelapse Frames` (`ke` varchar(50) NOT NULL,`mid` varchar(50) NOT NULL,`href` text NOT NULL,`details` longtext,`filename` varchar(50) NOT NULL,`time` timestamp NULL DEFAULT NULL,`size` int(11) NOT NULL)' + mySQLtail + ';',[],function(err){ + if(err)console.error(err) + },true) + //create Files table + var createFilesTableQuery = "CREATE TABLE IF NOT EXISTS `Files` (`ke` varchar(50) NOT NULL,`mid` varchar(50) NOT NULL,`name` tinytext NOT NULL,`size` float NOT NULL DEFAULT '0',`details` text NOT NULL,`status` int(1) NOT NULL DEFAULT '0',`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP)" + s.sqlQuery(createFilesTableQuery + mySQLtail + ';',[],function(err){ + if(err)console.error(err) + //add time column to Files table + if(config.databaseType === 'sqlite3'){ + var aQuery = "ALTER TABLE Files RENAME TO _Files_old;" + aQuery += createPresetsTableQuery + aQuery += "INSERT INTO Files (`ke`, `mid`, `name`, `details`, `size`, `status`, `time`) SELECT `ke`, `mid`, `name`, `details`, `size`, `status`, `time` FROM _Files_old;COMMIT;DROP TABLE _Files_old;" + }else{ + s.sqlQuery('ALTER TABLE `Files` ADD COLUMN `time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `status`;',[],function(err){ + if(err && err.sqlMessage && err.sqlMessage.indexOf('Duplicate') === -1)console.error(err) + },true) + } + },true) } - //add Presets table and modernize - var createPresetsTableQuery = 'CREATE TABLE IF NOT EXISTS `Presets` ( `ke` varchar(50) DEFAULT NULL, `name` text, `details` text, `type` varchar(50) DEFAULT NULL)' - s.sqlQuery( createPresetsTableQuery + mySQLtail + ';',[],function(err){ - if(err)console.error(err) - if(config.databaseType === 'sqlite3'){ - var aQuery = "ALTER TABLE Presets RENAME TO _Presets_old;" - aQuery += createPresetsTableQuery - aQuery += "INSERT INTO Presets (`ke`, `name`, `details`, `type`) SELECT `ke`, `name`, `details`, `type` FROM _Presets_old;COMMIT;DROP TABLE _Presets_old;" - }else{ - s.sqlQuery('ALTER TABLE `Presets` CHANGE COLUMN `type` `type` VARCHAR(50) NULL DEFAULT NULL AFTER `details`;',[],function(err){ - if(err)console.error(err) - },true) - } - },true) - //add Schedules table, will remove in future - s.sqlQuery("CREATE TABLE IF NOT EXISTS `Schedules` (`ke` varchar(50) DEFAULT NULL,`name` text,`details` text,`start` varchar(10) DEFAULT NULL,`end` varchar(10) DEFAULT NULL,`enabled` int(1) NOT NULL DEFAULT '1')" + mySQLtail + ';',[],function(err){ - if(err)console.error(err) - },true) - //add Timelapses and Timelapse Frames tables, will remove in future - s.sqlQuery("CREATE TABLE IF NOT EXISTS `Timelapses` (`ke` varchar(50) NOT NULL,`mid` varchar(50) NOT NULL,`details` longtext,`date` date NOT NULL,`time` timestamp NOT NULL,`end` timestamp NOT NULL,`size` int(11)NOT NULL)" + mySQLtail + ';',[],function(err){ - if(err)console.error(err) - },true) - s.sqlQuery("CREATE TABLE IF NOT EXISTS `Timelapse Frames` (`ke` varchar(50) NOT NULL,`mid` varchar(50) NOT NULL,`details` longtext,`filename` varchar(50) NOT NULL,`time` timestamp NULL DEFAULT NULL,`size` int(11) NOT NULL)" + mySQLtail + ';',[],function(err){ - if(err)console.error(err) - },true) - //add Cloud Videos table, will remove in future - s.sqlQuery('CREATE TABLE IF NOT EXISTS `Cloud Videos` (`mid` varchar(50) NOT NULL,`ke` varchar(50) DEFAULT NULL,`href` text NOT NULL,`size` float DEFAULT NULL,`time` timestamp NULL DEFAULT NULL,`end` timestamp NULL DEFAULT NULL,`status` int(1) DEFAULT \'0\',`details` text)' + mySQLtail + ';',[],function(err){ - if(err)console.error(err) - },true) - //add Cloud Timelapse Frames table, will remove in future - s.sqlQuery('CREATE TABLE IF NOT EXISTS `Cloud Timelapse Frames` (`ke` varchar(50) NOT NULL,`mid` varchar(50) NOT NULL,`href` text NOT NULL,`details` longtext,`filename` varchar(50) NOT NULL,`time` timestamp NULL DEFAULT NULL,`size` int(11) NOT NULL)' + mySQLtail + ';',[],function(err){ - if(err)console.error(err) - },true) - //create Files table - var createFilesTableQuery = "CREATE TABLE IF NOT EXISTS `Files` (`ke` varchar(50) NOT NULL,`mid` varchar(50) NOT NULL,`name` tinytext NOT NULL,`size` float NOT NULL DEFAULT '0',`details` text NOT NULL,`status` int(1) NOT NULL DEFAULT '0',`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP)" - s.sqlQuery(createFilesTableQuery + mySQLtail + ';',[],function(err){ - if(err)console.error(err) - //add time column to Files table - if(config.databaseType === 'sqlite3'){ - var aQuery = "ALTER TABLE Files RENAME TO _Files_old;" - aQuery += createPresetsTableQuery - aQuery += "INSERT INTO Files (`ke`, `mid`, `name`, `details`, `size`, `status`, `time`) SELECT `ke`, `mid`, `name`, `details`, `size`, `status`, `time` FROM _Files_old;COMMIT;DROP TABLE _Files_old;" - }else{ - s.sqlQuery('ALTER TABLE `Files` ADD COLUMN `time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `status`;',[],function(err){ - if(err && err.sqlMessage && err.sqlMessage.indexOf('Duplicate') === -1)console.error(err) - },true) - } - },true) delete(s.preQueries) } + s.sqlQueryBetweenTimesWithPermissions = (options,callback) => { + // options = { + // table: 'Events Counts', + // user: user, + // monitorId: req.params.id, + // startTime: req.query.start, + // endTime: req.query.end, + // startTimeOperator: req.query.startOperator, + // endTimeOperator: req.query.endOperator, + // limit: req.query.limit, + // archived: req.query.archived, + // endIsStartTo: !!req.query.endIsStartTo, + // parseRowDetails: true, + // rowName: 'counts' + // } + const rowName = options.rowName || 'rows' + const preliminaryValidationFailed = options.preliminaryValidationFailed || false + if(preliminaryValidationFailed){ + if(options.noFormat){ + callback([]); + }else{ + callback({ + ok: true, + [rowName]: [], + }) + } + return + } + const user = options.user + const groupKey = options.groupKey + const monitorId = options.monitorId + const archived = options.archived + const theTableSelected = options.table + const endIsStartTo = options.endIsStartTo + const userDetails = user.details + var endTime = options.endTime + var startTimeOperator = options.startTimeOperator + var endTimeOperator = options.endTimeOperator + var startTime = options.startTime + var limitString = `${options.limit}` + const monitorRestrictions = s.getMonitorRestrictions(options.user.details,monitorId) + getDatabaseRows({ + monitorRestrictions: monitorRestrictions, + table: theTableSelected, + groupKey: groupKey, + startDate: startTime, + endDate: endTime, + startOperator: startTimeOperator, + endOperator: endTimeOperator, + limit: options.limit, + archived: archived, + rowType: rowName, + endIsStartTo: endIsStartTo + },(response) => { + const limit = response.limit + const r = response[rowName]; + if(!r){ + callback({ + total: 0, + limit: response.limit, + skip: 0, + [rowName]: [] + }); + return + } + if(options.parseRowDetails){ + r.forEach((row) => { + row.details = JSON.parse(row.details) + }) + } + if(options.noCount){ + if(options.noFormat){ + callback(r) + }else{ + callback({ + ok: true, + limit: response.limit, + [rowName]: r, + endIsStartTo: endIsStartTo + }) + } + }else{ + getDatabaseRows({ + monitorRestrictions: monitorRestrictions, + columns: 'time', + count: true, + table: theTableSelected, + groupKey: groupKey, + startDate: startTime, + endDate: endTime, + startOperator: startTimeOperator, + endOperator: endTimeOperator, + archived: archived, + type: 'count', + endIsStartTo: endIsStartTo + },(response) => { + const count = response.count + var skipOver = 0 + if(limitString.indexOf(',') > -1){ + skipOver = parseInt(limitString.split(',')[0]) + limitString = parseInt(limitString.split(',')[1]) + }else{ + limitString = parseInt(limitString) + } + callback({ + total: response['count(*)'], + limit: response.limit, + skip: skipOver, + [rowName]: r, + endIsStartTo: endIsStartTo + }) + }) + } + }) + } } diff --git a/libs/startup.js b/libs/startup.js index d98fcd60..8cb64499 100644 --- a/libs/startup.js +++ b/libs/startup.js @@ -6,384 +6,446 @@ var crypto = require('crypto'); var exec = require('child_process').exec; var execSync = require('child_process').execSync; module.exports = function(s,config,lang,io){ - console.log('FFmpeg version : '+s.ffmpegVersion) - console.log('Node.js version : '+process.version) - s.processReady = function(){ - s.systemLog(lang.startUpText5) - s.onProcessReadyExtensions.forEach(function(extender){ - extender(true) - }) - process.send('ready') - } - var checkForTerminalCommands = function(callback){ - var next = function(){ - if(callback)callback() - } - if(!s.isWin && s.packageJson.mainDirectory !== '.'){ - var etcPath = '/etc/shinobisystems/cctv.txt' - fs.stat(etcPath,function(err,stat){ - if(err || !stat){ - exec('node '+ s.mainDirectory + '/INSTALL/terminalCommands.js',function(err){ - if(err)console.log(err) - }) - } - next() + return new Promise((resolve, reject) => { + var checkedAdminUsers = {} + console.log('FFmpeg version : '+s.ffmpegVersion) + console.log('Node.js version : '+process.version) + s.processReady = function(){ + delete(checkedAdminUsers) + resolve() + s.systemLog(lang.startUpText5) + s.onProcessReadyExtensions.forEach(function(extender){ + extender(true) }) - }else{ - next() + process.send('ready') } - } - var loadedAccounts = [] - var foundMonitors = [] - var loadMonitors = function(callback){ - s.beforeMonitorsLoadedOnStartupExtensions.forEach(function(extender){ - extender() - }) - s.systemLog(lang.startUpText4) - //preliminary monitor start - s.sqlQuery('SELECT * FROM Monitors', function(err,monitors) { - foundMonitors = monitors - if(err){s.systemLog(err)} + var checkForTerminalCommands = function(callback){ + var next = function(){ + if(callback)callback() + } + if(!s.isWin && s.packageJson.mainDirectory !== '.'){ + var etcPath = '/etc/shinobisystems/cctv.txt' + fs.stat(etcPath,function(err,stat){ + if(err || !stat){ + exec('node '+ s.mainDirectory + '/INSTALL/terminalCommands.js',function(err){ + if(err)console.log(err) + }) + } + next() + }) + }else{ + next() + } + } + var loadedAccounts = [] + var foundMonitors = [] + var loadMonitors = function(callback){ + s.beforeMonitorsLoadedOnStartupExtensions.forEach(function(extender){ + extender() + }) + s.systemLog(lang.startUpText4) + //preliminary monitor start + s.knexQuery({ + action: "select", + columns: "*", + table: "Monitors", + },function(err,monitors) { + foundMonitors = monitors + if(err){s.systemLog(err)} + if(monitors && monitors[0]){ + var didNotLoad = 0 + var loadCompleted = 0 + var orphanedVideosForMonitors = {} + var loadMonitor = function(monitor){ + const checkAnother = function(){ + ++loadCompleted + if(monitors[loadCompleted]){ + loadMonitor(monitors[loadCompleted]) + }else{ + if(didNotLoad > 0)console.log(`${didNotLoad} Monitor${didNotLoad === 1 ? '' : 's'} not loaded because Admin user does not exist for them. It may have been deleted.`); + callback() + } + } + if(checkedAdminUsers[monitor.ke]){ + setTimeout(function(){ + if(!orphanedVideosForMonitors[monitor.ke])orphanedVideosForMonitors[monitor.ke] = {} + if(!orphanedVideosForMonitors[monitor.ke][monitor.mid])orphanedVideosForMonitors[monitor.ke][monitor.mid] = 0 + s.initiateMonitorObject(monitor) + s.group[monitor.ke].rawMonitorConfigurations[monitor.mid] = monitor + s.sendMonitorStatus({id:monitor.mid,ke:monitor.ke,status:'Stopped'}); + var monObj = Object.assign(monitor,{id : monitor.mid}) + s.camera(monitor.mode,monObj) + checkAnother() + },1000) + }else{ + ++didNotLoad + checkAnother() + } + } + loadMonitor(monitors[loadCompleted]) + }else{ + callback() + } + }) + } + var checkForOrphanedVideos = function(callback){ + var monitors = foundMonitors if(monitors && monitors[0]){ var loadCompleted = 0 var orphanedVideosForMonitors = {} - var loadMonitor = function(monitor){ - setTimeout(function(){ - if(!orphanedVideosForMonitors[monitor.ke])orphanedVideosForMonitors[monitor.ke] = {} - if(!orphanedVideosForMonitors[monitor.ke][monitor.mid])orphanedVideosForMonitors[monitor.ke][monitor.mid] = 0 - s.initiateMonitorObject(monitor) - s.group[monitor.ke].rawMonitorConfigurations[monitor.mid] = monitor - s.sendMonitorStatus({id:monitor.mid,ke:monitor.ke,status:'Stopped'}); - var monObj = Object.assign(monitor,{id : monitor.mid}) - s.camera(monitor.mode,monObj) + var checkForOrphanedVideosForMonitor = function(monitor){ + if(!orphanedVideosForMonitors[monitor.ke])orphanedVideosForMonitors[monitor.ke] = {} + if(!orphanedVideosForMonitors[monitor.ke][monitor.mid])orphanedVideosForMonitors[monitor.ke][monitor.mid] = 0 + s.orphanedVideoCheck(monitor,null,function(orphanedFilesCount){ + if(orphanedFilesCount){ + orphanedVideosForMonitors[monitor.ke][monitor.mid] += orphanedFilesCount + } ++loadCompleted if(monitors[loadCompleted]){ - loadMonitor(monitors[loadCompleted]) + checkForOrphanedVideosForMonitor(monitors[loadCompleted]) }else{ + s.systemLog(lang.startUpText6+' : '+s.s(orphanedVideosForMonitors)) + delete(foundMonitors) callback() } - },2000) + }) } - loadMonitor(monitors[loadCompleted]) + checkForOrphanedVideosForMonitor(monitors[loadCompleted]) }else{ callback() } - }) - } - var checkForOrphanedVideos = function(callback){ - var monitors = foundMonitors - if(monitors && monitors[0]){ - var loadCompleted = 0 - var orphanedVideosForMonitors = {} - var checkForOrphanedVideosForMonitor = function(monitor){ - if(!orphanedVideosForMonitors[monitor.ke])orphanedVideosForMonitors[monitor.ke] = {} - if(!orphanedVideosForMonitors[monitor.ke][monitor.mid])orphanedVideosForMonitors[monitor.ke][monitor.mid] = 0 - s.orphanedVideoCheck(monitor,null,function(orphanedFilesCount){ - if(orphanedFilesCount){ - orphanedVideosForMonitors[monitor.ke][monitor.mid] += orphanedFilesCount - } - ++loadCompleted - if(monitors[loadCompleted]){ - checkForOrphanedVideosForMonitor(monitors[loadCompleted]) - }else{ - s.systemLog(lang.startUpText6+' : '+s.s(orphanedVideosForMonitors)) - delete(foundMonitors) - callback() - } - }) - } - checkForOrphanedVideosForMonitor(monitors[loadCompleted]) - }else{ - callback() } - } - var loadDiskUseForUser = function(user,callback){ - s.systemLog(user.mail+' : '+lang.startUpText0) - var userDetails = JSON.parse(user.details) - s.group[user.ke].sizeLimit = parseFloat(userDetails.size) || 10000 - s.group[user.ke].sizeLimitVideoPercent = parseFloat(userDetails.size_video_percent) || 90 - s.group[user.ke].sizeLimitTimelapseFramesPercent = parseFloat(userDetails.size_timelapse_percent) || 10 - s.sqlQuery('SELECT * FROM Videos WHERE ke=? AND status!=?',[user.ke,0],function(err,videos){ - s.sqlQuery('SELECT * FROM `Timelapse Frames` WHERE ke=?',[user.ke],function(err,timelapseFrames){ - s.sqlQuery('SELECT * FROM `Files` WHERE ke=?',[user.ke],function(err,files){ - var usedSpaceVideos = 0 - var usedSpaceTimelapseFrames = 0 - var usedSpaceFilebin = 0 - var addStorageData = { - files: [], - videos: [], - timelapeFrames: [], - } + var loadDiskUseForUser = function(user,callback){ + s.systemLog(user.mail+' : '+lang.startUpText0) + var userDetails = JSON.parse(user.details) + s.group[user.ke].sizeLimit = parseFloat(userDetails.size) || 10000 + s.group[user.ke].sizeLimitVideoPercent = parseFloat(userDetails.size_video_percent) || 90 + s.group[user.ke].sizeLimitTimelapseFramesPercent = parseFloat(userDetails.size_timelapse_percent) || 10 + s.knexQuery({ + action: "select", + columns: "*", + table: "Videos", + where: [ + ['ke','=',user.ke], + ['status','!=',0], + ] + },function(err,videos) { + s.knexQuery({ + action: "select", + columns: "*", + table: "Timelapse Frames", + where: [ + ['ke','=',user.ke], + ] + },function(err,timelapseFrames) { + s.knexQuery({ + action: "select", + columns: "*", + table: "Files", + where: [ + ['ke','=',user.ke], + ] + },function(err,files) { + var usedSpaceVideos = 0 + var usedSpaceTimelapseFrames = 0 + var usedSpaceFilebin = 0 + var addStorageData = { + files: [], + videos: [], + timelapeFrames: [], + } + if(videos && videos[0]){ + videos.forEach(function(video){ + video.details = s.parseJSON(video.details) + if(!video.details.dir){ + usedSpaceVideos += video.size + }else{ + addStorageData.videos.push(video) + } + }) + } + if(timelapseFrames && timelapseFrames[0]){ + timelapseFrames.forEach(function(frame){ + frame.details = s.parseJSON(frame.details) + if(!frame.details.dir){ + usedSpaceTimelapseFrames += frame.size + }else{ + addStorageData.timelapeFrames.push(frame) + } + }) + } + if(files && files[0]){ + files.forEach(function(file){ + file.details = s.parseJSON(file.details) + if(!file.details.dir){ + usedSpaceFilebin += file.size + }else{ + addStorageData.files.push(file) + } + }) + } + s.group[user.ke].usedSpace = (usedSpaceVideos + usedSpaceTimelapseFrames + usedSpaceFilebin) / 1048576 + s.group[user.ke].usedSpaceVideos = usedSpaceVideos / 1048576 + s.group[user.ke].usedSpaceFilebin = usedSpaceFilebin / 1048576 + s.group[user.ke].usedSpaceTimelapseFrames = usedSpaceTimelapseFrames / 1048576 + loadAddStorageDiskUseForUser(user,addStorageData,function(){ + callback() + }) + }) + }) + }) + } + var loadCloudDiskUseForUser = function(user,callback){ + var userDetails = JSON.parse(user.details) + user.cloudDiskUse = {} + user.size = 0 + user.limit = userDetails.size + s.cloudDisksLoaded.forEach(function(storageType){ + user.cloudDiskUse[storageType] = { + usedSpace : 0, + firstCount : 0 + } + if(s.cloudDiskUseStartupExtensions[storageType])s.cloudDiskUseStartupExtensions[storageType](user,userDetails) + }) + var loadCloudVideos = function(callback){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Cloud Videos", + where: [ + ['ke','=',user.ke], + ['status','!=',0], + ] + },function(err,videos) { if(videos && videos[0]){ videos.forEach(function(video){ - video.details = s.parseJSON(video.details) - if(!video.details.dir){ - usedSpaceVideos += video.size - }else{ - addStorageData.videos.push(video) - } + var storageType = JSON.parse(video.details).type + if(!storageType)storageType = 's3' + var videoSize = video.size / 1048576 + user.cloudDiskUse[storageType].usedSpace += videoSize + user.cloudDiskUse[storageType].usedSpaceVideos += videoSize + ++user.cloudDiskUse[storageType].firstCount + }) + s.cloudDisksLoaded.forEach(function(storageType){ + var firstCount = user.cloudDiskUse[storageType].firstCount + s.systemLog(user.mail+' : '+lang.startUpText1+' : '+firstCount,storageType,user.cloudDiskUse[storageType].usedSpace) + delete(user.cloudDiskUse[storageType].firstCount) }) } - if(timelapseFrames && timelapseFrames[0]){ - timelapseFrames.forEach(function(frame){ - frame.details = s.parseJSON(frame.details) - if(!frame.details.dir){ - usedSpaceTimelapseFrames += frame.size - }else{ - addStorageData.timelapeFrames.push(frame) - } + callback() + }) + } + var loadCloudTimelapseFrames = function(callback){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Cloud Timelapse Frames", + where: [ + ['ke','=',user.ke], + ] + },function(err,frames) { + if(frames && frames[0]){ + frames.forEach(function(frame){ + var storageType = JSON.parse(frame.details).type + if(!storageType)storageType = 's3' + var frameSize = frame.size / 1048576 + user.cloudDiskUse[storageType].usedSpace += frameSize + user.cloudDiskUse[storageType].usedSpaceTimelapseFrames += frameSize }) } - if(files && files[0]){ - files.forEach(function(file){ - file.details = s.parseJSON(file.details) - if(!file.details.dir){ - usedSpaceFilebin += file.size - }else{ - addStorageData.files.push(file) - } - }) - } - s.group[user.ke].usedSpace = (usedSpaceVideos + usedSpaceTimelapseFrames + usedSpaceFilebin) / 1000000 - s.group[user.ke].usedSpaceVideos = usedSpaceVideos / 1000000 - s.group[user.ke].usedSpaceFilebin = usedSpaceFilebin / 1000000 - s.group[user.ke].usedSpaceTimelapseFrames = usedSpaceTimelapseFrames / 1000000 - loadAddStorageDiskUseForUser(user,addStorageData,function(){ - callback() - }) + callback() + }) + } + loadCloudVideos(function(){ + loadCloudTimelapseFrames(function(){ + s.group[user.ke].cloudDiskUse = user.cloudDiskUse + callback() }) }) - }) - } - var loadCloudDiskUseForUser = function(user,callback){ - var userDetails = JSON.parse(user.details) - user.cloudDiskUse = {} - user.size = 0 - user.limit = userDetails.size - s.cloudDisksLoaded.forEach(function(storageType){ - user.cloudDiskUse[storageType] = { - usedSpace : 0, - firstCount : 0 - } - if(s.cloudDiskUseStartupExtensions[storageType])s.cloudDiskUseStartupExtensions[storageType](user,userDetails) - }) - var loadCloudVideos = function(callback){ - s.sqlQuery('SELECT * FROM `Cloud Videos` WHERE ke=? AND status!=?',[user.ke,0],function(err,videos){ + } + var loadAddStorageDiskUseForUser = function(user,data,callback){ + var videos = data.videos + var timelapseFrames = data.timelapseFrames + var files = data.files + var userDetails = JSON.parse(user.details) + var userAddStorageData = s.parseJSON(userDetails.addStorage) || {} + var currentStorageNumber = 0 + var readStorageArray = function(){ + var storage = s.listOfStorage[currentStorageNumber] + if(!storage){ + //done all checks, move on to next user + callback() + return + } + var path = storage.value + if(path === ''){ + ++currentStorageNumber + readStorageArray() + return + } + var storageId = path + var storageData = userAddStorageData[storageId] || {} + if(!s.group[user.ke].addStorageUse[storageId])s.group[user.ke].addStorageUse[storageId] = {} + var storageIndex = s.group[user.ke].addStorageUse[storageId] + storageIndex.name = storage.name + storageIndex.path = path + storageIndex.usedSpace = 0 + storageIndex.sizeLimit = parseFloat(storageData.limit) || parseFloat(userDetails.size) || 10000 + var usedSpaceVideos = 0 + var usedSpaceTimelapseFrames = 0 + var usedSpaceFilebin = 0 if(videos && videos[0]){ videos.forEach(function(video){ - var storageType = JSON.parse(video.details).type - if(!storageType)storageType = 's3' - var videoSize = video.size / 1000000 - user.cloudDiskUse[storageType].usedSpace += videoSize - user.cloudDiskUse[storageType].usedSpaceVideos += videoSize - ++user.cloudDiskUse[storageType].firstCount - }) - s.cloudDisksLoaded.forEach(function(storageType){ - var firstCount = user.cloudDiskUse[storageType].firstCount - s.systemLog(user.mail+' : '+lang.startUpText1+' : '+firstCount,storageType,user.cloudDiskUse[storageType].usedSpace) - delete(user.cloudDiskUse[storageType].firstCount) + if(video.details.dir === storage.value){ + usedSpaceVideos += video.size + } }) } - callback() - }) - } - var loadCloudTimelapseFrames = function(callback){ - s.sqlQuery('SELECT * FROM `Cloud Timelapse Frames` WHERE ke=?',[user.ke],function(err,frames){ - if(frames && frames[0]){ - frames.forEach(function(frame){ - var storageType = JSON.parse(frame.details).type - if(!storageType)storageType = 's3' - var frameSize = frame.size / 1000000 - user.cloudDiskUse[storageType].usedSpace += frameSize - user.cloudDiskUse[storageType].usedSpaceTimelapseFrames += frameSize + if(timelapseFrames && timelapseFrames[0]){ + timelapseFrames.forEach(function(frame){ + if(video.details.dir === storage.value){ + usedSpaceTimelapseFrames += frame.size + } }) } - callback() - }) - } - loadCloudVideos(function(){ - loadCloudTimelapseFrames(function(){ - s.group[user.ke].cloudDiskUse = user.cloudDiskUse - callback() - }) - }) - } - var loadAddStorageDiskUseForUser = function(user,data,callback){ - var videos = data.videos - var timelapseFrames = data.timelapseFrames - var files = data.files - var userDetails = JSON.parse(user.details) - var userAddStorageData = s.parseJSON(userDetails.addStorage) || {} - var currentStorageNumber = 0 - var readStorageArray = function(){ - var storage = s.listOfStorage[currentStorageNumber] - if(!storage){ - //done all checks, move on to next user - callback() - return - } - var path = storage.value - if(path === ''){ + if(files && files[0]){ + files.forEach(function(file){ + if(video.details.dir === storage.value){ + usedSpaceFilebin += file.size + } + }) + } + storageIndex.usedSpace = (usedSpaceVideos + usedSpaceTimelapseFrames + usedSpaceFilebin) / 1048576 + storageIndex.usedSpaceVideos = usedSpaceVideos / 1048576 + storageIndex.usedSpaceFilebin = usedSpaceFilebin / 1048576 + storageIndex.usedSpaceTimelapseFrames = usedSpaceTimelapseFrames / 1048576 + s.systemLog(user.mail+' : '+path+' : '+videos.length,storageIndex.usedSpace) ++currentStorageNumber readStorageArray() - return } - var storageId = path - var storageData = userAddStorageData[storageId] || {} - if(!s.group[user.ke].addStorageUse[storageId])s.group[user.ke].addStorageUse[storageId] = {} - var storageIndex = s.group[user.ke].addStorageUse[storageId] - storageIndex.name = storage.name - storageIndex.path = path - storageIndex.usedSpace = 0 - storageIndex.sizeLimit = parseFloat(storageData.limit) || parseFloat(userDetails.size) || 10000 - var usedSpaceVideos = 0 - var usedSpaceTimelapseFrames = 0 - var usedSpaceFilebin = 0 - if(videos && videos[0]){ - videos.forEach(function(video){ - if(video.details.dir === storage.value){ - usedSpaceVideos += video.size - } - }) - } - if(timelapseFrames && timelapseFrames[0]){ - timelapseFrames.forEach(function(frame){ - if(video.details.dir === storage.value){ - usedSpaceTimelapseFrames += frame.size - } - }) - } - if(files && files[0]){ - files.forEach(function(file){ - if(video.details.dir === storage.value){ - usedSpaceFilebin += file.size - } - }) - } - storageIndex.usedSpace = (usedSpaceVideos + usedSpaceTimelapseFrames + usedSpaceFilebin) / 1000000 - storageIndex.usedSpaceVideos = usedSpaceVideos / 1000000 - storageIndex.usedSpaceFilebin = usedSpaceFilebin / 1000000 - storageIndex.usedSpaceTimelapseFrames = usedSpaceTimelapseFrames / 1000000 - s.systemLog(user.mail+' : '+path+' : '+videos.length,storageIndex.usedSpace) - ++currentStorageNumber readStorageArray() } - readStorageArray() - } - var loadAdminUsers = function(callback){ - //get current disk used for each isolated account (admin user) on startup - s.sqlQuery('SELECT * FROM Users WHERE details NOT LIKE ?',['%"sub"%'],function(err,users){ - if(users && users[0]){ - var loadLocalDiskUse = function(callback){ - var count = users.length - var countFinished = 0 + var loadAdminUsers = function(callback){ + //get current disk used for each isolated account (admin user) on startup + s.knexQuery({ + action: "select", + columns: "*", + table: "Users", + where: [ + ['details','NOT LIKE','%"sub"%'] + ] + },function(err,users) { + if(users && users[0]){ users.forEach(function(user){ - s.loadGroup(user) - s.loadGroupApps(user) - loadedAccounts.push(user.ke) - loadDiskUseForUser(user,function(){ - ++countFinished - if(countFinished === count){ - callback() - } + checkedAdminUsers[user.ke] = user + }) + var loadLocalDiskUse = function(callback){ + var count = users.length + var countFinished = 0 + users.forEach(function(user){ + s.loadGroup(user) + s.loadGroupApps(user) + loadedAccounts.push(user.ke) + loadDiskUseForUser(user,function(){ + ++countFinished + if(countFinished === count){ + callback() + } + }) + }) + } + var loadCloudDiskUse = function(callback){ + var count = users.length + var countFinished = 0 + users.forEach(function(user){ + loadCloudDiskUseForUser(user,function(){ + ++countFinished + if(countFinished === count){ + callback() + } + }) + }) + } + loadLocalDiskUse(function(){ + loadCloudDiskUse(function(){ + callback() }) }) + }else{ + s.processReady() } - var loadCloudDiskUse = function(callback){ - var count = users.length - var countFinished = 0 - users.forEach(function(user){ - loadCloudDiskUseForUser(user,function(){ - ++countFinished - if(countFinished === count){ - callback() - } - }) - }) - } - loadLocalDiskUse(function(){ - loadCloudDiskUse(function(){ - callback() - }) + }) + } + config.userHasSubscribed = false + var checkSubscription = function(callback){ + var subscriptionFailed = function(){ + console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') + console.error('This Install of Shinobi is NOT Activated') + console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') + console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') + s.systemLog('This Install of Shinobi is NOT Activated') + console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') + console.log('https://licenses.shinobi.video/subscribe') + } + if(config.subscriptionId && config.subscriptionId !== 'sub_XXXXXXXXXXXX'){ + var url = 'https://licenses.shinobi.video/subscribe/check?subscriptionId=' + config.subscriptionId + request(url,{ + method: 'GET', + timeout: 30000 + }, function(err,resp,body){ + var json = s.parseJSON(body) + if(err)console.log(err,json) + var hasSubcribed = json && !!json.ok + config.userHasSubscribed = hasSubcribed + callback(hasSubcribed) + if(config.userHasSubscribed){ + s.systemLog('This Install of Shinobi is Activated') + if(!json.expired){ + s.systemLog(`This License expires on ${json.timeExpires}`) + } + }else{ + subscriptionFailed() + } }) }else{ - s.processReady() + subscriptionFailed() + callback(false) } - }) - } - config.userHasSubscribed = false - var checkSubscription = function(callback){ - var subscriptionFailed = function(){ - console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') - console.error('This Install of Shinobi is NOT Activated') - console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') - console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') - s.systemLog('This Install of Shinobi is NOT Activated') - console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') - console.log('https://licenses.shinobi.video/subscribe') } - if(config.subscriptionId){ - var url = 'https://licenses.shinobi.video/subscribe/check?subscriptionId=' + config.subscriptionId - request(url,{ - method: 'GET', - timeout: 30000 - }, function(err,resp,body){ - var json = s.parseJSON(body) - if(err)console.log(err,json) - var hasSubcribed = !!json.ok - config.userHasSubscribed = hasSubcribed - callback(hasSubcribed) - if(config.userHasSubscribed){ - s.systemLog('This Install of Shinobi is Activated') - }else{ - subscriptionFailed() - } - }) - }else{ - subscriptionFailed() - callback(false) + //check disk space every 20 minutes + if(config.autoDropCache===true){ + setInterval(function(){ + exec('echo 3 > /proc/sys/vm/drop_caches',{detached: true}) + },60000*20) } - } - //check disk space every 20 minutes - if(config.autoDropCache===true){ - setInterval(function(){ - exec('echo 3 > /proc/sys/vm/drop_caches',{detached: true}) - },60000*20) - } - if(config.childNodes.mode !== 'child'){ - //master node - startup functions - setInterval(function(){ - s.cpuUsage(function(cpu){ - s.ramUsage(function(ram){ - s.tx({f:'os',cpu:cpu,ram:ram},'CPU'); - }) - }) - },10000) - //hourly check to see if sizePurge has failed to unlock - //checks to see if request count is the number of monitors + 10 - s.checkForStalePurgeLocks() - //run prerequsite queries, load users and monitors - //sql/database connection with knex - s.databaseEngine = require('knex')(s.databaseOptions) - //run prerequsite queries - s.preQueries() - setTimeout(function(){ - //check for subscription - checkSubscription(function(){ - //check terminal commander - checkForTerminalCommands(function(){ - //load administrators (groups) - loadAdminUsers(function(){ - //load monitors (for groups) - loadMonitors(function(){ - //check for orphaned videos - checkForOrphanedVideos(function(){ - s.processReady() + if(config.childNodes.mode !== 'child'){ + //master node - startup functions + //hourly check to see if sizePurge has failed to unlock + //checks to see if request count is the number of monitors + 10 + s.checkForStalePurgeLocks() + //run prerequsite queries, load users and monitors + //sql/database connection with knex + s.databaseEngine = require('knex')(s.databaseOptions) + //run prerequsite queries + s.preQueries() + setTimeout(() => { + //check for subscription + checkSubscription(function(){ + //check terminal commander + checkForTerminalCommands(function(){ + //load administrators (groups) + loadAdminUsers(function(){ + //load monitors (for groups) + loadMonitors(function(){ + //check for orphaned videos + checkForOrphanedVideos(async () => { + s.processReady() + }) }) }) }) }) - }) - },1500) - } + },1500) + } + }) } diff --git a/libs/timelapse.js b/libs/timelapse.js index 92062043..ccdb926c 100644 --- a/libs/timelapse.js +++ b/libs/timelapse.js @@ -74,9 +74,13 @@ module.exports = function(s,config,lang,app,io){ } } s.insertTimelapseFrameDatabaseRow = function(e,queryInfo,filePath){ - s.sqlQuery('INSERT INTO `Timelapse Frames` ('+Object.keys(queryInfo).join(',')+') VALUES (?,?,?,?,?,?)',Object.values(queryInfo)) - s.setDiskUsedForGroup(e,queryInfo.size / 1000000,'timelapeFrames') - s.purgeDiskForGroup(e) + s.knexQuery({ + action: "insert", + table: "Timelapse Frames", + insert: queryInfo + }) + s.setDiskUsedForGroup(e.ke,queryInfo.size / 1048576,'timelapeFrames') + s.purgeDiskForGroup(e.ke) s.onInsertTimelapseFrameExtensions.forEach(function(extender){ extender(e,queryInfo,filePath) }) @@ -103,11 +107,26 @@ module.exports = function(s,config,lang,app,io){ s.deleteTimelapseFrameFromCloud = function(e){ // e = video object s.checkDetails(e) - var frameSelector = [e.id,e.ke,new Date(e.time)] - s.sqlQuery('SELECT * FROM `Cloud Timelapse Frames` WHERE `mid`=? AND `ke`=? AND `time`=?',frameSelector,function(err,r){ - if(r&&r[0]){ + var frameSelector = { + ke: e.ke, + mid: e.id, + time: new Date(e.time), + } + s.knexQuery({ + action: "select", + columns: "*", + table: "Cloud Timelapse Frames", + where: frameSelector, + limit: 1 + },function(err,r){ + if(r && r[0]){ r = r[0] - s.sqlQuery('DELETE FROM `Cloud Timelapse Frames` WHERE `mid`=? AND `ke`=? AND `time`=?',frameSelector,function(){ + s.knexQuery({ + action: "delete", + table: "Cloud Timelapse Frames", + where: frameSelector, + limit: 1 + },function(){ s.onDeleteTimelapseFrameFromCloudExtensionsRunner(e,r) }) }else{ @@ -131,112 +150,54 @@ module.exports = function(s,config,lang,app,io){ var hasRestrictions = user.details.sub && user.details.allmonitors !== '1' if( user.permissions.watch_videos==="0" || - hasRestrictions && (!user.details.video_view || user.details.video_view.indexOf(req.params.id)===-1) + hasRestrictions && + ( + !user.details.video_view || + user.details.video_view.indexOf(req.params.id) === -1 + ) ){ - res.end(s.prettyPrint([])) + s.closeJsonResponse(res,[]) return } - req.sql='SELECT * FROM `Timelapse Frames` WHERE ke=?';req.ar=[req.params.ke]; - if(req.query.archived=='1'){ - req.sql+=' AND details LIKE \'%"archived":"1"\'' - } - if(!req.params.id){ - if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ - try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} - req.or=[]; - user.details.monitors.forEach(function(v,n){ - req.or.push('mid=?');req.ar.push(v) - }) - req.sql+=' AND ('+req.or.join(' OR ')+')' - } - }else{ - if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){ - req.sql+=' and mid=?' - req.ar.push(req.params.id) - }else{ - res.end('[]'); - return; - } - } - var isMp4Call = false - if(req.query.mp4){ - isMp4Call = true - } - if(req.params.date){ - if(req.params.date.indexOf('-') === -1 && !isNaN(req.params.date)){ - req.params.date = parseInt(req.params.date) - } - var selectedDate = req.params.date - if(typeof req.params.date === 'string' && req.params.date.indexOf('.') > -1){ - isMp4Call = true - selectedDate = req.params.date.split('.')[0] - } - selectedDate = new Date(selectedDate) - var utcSelectedDate = new Date(selectedDate.getTime() + selectedDate.getTimezoneOffset() * 60000) - req.query.start = moment(utcSelectedDate).format('YYYY-MM-DD HH:mm:ss') - var dayAfter = utcSelectedDate - dayAfter.setDate(dayAfter.getDate() + 1) - req.query.end = moment(dayAfter).format('YYYY-MM-DD HH:mm:ss') - } - if(req.query.start||req.query.end){ - if(!req.query.startOperator||req.query.startOperator==''){ - req.query.startOperator='>=' - } - if(!req.query.endOperator||req.query.endOperator==''){ - req.query.endOperator='<=' - } - if(req.query.start && req.query.start !== '' && req.query.end && req.query.end !== ''){ - req.query.start = s.stringToSqlTime(req.query.start) - req.query.end = s.stringToSqlTime(req.query.end) - req.sql+=' AND `time` '+req.query.startOperator+' ? AND `time` '+req.query.endOperator+' ?'; - req.ar.push(req.query.start) - req.ar.push(req.query.end) - }else if(req.query.start && req.query.start !== ''){ - req.query.start = s.stringToSqlTime(req.query.start) - req.sql+=' AND `time` '+req.query.startOperator+' ?'; - req.ar.push(req.query.start) - } - } - // if(!req.query.limit||req.query.limit==''){req.query.limit=288} - req.sql+=' ORDER BY `time` DESC' - s.sqlQuery(req.sql,req.ar,function(err,r){ - if(isMp4Call){ - if(r && r[0]){ - s.createVideoFromTimelapse(r,req.query.fps,function(response){ - if(response.fileExists){ - if(req.query.download){ - res.setHeader('Content-Type', 'video/mp4') - s.streamMp4FileOverHttp(response.fileLocation,req,res) - }else{ - res.setHeader('Content-Type', 'application/json') - res.end(s.prettyPrint({ - ok : response.ok, - fileExists : response.fileExists, - msg : response.msg, - })) - } + const monitorRestrictions = s.getMonitorRestrictions(user.details,req.params.id) + s.getDatabaseRows({ + monitorRestrictions: monitorRestrictions, + table: 'Timelapse Frames', + groupKey: req.params.ke, + date: req.query.date, + startDate: req.query.start, + endDate: req.query.end, + startOperator: req.query.startOperator, + endOperator: req.query.endOperator, + limit: req.query.limit, + archived: req.query.archived, + rowType: 'frames', + endIsStartTo: true + },(response) => { + var isMp4Call = !!(req.query.mp4 || (req.params.date && typeof req.params.date === 'string' && req.params.date.indexOf('.') > -1)) + if(isMp4Call && response.frames[0]){ + s.createVideoFromTimelapse(response.frames,req.query.fps,function(response){ + if(response.fileExists){ + if(req.query.download){ + res.setHeader('Content-Type', 'video/mp4') + s.streamMp4FileOverHttp(response.fileLocation,req,res) }else{ - res.setHeader('Content-Type', 'application/json') - res.end(s.prettyPrint({ + s.closeJsonResponse(res,{ ok : response.ok, fileExists : response.fileExists, msg : response.msg, - })) + }) } - }) - }else{ - res.setHeader('Content-Type', 'application/json'); - res.end(s.prettyPrint([])) - } + }else{ + s.closeJsonResponse(res,{ + ok : response.ok, + fileExists : response.fileExists, + msg : response.msg, + }) + } + }) }else{ - if(r && r[0]){ - r.forEach(function(file){ - file.details = s.parseJSON(file.details) - }) - res.end(s.prettyPrint(r)) - }else{ - res.end(s.prettyPrint([])) - } + s.closeJsonResponse(res,response.frames) } }) },res,req); @@ -257,35 +218,18 @@ module.exports = function(s,config,lang,app,io){ res.end(s.prettyPrint([])) return } - req.sql='SELECT * FROM `Timelapse Frames` WHERE ke=?';req.ar=[req.params.ke]; - if(req.query.archived=='1'){ - req.sql+=' AND details LIKE \'%"archived":"1"\'' - } - if(!req.params.id){ - if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ - try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} - req.or=[]; - user.details.monitors.forEach(function(v,n){ - req.or.push('mid=?');req.ar.push(v) - }) - req.sql+=' AND ('+req.or.join(' OR ')+')' - } - }else{ - if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){ - req.sql+=' and mid=?' - req.ar.push(req.params.id) - }else{ - res.end('[]'); - return; - } - } - req.sql+=' AND filename=?' - req.ar.push(req.params.filename) - req.sql+=' ORDER BY `time` DESC' - s.sqlQuery(req.sql,req.ar,function(err,r){ - if(r && r[0]){ - var frame = r[0] - frame.details = s.parseJSON(frame.details) + const monitorRestrictions = s.getMonitorRestrictions(user.details,req.params.id) + s.getDatabaseRows({ + monitorRestrictions: monitorRestrictions, + table: 'Timelapse Frames', + groupKey: req.params.ke, + archived: req.query.archived, + filename: req.params.filename, + rowType: 'frames', + endIsStartTo: true + },(response) => { + var frame = response.frames[0] + if(frame){ var fileLocation if(frame.details.dir){ fileLocation = `${s.checkCorrectPathEnding(frame.details.dir)}` @@ -303,11 +247,11 @@ module.exports = function(s,config,lang,app,io){ res.on('finish',function(){res.end()}) fs.createReadStream(fileLocation).pipe(res) }else{ - res.end(s.prettyPrint({ok: false, msg: lang[`Nothing exists`]})) + s.closeJsonResponse(res,{ok: false, msg: lang[`Nothing exists`]}) } }) }else{ - res.end(s.prettyPrint({ok: false, msg: lang[`Nothing exists`]})) + s.closeJsonResponse(res,{ok: false, msg: lang[`Nothing exists`]}) } }) },res,req); @@ -338,7 +282,15 @@ module.exports = function(s,config,lang,app,io){ if(hoursNow === 1){ var dateNowMoment = moment(dateNow).utc().format('YYYY-MM-DDTHH:mm:ss') var dateMinusOneDay = moment(dateNow).utc().subtract(1, 'days').format('YYYY-MM-DDTHH:mm:ss') - s.sqlQuery('SELECT * FROM `Timelapse Frames` WHERE time => ? AND time =< ?',[dateMinusOneDay,dateNowMoment],function(err,frames){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Timelapse Frames", + where: [ + ['time','=>',dateMinusOneDay], + ['time','=<',dateNowMoment], + ] + },function(err,frames) { console.log(frames.length) var groups = {} frames.forEach(function(frame){ diff --git a/libs/uploaders.js b/libs/uploaders.js index 2c19cb94..eb143039 100644 --- a/libs/uploaders.js +++ b/libs/uploaders.js @@ -1,21 +1,20 @@ -module.exports = function(s,config,lang){ +module.exports = function(s,config,lang,app,io){ s.uploaderFields = [] - var loadLib = function(lib){ - var uploadersFolder = __dirname + '/uploaders/' - var libraryPath = uploadersFolder + lib + '.js' - var loadedLib = require(libraryPath)(s,config,lang) - if(lib !== 'loader'){ - loadedLib.isFormGroupGroup = true - s.uploaderFields.push(loadedLib) - } - return loadedLib + require('./uploaders/loader.js')(s,config,lang,app,io) + const loadedLibraries = { + //cloud storage + s3based: require('./uploaders/s3based.js'), + backblazeB2: require('./uploaders/backblazeB2.js'), + amazonS3: require('./uploaders/amazonS3.js'), + webdav: require('./uploaders/webdav.js'), + //oauth + googleDrive: require('./uploaders/googleDrive.js'), + //simple storage + sftp: require('./uploaders/sftp.js'), } - loadLib('loader') - //cloud storage - loadLib('s3based') - loadLib('backblazeB2') - loadLib('amazonS3') - loadLib('webdav') - //simple storage - loadLib('sftp') + Object.keys(loadedLibraries).forEach((key) => { + var loadedLib = loadedLibraries[key](s,config,lang,app,io) + loadedLib.isFormGroupGroup = true + s.uploaderFields.push(loadedLib) + }) } diff --git a/libs/uploaders/amazonS3.js b/libs/uploaders/amazonS3.js index 58aeef84..a16e351b 100644 --- a/libs/uploaders/amazonS3.js +++ b/libs/uploaders/amazonS3.js @@ -3,8 +3,8 @@ module.exports = function(s,config,lang){ //Amazon S3 var beforeAccountSaveForAmazonS3 = function(d){ //d = save event - d.form.details.aws_use_global=d.d.aws_use_global - d.form.details.use_aws_s3=d.d.use_aws_s3 + d.formDetails.aws_use_global=d.d.aws_use_global + d.formDetails.use_aws_s3=d.d.use_aws_s3 } var cloudDiskUseStartupForAmazonS3 = function(group,userDetails){ group.cloudDiskUse['s3'].name = 'Amazon S3' @@ -100,23 +100,26 @@ module.exports = function(s,config,lang){ s.userLog(e,{type:lang['Amazon S3 Upload Error'],msg:err}) } if(s.group[e.ke].init.aws_s3_log === '1' && data && data.Location){ - var save = [ - e.mid, - e.ke, - k.startTime, - 1, - s.s({ - type : 's3', - location : saveLocation - }), - k.filesize, - k.endTime, - data.Location - ] - s.sqlQuery('INSERT INTO `Cloud Videos` (mid,ke,time,status,details,size,end,href) VALUES (?,?,?,?,?,?,?,?)',save) - s.setCloudDiskUsedForGroup(e,{ - amount : k.filesizeMB, - storageType : 's3' + s.knexQuery({ + action: "insert", + table: "Cloud Videos", + insert: { + mid: e.mid, + ke: e.ke, + time: k.startTime, + status: 1, + details: s.s({ + type : 's3', + location : saveLocation + }), + size: k.filesize, + end: k.endTime, + href: data.Location + } + }) + s.setCloudDiskUsedForGroup(e.ke,{ + amount: k.filesizeMB, + storageType: 's3' }) s.purgeCloudDiskForGroup(e,'s3') } @@ -142,19 +145,22 @@ module.exports = function(s,config,lang){ s.userLog(e,{type:lang['Wasabi Hot Cloud Storage Upload Error'],msg:err}) } if(s.group[e.ke].init.aws_s3_log === '1' && data && data.Location){ - var save = [ - queryInfo.mid, - queryInfo.ke, - queryInfo.time, - s.s({ - type : 's3', - location : saveLocation, - }), - queryInfo.size, - data.Location - ] - s.sqlQuery('INSERT INTO `Cloud Timelapse Frames` (mid,ke,time,details,size,href) VALUES (?,?,?,?,?,?)',save) - s.setCloudDiskUsedForGroup(e,{ + s.knexQuery({ + action: "insert", + table: "Cloud Timelapse Frames", + insert: { + mid: queryInfo.mid, + ke: queryInfo.ke, + time: queryInfo.time, + details: s.s({ + type : 's3', + location : saveLocation + }), + size: queryInfo.size, + href: data.Location + } + }) + s.setCloudDiskUsedForGroup(e.ke,{ amount : s.kilobyteToMegabyte(queryInfo.size), storageType : 's3' },'timelapseFrames') @@ -405,4 +411,4 @@ module.exports = function(s,config,lang){ }, ] } -} \ No newline at end of file +} diff --git a/libs/uploaders/backblazeB2.js b/libs/uploaders/backblazeB2.js index 14a80b37..b61cefdc 100644 --- a/libs/uploaders/backblazeB2.js +++ b/libs/uploaders/backblazeB2.js @@ -3,8 +3,8 @@ module.exports = function(s,config,lang){ //Backblaze B2 var beforeAccountSaveForBackblazeB2 = function(d){ //d = save event - d.form.details.b2_use_global=d.d.b2_use_global - d.form.details.use_bb_b2=d.d.use_bb_b2 + d.formDetails.b2_use_global=d.d.b2_use_global + d.formDetails.use_bb_b2=d.d.use_bb_b2 } var cloudDiskUseStartupForBackblazeB2 = function(group,userDetails){ group.cloudDiskUse['b2'].name = 'Backblaze B2' @@ -44,7 +44,7 @@ module.exports = function(s,config,lang){ } var backblazeErr = function(err){ // console.log(err) - s.userLog({mid:'$USER',ke:e.ke},{type:lang['Backblaze Error'],msg:err.data || err}) + s.userLog({mid:'$USER',ke:e.ke},{type:lang['Backblaze Error'],msg:err.stack || err.data || err}) } var createB2Connection = function(){ var b2 = new B2({ @@ -129,23 +129,26 @@ module.exports = function(s,config,lang){ }).then(function(resp){ if(s.group[e.ke].init.bb_b2_log === '1' && resp.data.fileId){ var backblazeDownloadUrl = s.group[e.ke].bb_b2_downloadUrl + '/file/' + s.group[e.ke].init.bb_b2_bucket + '/' + backblazeSavePath - var save = [ - e.mid, - e.ke, - k.startTime, - 1, - s.s({ - type : 'b2', - bucketId : resp.data.bucketId, - fileId : resp.data.fileId, - fileName : resp.data.fileName - }), - k.filesize, - k.endTime, - backblazeDownloadUrl - ] - s.sqlQuery('INSERT INTO `Cloud Videos` (mid,ke,time,status,details,size,end,href) VALUES (?,?,?,?,?,?,?,?)',save) - s.setCloudDiskUsedForGroup(e,{ + s.knexQuery({ + action: "insert", + table: "Cloud Videos", + insert: { + mid: e.mid, + ke: e.ke, + time: k.startTime, + status: 1, + details: s.s({ + type : 'b2', + bucketId : resp.data.bucketId, + fileId : resp.data.fileId, + fileName : resp.data.fileName + }), + size: k.filesize, + end: k.endTime, + href: backblazeDownloadUrl + } + }) + s.setCloudDiskUsedForGroup(e.ke,{ amount : k.filesizeMB, storageType : 'b2' }) diff --git a/libs/uploaders/googleDrive.js b/libs/uploaders/googleDrive.js new file mode 100644 index 00000000..7d50bbe5 --- /dev/null +++ b/libs/uploaders/googleDrive.js @@ -0,0 +1,418 @@ +var fs = require('fs'); +const {google} = require('googleapis'); +module.exports = (s,config,lang,app,io) => { + const initializeOAuth = (credentials) => { + const creds = s.parseJSON(credentials) + if(!creds || !creds.installed)return; + const {client_secret, client_id, redirect_uris} = creds.installed; + const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]); + return oAuth2Client + } + const getVideoDirectoryId = (video) => { + return new Promise((resolve, reject) => { + const videoDirectory = s.group[video.ke].init.googd_dir + video.ke + '/' + video.mid + if(!s.group[video.ke].googleDriveFolderIds[video.ke + video.mid]){ + var pageToken = null; + s.group[video.ke].googleDrive.files.list({ + q: `name='${videoDirectory}'`, + fields: 'nextPageToken, files(id, name)', + spaces: 'drive', + pageToken: pageToken + }, function (err, res) { + if (err) { + reject(err) + } else { + var file = res.data.files + if(file[0]){ + file = file[0] + s.group[video.ke].googleDriveFolderIds[video.ke + video.mid] = file.id + resolve(file.id) + }else{ + s.group[video.ke].googleDrive.files.create({ + resource: { + 'name': videoDirectory, + 'mimeType': 'application/vnd.google-apps.folder' + }, + fields: 'id' + }, function (err, response) { + if (err) { + reject(err) + } else { + s.group[video.ke].googleDriveFolderIds[video.ke + video.mid] = response.data.id + resolve(response.data.id) + } + }) + } + } + }) + }else{ + resolve(s.group[video.ke].googleDriveFolderIds[video.ke + video.mid]) + } + + }) + } + //Google Drive Storage + var beforeAccountSaveForGoogleDrive = function(d){ + //d = save event + d.formDetails.googd_use_global = d.d.googd_use_global + d.formDetails.use_googd = d.d.use_googd + } + var cloudDiskUseStartupForGoogleDrive = function(group,userDetails){ + group.cloudDiskUse['googd'].name = 'Google Drive Storage' + group.cloudDiskUse['googd'].sizeLimitCheck = (userDetails.use_googd_size_limit === '1') + if(!userDetails.googd_size_limit || userDetails.googd_size_limit === ''){ + group.cloudDiskUse['googd'].sizeLimit = 10000 + }else{ + group.cloudDiskUse['googd'].sizeLimit = parseFloat(userDetails.googd_size_limit) + } + } + var loadGoogleDriveForUser = async function(e){ + // e = user + var userDetails = JSON.parse(e.details) + if(userDetails.googd_use_global === '1' && config.cloudUploaders && config.cloudUploaders.GoogleDrive){ + // { + // googd_accessKeyId: "", + // googd_secretAccessKey: "", + // googd_region: "", + // googd_bucket: "", + // googd_dir: "", + // } + userDetails = Object.assign(userDetails,config.cloudUploaders.GoogleDrive) + } + if(userDetails.googd_save === '1'){ + s.group[e.ke].googleDriveFolderIds = {} + var oAuth2Client + if(!s.group[e.ke].googleDriveOAuth2Client){ + oAuth2Client = initializeOAuth(userDetails.googd_credentials) + s.group[e.ke].googleDriveOAuth2Client = oAuth2Client + }else{ + oAuth2Client = s.group[e.ke].googleDriveOAuth2Client + } + if(userDetails.googd_code && userDetails.googd_code !== 'Authorized. Token Set.' && !s.group[e.ke].googleDrive){ + oAuth2Client.getToken(userDetails.googd_code, (err, token) => { + if (err) return console.error('Error retrieving access token', err) + oAuth2Client.setCredentials(token) + s.accountSettingsEdit({ + ke: e.ke, + uid: e.uid, + form: { + details: JSON.stringify(Object.assign(userDetails,{ + googd_code: 'Authorized. Token Set.', + googd_token: token, + })) + }, + },true) + }) + }else if(userDetails.googd_token && !s.group[e.ke].googleDrive){ + oAuth2Client.setCredentials(userDetails.googd_token) + const auth = oAuth2Client + const drive = google.drive({version: 'v3', auth}); + s.group[e.ke].googleDrive = drive + } + } + } + var unloadGoogleDriveForUser = function(user){ + s.group[user.ke].googleDrive = null + s.group[user.ke].googleDriveOAuth2Client = null + } + var deleteVideoFromGoogleDrive = function(e,video,callback){ + // e = user + var videoDetails = s.parseJSON(video.details) + s.group[e.ke].googleDrive.files.delete({ + fileId: videoDetails.id + }, function(err, resp){ + if (err) { + console.log('Error code:', err.code) + } else { + // console.log('Successfully deleted', file); + } + callback() + }) + } + var uploadVideoToGoogleDrive = async function(e,k){ + //e = video object + //k = temporary values + if(!k)k={}; + //cloud saver - Google Drive Storage + if(s.group[e.ke].googleDrive && s.group[e.ke].init.use_googd !== '0' && s.group[e.ke].init.googd_save === '1'){ + var ext = k.filename.split('.') + ext = ext[ext.length - 1] + var fileStream = fs.createReadStream(k.dir+k.filename); + fileStream.on('error', function (err) { + console.error(err) + }) + var bucketName = s.group[e.ke].init.googd_bucket + var saveLocation = s.group[e.ke].init.googd_dir+e.ke+'/'+e.mid+'/'+k.filename + s.group[e.ke].googleDrive.files.create({ + requestBody: { + name: k.filename, + parents: [await getVideoDirectoryId(e)], + mimeType: 'video/'+ext + }, + media: { + mimeType: 'video/'+ext, + body: fileStream + } + }).then(function(response){ + const data = response.data + + if(s.group[e.ke].init.googd_log === '1' && data && data.id){ + s.knexQuery({ + action: "insert", + table: "Cloud Videos", + insert: { + mid: e.mid, + ke: e.ke, + time: k.startTime, + status: 1, + details: s.s({ + type: 'googd', + id: data.id + }), + size: k.filesize, + end: k.endTime, + href: '' + } + }) + s.setCloudDiskUsedForGroup(e.ke,{ + amount : k.filesizeMB, + storageType : 'googd' + }) + s.purgeCloudDiskForGroup(e,'googd') + } + }).catch((err) => { + if(err){ + s.userLog(e,{type:lang['Google Drive Storage Upload Error'],msg:err}) + } + console.log(err) + }) + } + } + var onInsertTimelapseFrame = function(monitorObject,queryInfo,filePath){ + var e = monitorObject + if(s.group[e.ke].googd && s.group[e.ke].init.use_googd !== '0' && s.group[e.ke].init.googd_save === '1'){ + var fileStream = fs.createReadStream(filePath) + fileStream.on('error', function (err) { + console.error(err) + }) + var saveLocation = s.group[e.ke].init.googd_dir + e.ke + '/' + e.mid + '_timelapse/' + queryInfo.filename + s.group[e.ke].googleDrive.files.create({ + requestBody: { + name: saveLocation, + mimeType: 'image/jpeg' + }, + media: { + mimeType: 'image/jpeg', + body: fileStream + } + }).then(function(response){ + const data = response.data + if(err){ + s.userLog(e,{type:lang['Google Drive Storage Upload Error'],msg:err}) + } + if(s.group[e.ke].init.googd_log === '1' && data && data.id){ + s.knexQuery({ + action: "insert", + table: "Cloud Timelapse Frames", + insert: { + mid: queryInfo.mid, + ke: queryInfo.ke, + time: queryInfo.time, + details: s.s({ + type : 'googd', + id : data.id, + }), + size: queryInfo.size, + href: '' + } + }) + s.setCloudDiskUsedForGroup(e.ke,{ + amount : s.kilobyteToMegabyte(queryInfo.size), + storageType : 'googd' + },'timelapseFrames') + s.purgeCloudDiskForGroup(e,'googd','timelapseFrames') + } + }) + } + } + var onDeleteTimelapseFrameFromCloud = function(e,frame,callback){ + // e = user + var frameDetails = s.parseJSON(frame.details) + if(frameDetails.type !== 'googd'){ + return + } + s.group[e.ke].googleDrive.files.delete({ + fileId: frameDetails.id + }, function(err, resp){ + if (err) console.log(err); + callback() + }); + } + var onGetVideoData = async (video) => { + // e = user + var videoDetails = s.parseJSON(video.details) + const fileId = videoDetails.id + if(videoDetails.type !== 'googd'){ + return + } + return new Promise((resolve, reject) => { + s.group[video.ke].googleDrive.files + .get({fileId, alt: 'media'}, {responseType: 'stream'}) + .then(res => { + resolve(res.data) + }).catch(reject) + }) + } + // + app.get([config.webPaths.apiPrefix+':auth/googleDriveOAuthRequest/:ke'], (req,res) => { + s.auth(req.params,async (user) => { + var response = {ok: false} + var oAuth2Client = s.group[req.params.ke].googleDriveOAuth2Client + if(!oAuth2Client){ + oAuth2Client = initializeOAuth(s.group[req.params.ke].googd_credentials) + s.group[req.params.ke].googleDriveOAuth2Client = oAuth2Client + } + if(oAuth2Client){ + const authUrl = oAuth2Client.generateAuthUrl({ + access_type: 'offline', + scope: ['https://www.googleapis.com/auth/drive.file'], + }) + response.ok = true + response.authUrl = authUrl + } + s.closeJsonResponse(res,response) + }) + }); + //wasabi + s.addCloudUploader({ + name: 'googd', + loadGroupAppExtender: loadGoogleDriveForUser, + unloadGroupAppExtender: unloadGoogleDriveForUser, + insertCompletedVideoExtender: uploadVideoToGoogleDrive, + deleteVideoFromCloudExtensions: deleteVideoFromGoogleDrive, + cloudDiskUseStartupExtensions: cloudDiskUseStartupForGoogleDrive, + beforeAccountSave: beforeAccountSaveForGoogleDrive, + onAccountSave: cloudDiskUseStartupForGoogleDrive, + onInsertTimelapseFrame: onInsertTimelapseFrame, + onDeleteTimelapseFrameFromCloud: onDeleteTimelapseFrameFromCloud, + onGetVideoData: onGetVideoData + }) + return { + "evaluation": "details.use_googd !== '0'", + "name": lang["Google Drive"], + "color": "forestgreen", + "info": [ + { + "name": "detail=googd_save", + "selector":"autosave_googd", + "field": lang.Autosave, + "description": "", + "default": lang.No, + "example": "", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + "hidden": true, + "field": lang['OAuth Credentials'], + "name": "detail=googd_credentials", + "form-group-class": "autosave_googd_input autosave_googd_1", + "description": "", + "default": "", + "example": "", + "possible": "" + }, + { + "hidden": true, + "fieldType": "btn", + "attribute": `style="margin-bottom:1em" href="javascript:$.get(getApiPrefix() + '/googleDriveOAuthRequest/' + $user.ke,function(data){if(data.ok)window.open(data.authUrl, 'Google Drive Authentication', 'width=800,height=400');})"`, + "class": `btn-success`, + "form-group-class": "autosave_googd_input autosave_googd_1", + "btnContent": `   ${lang['Get Code']}`, + }, + { + "hidden": true, + "field": lang['OAuth Code'], + "name": "detail=googd_code", + "form-group-class": "autosave_googd_input autosave_googd_1", + "description": "", + "default": "", + "example": "", + "possible": "" + }, + { + "hidden": true, + "name": "detail=googd_log", + "field": lang['Save Links to Database'], + "fieldType": "select", + "selector": "h_googdsld", + "form-group-class":"autosave_googd_input autosave_googd_1", + "description": "", + "default": "", + "example": "", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + "hidden": true, + "name": "detail=use_googd_size_limit", + "field": lang['Use Max Storage Amount'], + "fieldType": "select", + "selector": "h_googdzl", + "form-group-class":"autosave_googd_input autosave_googd_1", + "form-group-class-pre-layer":"h_googdsld_input h_googdsld_1", + "description": "", + "default": "", + "example": "", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + "hidden": true, + "name": "detail=googd_size_limit", + "field": lang['Max Storage Amount'], + "form-group-class":"autosave_googd_input autosave_googd_1", + "form-group-class-pre-layer":"h_googdsld_input h_googdsld_1", + "description": "", + "default": "10000", + "example": "", + "possible": "" + }, + { + "hidden": true, + "name": "detail=googd_dir", + "field": lang['Save Directory'], + "form-group-class":"autosave_googd_input autosave_googd_1", + "description": "", + "default": "/", + "example": "", + "possible": "" + }, + ] + } +} diff --git a/libs/uploaders/loader.js b/libs/uploaders/loader.js index 985920c3..6e81ec28 100644 --- a/libs/uploaders/loader.js +++ b/libs/uploaders/loader.js @@ -10,6 +10,7 @@ module.exports = function(s){ s.beforeAccountSave(opt.beforeAccountSave) s.onAccountSave(opt.onAccountSave) s.cloudDisksLoader(opt.name) + if(opt.onGetVideoData)s.cloudDiskUseOnGetVideoDataExtensions[opt.name] = opt.onGetVideoData } s.addSimpleUploader = function(opt){ s.loadGroupAppExtender(opt.loadGroupAppExtender) diff --git a/libs/uploaders/s3based.js b/libs/uploaders/s3based.js index 243530db..24b2b66f 100644 --- a/libs/uploaders/s3based.js +++ b/libs/uploaders/s3based.js @@ -3,8 +3,8 @@ module.exports = function(s,config,lang){ //Wasabi Hot Cloud Storage var beforeAccountSaveForWasabiHotCloudStorage = function(d){ //d = save event - d.form.details.whcs_use_global=d.d.whcs_use_global - d.form.details.use_whcs=d.d.use_whcs + d.formDetails.whcs_use_global=d.d.whcs_use_global + d.formDetails.use_whcs=d.d.use_whcs } var cloudDiskUseStartupForWasabiHotCloudStorage = function(group,userDetails){ group.cloudDiskUse['whcs'].name = 'Wasabi Hot Cloud Storage' @@ -117,21 +117,24 @@ module.exports = function(s,config,lang){ if(s.group[e.ke].init.whcs_log === '1' && data && data.Location){ var cloudLink = data.Location cloudLink = fixCloudianUrl(e,cloudLink) - var save = [ - e.mid, - e.ke, - k.startTime, - 1, - s.s({ - type : 'whcs', - location : saveLocation - }), - k.filesize, - k.endTime, - cloudLink - ] - s.sqlQuery('INSERT INTO `Cloud Videos` (mid,ke,time,status,details,size,end,href) VALUES (?,?,?,?,?,?,?,?)',save) - s.setCloudDiskUsedForGroup(e,{ + s.knexQuery({ + action: "insert", + table: "Cloud Videos", + insert: { + mid: e.mid, + ke: e.ke, + time: k.startTime, + status: 1, + details: s.s({ + type : 'whcs', + location : saveLocation + }), + size: k.filesize, + end: k.endTime, + href: cloudLink + } + }) + s.setCloudDiskUsedForGroup(e.ke,{ amount : k.filesizeMB, storageType : 'whcs' }) @@ -159,19 +162,22 @@ module.exports = function(s,config,lang){ s.userLog(e,{type:lang['Wasabi Hot Cloud Storage Upload Error'],msg:err}) } if(s.group[e.ke].init.whcs_log === '1' && data && data.Location){ - var save = [ - queryInfo.mid, - queryInfo.ke, - queryInfo.time, - s.s({ - type : 'whcs', - location : saveLocation, - }), - queryInfo.size, - data.Location - ] - s.sqlQuery('INSERT INTO `Cloud Timelapse Frames` (mid,ke,time,details,size,href) VALUES (?,?,?,?,?,?)',save) - s.setCloudDiskUsedForGroup(e,{ + s.knexQuery({ + action: "insert", + table: "Cloud Timelapse Frames", + insert: { + mid: queryInfo.mid, + ke: queryInfo.ke, + time: queryInfo.time, + details: s.s({ + type : 'whcs', + location : saveLocation + }), + size: queryInfo.size, + href: data.Location + } + }) + s.setCloudDiskUsedForGroup(e.ke,{ amount : s.kilobyteToMegabyte(queryInfo.size), storageType : 'whcs' },'timelapseFrames') diff --git a/libs/uploaders/sftp.js b/libs/uploaders/sftp.js index 76404c30..37b9cce8 100644 --- a/libs/uploaders/sftp.js +++ b/libs/uploaders/sftp.js @@ -2,13 +2,12 @@ var fs = require('fs'); var ssh2SftpClient = require('node-ssh') module.exports = function(s,config,lang){ //SFTP - var sftpErr = function(err){ - // console.log(err) - s.userLog({mid:'$USER',ke:e.ke},{type:lang['SFTP Error'],msg:err.data || err}) + var sftpErr = function(groupKey,err){ + s.userLog({mid:'$USER',ke:groupKey},{type:lang['SFTP Error'],msg:err.data || err}) } var beforeAccountSaveForSftp = function(d){ //d = save event - d.form.details.use_sftp = d.d.use_sftp + d.formDetails.use_sftp = d.d.use_sftp } var loadSftpForUser = function(e){ // e = user @@ -37,7 +36,9 @@ module.exports = function(s,config,lang){ if(userDetails.sftp_username && userDetails.sftp_username !== '')connectionDetails.username = userDetails.sftp_username if(userDetails.sftp_password && userDetails.sftp_password !== '')connectionDetails.password = userDetails.sftp_password if(userDetails.sftp_privateKey && userDetails.sftp_privateKey !== '')connectionDetails.privateKey = userDetails.sftp_privateKey - sftp.connect(connectionDetails).catch(sftpErr) + sftp.connect(connectionDetails).catch((err) => { + sftpErr(e.ke,err) + }) s.group[e.ke].sftp = sftp } } @@ -54,14 +55,16 @@ module.exports = function(s,config,lang){ if(s.group[e.ke].sftp && s.group[e.ke].init.use_sftp !== '0' && s.group[e.ke].init.sftp_save === '1'){ var localPath = k.dir + k.filename var saveLocation = s.group[e.ke].init.sftp_dir + e.ke + '/' + e.mid + '/' + k.filename - s.group[e.ke].sftp.putFile(localPath, saveLocation).catch(sftpErr) + s.group[e.ke].sftp.putFile(localPath, saveLocation).catch((err) => { + sftpErr(e.ke,err) + }) } } var createSftpDirectory = function(monitorConfig){ var monitorSaveDirectory = s.group[monitorConfig.ke].init.sftp_dir + monitorConfig.ke + '/' + monitorConfig.mid s.group[monitorConfig.ke].sftp.mkdir(monitorSaveDirectory, true).catch(function(err){ if(err.code !== 'ERR_ASSERTION'){ - sftpErr(err) + sftpErr(monitorConfig.ke,err) } }) } diff --git a/libs/uploaders/webdav.js b/libs/uploaders/webdav.js index d6840fb9..ca9dc9ae 100644 --- a/libs/uploaders/webdav.js +++ b/libs/uploaders/webdav.js @@ -4,8 +4,8 @@ module.exports = function(s,config,lang){ // WebDAV var beforeAccountSaveForWebDav = function(d){ //d = save event - d.form.details.webdav_use_global=d.d.webdav_use_global - d.form.details.use_webdav=d.d.use_webdav + d.formDetails.webdav_use_global=d.d.webdav_use_global + d.formDetails.use_webdav=d.d.use_webdav } var cloudDiskUseStartupForWebDav = function(group,userDetails){ group.cloudDiskUse['webdav'].name = 'WebDAV' @@ -81,23 +81,26 @@ module.exports = function(s,config,lang){ fs.createReadStream(k.dir + k.filename).pipe(wfs.createWriteStream(webdavUploadDir + k.filename)) if(s.group[e.ke].init.webdav_log === '1'){ var webdavRemoteUrl = s.addUserPassToUrl(s.checkCorrectPathEnding(s.group[e.ke].init.webdav_url),s.group[e.ke].init.webdav_user,s.group[e.ke].init.webdav_pass) + s.group[e.ke].init.webdav_dir + e.ke + '/'+e.mid+'/'+k.filename - var save = [ - e.mid, - e.ke, - k.startTime, - 1, - s.s({ - type : 'webdav', - location : webdavUploadDir + k.filename - }), - k.filesize, - k.endTime, - webdavRemoteUrl - ] - s.sqlQuery('INSERT INTO `Cloud Videos` (mid,ke,time,status,details,size,end,href) VALUES (?,?,?,?,?,?,?,?)',save) - s.setCloudDiskUsedForGroup(e,{ - amount : k.filesizeMB, - storageType : 'webdav' + s.knexQuery({ + action: "insert", + table: "Cloud Videos", + insert: { + mid: e.mid, + ke: e.ke, + time: k.startTime, + status: 1, + details: s.s({ + type : 'webdav', + location : webdavUploadDir + k.filename + }), + size: k.filesize, + end: k.endTime, + href: webdavRemoteUrl + } + }) + s.setCloudDiskUsedForGroup(e.ke,{ + amount: k.filesizeMB, + storageType: 'webdav' }) s.purgeCloudDiskForGroup(e,'webdav') } diff --git a/libs/user.js b/libs/user.js index 2adef68b..8e6f1289 100644 --- a/libs/user.js +++ b/libs/user.js @@ -2,278 +2,63 @@ var fs = require('fs'); var events = require('events'); var spawn = require('child_process').spawn; var exec = require('child_process').exec; +var async = require("async"); module.exports = function(s,config,lang){ - s.purgeDiskForGroup = function(e){ - if(config.cron.deleteOverMax === true && s.group[e.ke] && s.group[e.ke].sizePurgeQueue){ - s.group[e.ke].sizePurgeQueue.push(1) - if(s.group[e.ke].sizePurging !== true){ - s.group[e.ke].sizePurging = true - var finish = function(){ - //remove value just used from queue - s.group[e.ke].sizePurgeQueue.shift() - //do next one - if(s.group[e.ke].sizePurgeQueue.length > 0){ - checkQueue() - }else{ - s.group[e.ke].sizePurging = false - s.sendDiskUsedAmountToClients(e) - } - } - var checkQueue = function(){ - //get first in queue - var currentPurge = s.group[e.ke].sizePurgeQueue[0] - var reRunCheck = function(){} - var deleteSetOfVideos = function(err,videos,storageIndex,callback){ - var videosToDelete = [] - var queryValues = [e.ke] - var completedCheck = 0 - if(videos){ - videos.forEach(function(video){ - video.dir = s.getVideoDirectory(video) + s.formattedTime(video.time) + '.' + video.ext - videosToDelete.push('(mid=? AND `time`=?)') - queryValues.push(video.mid) - queryValues.push(video.time) - fs.chmod(video.dir,0o777,function(err){ - fs.unlink(video.dir,function(err){ - ++completedCheck - if(err){ - fs.stat(video.dir,function(err){ - if(!err){ - s.file('delete',video.dir) - } - }) - } - if(videosToDelete.length === completedCheck){ - videosToDelete = videosToDelete.join(' OR ') - s.sqlQuery('DELETE FROM Videos WHERE ke =? AND ('+videosToDelete+')',queryValues,function(){ - reRunCheck() - }) - } - }) - }) - if(storageIndex){ - s.setDiskUsedForGroupAddStorage(e,{ - size: -(video.size/1000000), - storageIndex: storageIndex - }) - }else{ - s.setDiskUsedForGroup(e,-(video.size/1000000)) - } - s.tx({ - f: 'video_delete', - ff: 'over_max', - filename: s.formattedTime(video.time)+'.'+video.ext, - mid: video.mid, - ke: video.ke, - time: video.time, - end: s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss') - },'GRP_'+e.ke) - }) - }else{ - console.log(err) - } - if(videosToDelete.length === 0){ - if(callback)callback() - } - } - var deleteSetOfTimelapseFrames = function(err,frames,storageIndex,callback){ - var framesToDelete = [] - var queryValues = [e.ke] - var completedCheck = 0 - if(frames){ - frames.forEach(function(frame){ - var selectedDate = frame.filename.split('T')[0] - var dir = s.getTimelapseFrameDirectory(frame) - var fileLocationMid = `${dir}` + frame.filename - framesToDelete.push('(mid=? AND `time`=?)') - queryValues.push(frame.mid) - queryValues.push(frame.time) - fs.unlink(fileLocationMid,function(err){ - ++completedCheck - if(err){ - fs.stat(fileLocationMid,function(err){ - if(!err){ - s.file('delete',fileLocationMid) - } - }) - } - if(framesToDelete.length === completedCheck){ - framesToDelete = framesToDelete.join(' OR ') - s.sqlQuery('DELETE FROM `Timelapse Frames` WHERE ke =? AND ('+framesToDelete+')',queryValues,function(){ - reRunCheck() - }) - } - }) - if(storageIndex){ - s.setDiskUsedForGroupAddStorage(e,{ - size: -(frame.size/1000000), - storageIndex: storageIndex - },'timelapeFrames') - }else{ - s.setDiskUsedForGroup(e,-(frame.size/1000000),'timelapeFrames') - } - // s.tx({ - // f: 'timelapse_frame_delete', - // ff: 'over_max', - // filename: s.formattedTime(video.time)+'.'+video.ext, - // mid: video.mid, - // ke: video.ke, - // time: video.time, - // end: s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss') - // },'GRP_'+e.ke) - }) - }else{ - console.log(err) - } - if(framesToDelete.length === 0){ - if(callback)callback() - } - } - var deleteSetOfFileBinFiles = function(err,files,storageIndex,callback){ - var filesToDelete = [] - var queryValues = [e.ke] - var completedCheck = 0 - if(files){ - files.forEach(function(file){ - var dir = s.getFileBinDirectory(file) - var fileLocationMid = `${dir}` + file.name - filesToDelete.push('(mid=? AND `name`=?)') - queryValues.push(file.mid) - queryValues.push(file.name) - fs.unlink(fileLocationMid,function(err){ - ++completedCheck - if(err){ - fs.stat(fileLocationMid,function(err){ - if(!err){ - s.file('delete',fileLocationMid) - } - }) - } - if(filesToDelete.length === completedCheck){ - filesToDelete = filesToDelete.join(' OR ') - s.sqlQuery('DELETE FROM `Files` WHERE ke =? AND ('+filesToDelete+')',queryValues,function(){ - reRunCheck() - }) - } - }) - if(storageIndex){ - s.setDiskUsedForGroupAddStorage(e,{ - size: -(file.size/1000000), - storageIndex: storageIndex - },'fileBin') - }else{ - s.setDiskUsedForGroup(e,-(file.size/1000000),'fileBin') - } - }) - }else{ - console.log(err) - } - if(framesToDelete.length === 0){ - if(callback)callback() - } - } - var deleteMainVideos = function(callback){ - reRunCheck = function(){ - return deleteMainVideos(callback) - } - //run purge command - if(s.group[e.ke].usedSpaceVideos > (s.group[e.ke].sizeLimit * (s.group[e.ke].sizeLimitVideoPercent / 100) * config.cron.deleteOverMaxOffset)){ - s.sqlQuery('SELECT * FROM Videos WHERE status != 0 AND details NOT LIKE \'%"archived":"1"%\' AND ke=? AND details NOT LIKE \'%"dir"%\' ORDER BY `time` ASC LIMIT 3',[e.ke],function(err,rows){ - deleteSetOfVideos(err,rows,null,callback) - }) - }else{ - callback() - } - } - var deleteAddStorageVideos = function(callback){ - reRunCheck = function(){ - return deleteAddStorageVideos(callback) - } - var currentStorageNumber = 0 - var readStorageArray = function(finishedReading){ - setTimeout(function(){ - reRunCheck = readStorageArray - var storage = s.listOfStorage[currentStorageNumber] - if(!storage){ - //done all checks, move on to next user - callback() - return - } - var storageId = storage.value - if(storageId === '' || !s.group[e.ke].addStorageUse[storageId]){ - ++currentStorageNumber - readStorageArray() - return - } - var storageIndex = s.group[e.ke].addStorageUse[storageId] - //run purge command - if(storageIndex.usedSpace > (storageIndex.sizeLimit * (storageIndex.deleteOffset || config.cron.deleteOverMaxOffset))){ - s.sqlQuery('SELECT * FROM Videos WHERE status != 0 AND details NOT LIKE \'%"archived":"1"%\' AND ke=? AND details LIKE ? ORDER BY `time` ASC LIMIT 3',[e.ke,`%"dir":"${storage.value}"%`],function(err,rows){ - deleteSetOfVideos(err,rows,storageIndex,callback) - }) - }else{ - ++currentStorageNumber - readStorageArray() - } - }) - } - readStorageArray() - } - var deleteTimelapseFrames = function(callback){ - reRunCheck = function(){ - return deleteTimelapseFrames(callback) - } - //run purge command - if(s.group[e.ke].usedSpaceTimelapseFrames > (s.group[e.ke].sizeLimit * (s.group[e.ke].sizeLimitTimelapseFramesPercent / 100) * config.cron.deleteOverMaxOffset)){ - s.sqlQuery('SELECT * FROM `Timelapse Frames` WHERE ke=? AND details NOT LIKE \'%"archived":"1"%\' ORDER BY `time` ASC LIMIT 3',[e.ke],function(err,frames){ - deleteSetOfTimelapseFrames(err,frames,null,callback) - }) - }else{ - callback() - } - } - var deleteFileBinFiles = function(callback){ - if(config.deleteFileBinsOverMax === true){ - reRunCheck = function(){ - return deleteSetOfFileBinFiles(callback) - } - //run purge command - if(s.group[e.ke].usedSpaceFileBin > (s.group[e.ke].sizeLimit * (s.group[e.ke].sizeLimitFileBinPercent / 100) * config.cron.deleteOverMaxOffset)){ - s.sqlQuery('SELECT * FROM `Files` WHERE ke=? ORDER BY `time` ASC LIMIT 1',[e.ke],function(err,frames){ - deleteSetOfFileBinFiles(err,frames,null,callback) - }) - }else{ - callback() - } - }else{ - callback() - } - } - deleteMainVideos(function(){ - deleteTimelapseFrames(function(){ - deleteFileBinFiles(function(){ - deleteAddStorageVideos(function(){ - finish() + const { + deleteSetOfVideos, + deleteSetOfTimelapseFrames, + deleteSetOfFileBinFiles, + deleteAddStorageVideos, + deleteMainVideos, + deleteTimelapseFrames, + deleteFileBinFiles, + deleteCloudVideos, + deleteCloudTimelapseFrames, + } = require("./user/utils.js")(s,config,lang); + let purgeDiskGroup = () => {} + const runQuery = async.queue(function(groupKey, callback) { + purgeDiskGroup(groupKey,callback) + }, 1); + if(config.cron.deleteOverMax === true){ + purgeDiskGroup = (groupKey,callback) => { + if(s.group[groupKey]){ + if(s.group[groupKey].sizePurging !== true){ + s.group[groupKey].sizePurging = true + s.debugLog(`${groupKey} deleteMainVideos`) + deleteMainVideos(groupKey,() => { + s.debugLog(`${groupKey} deleteTimelapseFrames`) + deleteTimelapseFrames(groupKey,() => { + s.debugLog(`${groupKey} deleteFileBinFiles`) + deleteFileBinFiles(groupKey,() => { + s.debugLog(`${groupKey} deleteAddStorageVideos`) + deleteAddStorageVideos(groupKey,() => { + s.group[groupKey].sizePurging = false + s.sendDiskUsedAmountToClients(groupKey) + callback(); }) }) }) }) + }else{ + s.sendDiskUsedAmountToClients(groupKey) } - checkQueue() } - }else{ - s.sendDiskUsedAmountToClients(e) } } - s.setDiskUsedForGroup = function(e,bytes,storagePoint){ + s.purgeDiskForGroup = (groupKey) => { + return runQuery.push(groupKey,function(){ + //... + }) + } + s.setDiskUsedForGroup = function(groupKey,bytes,storagePoint){ //`bytes` will be used as the value to add or substract - if(s.group[e.ke] && s.group[e.ke].diskUsedEmitter){ - s.group[e.ke].diskUsedEmitter.emit('set',bytes,storagePoint) + if(s.group[groupKey] && s.group[groupKey].diskUsedEmitter){ + s.group[groupKey].diskUsedEmitter.emit('set',bytes,storagePoint) } } - s.setDiskUsedForGroupAddStorage = function(e,data,storagePoint){ - if(s.group[e.ke] && s.group[e.ke].diskUsedEmitter){ - s.group[e.ke].diskUsedEmitter.emit('setAddStorage',data,storagePoint) + s.setDiskUsedForGroupAddStorage = function(groupKey,data,storagePoint){ + if(s.group[groupKey] && s.group[groupKey].diskUsedEmitter){ + s.group[groupKey].diskUsedEmitter.emit('setAddStorage',data,storagePoint) } } s.purgeCloudDiskForGroup = function(e,storageType,storagePoint){ @@ -281,33 +66,44 @@ module.exports = function(s,config,lang){ s.group[e.ke].diskUsedEmitter.emit('purgeCloud',storageType,storagePoint) } } - s.setCloudDiskUsedForGroup = function(e,usage,storagePoint){ + s.setCloudDiskUsedForGroup = function(groupKey,usage,storagePoint){ //`usage` will be used as the value to add or substract - if(s.group[e.ke].diskUsedEmitter){ - s.group[e.ke].diskUsedEmitter.emit('setCloud',usage,storagePoint) + if(s.group[groupKey].diskUsedEmitter){ + s.group[groupKey].diskUsedEmitter.emit('setCloud',usage,storagePoint) } } - s.sendDiskUsedAmountToClients = function(e){ + s.sendDiskUsedAmountToClients = function(groupKey){ //send the amount used disk space to connected users - if(s.group[e.ke]&&s.group[e.ke].init){ + if(s.group[groupKey]&&s.group[groupKey].init){ s.tx({ f: 'diskUsed', - size: s.group[e.ke].usedSpace, - usedSpace: s.group[e.ke].usedSpace, - usedSpaceVideos: s.group[e.ke].usedSpaceVideos, - usedSpaceFilebin: s.group[e.ke].usedSpaceFilebin, - usedSpaceTimelapseFrames: s.group[e.ke].usedSpaceTimelapseFrames, - limit: s.group[e.ke].sizeLimit, - addStorage: s.group[e.ke].addStorageUse - },'GRP_'+e.ke); + size: s.group[groupKey].usedSpace, + usedSpace: s.group[groupKey].usedSpace, + usedSpaceVideos: s.group[groupKey].usedSpaceVideos, + usedSpaceFilebin: s.group[groupKey].usedSpaceFilebin, + usedSpaceTimelapseFrames: s.group[groupKey].usedSpaceTimelapseFrames, + limit: s.group[groupKey].sizeLimit, + addStorage: s.group[groupKey].addStorageUse + },'GRP_'+groupKey); } } //user log s.userLog = function(e,x){ if(e.id && !e.mid)e.mid = e.id if(!x||!e.mid){return} - if((e.details&&e.details.sqllog==='1')||e.mid.indexOf('$')>-1){ - s.sqlQuery('INSERT INTO Logs (ke,mid,info) VALUES (?,?,?)',[e.ke,e.mid,s.s(x)]); + if( + (e.details && e.details.sqllog === '1') || + e.mid.indexOf('$') > -1 + ){ + s.knexQuery({ + action: "insert", + table: "Logs", + insert: { + ke: e.ke, + mid: e.mid, + info: s.s(x), + } + }) } s.tx({f:'log',ke:e.ke,mid:e.mid,log:x,time:s.timeObject()},'GRPLOG_'+e.ke); } @@ -334,22 +130,31 @@ module.exports = function(s,config,lang){ s.group[e.ke].sizeLimitTimelapseFramesPercent = parseFloat(s.group[e.ke].init.size_timelapse_percent) || 5 s.group[e.ke].sizeLimitFileBinPercent = parseFloat(s.group[e.ke].init.size_filebin_percent) || 5 //save global used space as megabyte value - s.group[e.ke].usedSpace = s.group[e.ke].usedSpace || ((e.size || 0) / 1000000) + s.group[e.ke].usedSpace = s.group[e.ke].usedSpace || ((e.size || 0) / 1048576) //emit the changes to connected users - s.sendDiskUsedAmountToClients(e) + s.sendDiskUsedAmountToClients(e.ke) } s.loadGroupApps = function(e){ // e = user if(!s.group[e.ke].init){ s.group[e.ke].init={}; } - s.sqlQuery('SELECT * FROM Users WHERE ke=? AND details NOT LIKE ?',[e.ke,'%"sub"%'],function(ar,r){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Users", + where: [ + ['ke','=',e.ke], + ['details','NOT LIKE',`%"sub"%`], + ], + limit: 1 + },(err,r) => { if(r && r[0]){ r = r[0]; - ar = JSON.parse(r.details); + const details = JSON.parse(r.details); //load extenders s.loadGroupAppExtensions.forEach(function(extender){ - extender(r,ar) + extender(r,details) }) //disk Used Emitter if(!s.group[e.ke].diskUsedEmitter){ @@ -381,82 +186,15 @@ module.exports = function(s,config,lang){ break; } }) - s.group[e.ke].diskUsedEmitter.on('purgeCloud',function(storageType,storagePoint){ - if(config.cron.deleteOverMax === true){ - var cloudDisk = s.group[e.ke].cloudDiskUse[storageType] - //set queue processor - var finish=function(){ - // s.sendDiskUsedAmountToClients(e) - } - var deleteVideos = function(){ - //run purge command - if(cloudDisk.sizeLimitCheck && cloudDisk.usedSpace > (cloudDisk.sizeLimit*config.cron.deleteOverMaxOffset)){ - s.sqlQuery('SELECT * FROM `Cloud Videos` WHERE status != 0 AND ke=? AND details LIKE \'%"type":"'+storageType+'"%\' ORDER BY `time` ASC LIMIT 2',[e.ke],function(err,videos){ - var videosToDelete = [] - var queryValues = [e.ke] - if(!videos)return console.log(err) - videos.forEach(function(video){ - video.dir = s.getVideoDirectory(video) + s.formattedTime(video.time) + '.' + video.ext - videosToDelete.push('(mid=? AND `time`=?)') - queryValues.push(video.mid) - queryValues.push(video.time) - s.setCloudDiskUsedForGroup(e,{ - amount : -(video.size/1000000), - storageType : storageType - }) - s.deleteVideoFromCloudExtensionsRunner(e,storageType,video) - }) - if(videosToDelete.length > 0){ - videosToDelete = videosToDelete.join(' OR ') - s.sqlQuery('DELETE FROM `Cloud Videos` WHERE ke =? AND ('+videosToDelete+')',queryValues,function(){ - deleteVideos() - }) - }else{ - finish() - } - }) - }else{ - finish() - } - } - var deleteTimelapseFrames = function(callback){ - reRunCheck = function(){ - return deleteTimelapseFrames(callback) - } - //run purge command - if(cloudDisk.usedSpaceTimelapseFrames > (cloudDisk.sizeLimit * (s.group[e.ke].sizeLimitTimelapseFramesPercent / 100) * config.cron.deleteOverMaxOffset)){ - s.sqlQuery('SELECT * FROM `Cloud Timelapse Frames` WHERE ke=? AND details NOT LIKE \'%"archived":"1"%\' ORDER BY `time` ASC LIMIT 3',[e.ke],function(err,frames){ - var framesToDelete = [] - var queryValues = [e.ke] - if(!frames)return console.log(err) - frames.forEach(function(frame){ - frame.dir = s.getVideoDirectory(frame) + s.formattedTime(frame.time) + '.' + frame.ext - framesToDelete.push('(mid=? AND `time`=?)') - queryValues.push(frame.mid) - queryValues.push(frame.time) - s.setCloudDiskUsedForGroup(e,{ - amount : -(frame.size/1000000), - storageType : storageType - }) - s.deleteVideoFromCloudExtensionsRunner(e,storageType,frame) - }) - s.sqlQuery('DELETE FROM `Cloud Timelapse Frames` WHERE ke =? AND ('+framesToDelete+')',queryValues,function(){ - deleteTimelapseFrames(callback) - }) - }) - }else{ - callback() - } - } - deleteVideos(function(){ - deleteTimelapseFrames(function(){ + if(config.cron.deleteOverMax === true){ + s.group[e.ke].diskUsedEmitter.on('purgeCloud',function(storageType,storagePoint){ + deleteCloudVideos(storageType,storagePoint,function(){ + deleteCloudTimelapseFrames(storageType,storagePoint,function(){ }) }) - }else{ - // s.sendDiskUsedAmountToClients(e) - } - }) + }) + } //s.setDiskUsedForGroup s.group[e.ke].diskUsedEmitter.on('set',function(currentChange,storageType){ //validate current values @@ -482,7 +220,7 @@ module.exports = function(s,config,lang){ break; } //remove value just used from queue - s.sendDiskUsedAmountToClients(e) + s.sendDiskUsedAmountToClients(e.ke) }) s.group[e.ke].diskUsedEmitter.on('setAddStorage',function(data,storageType){ var currentSize = data.size @@ -510,60 +248,76 @@ module.exports = function(s,config,lang){ break; } //remove value just used from queue - s.sendDiskUsedAmountToClients(e) + s.sendDiskUsedAmountToClients(e.ke) }) } - Object.keys(ar).forEach(function(v){ - s.group[e.ke].init[v] = ar[v] + Object.keys(details).forEach(function(v){ + s.group[e.ke].init[v] = details[v] }) } }) } - s.accountSettingsEdit = function(d){ - s.sqlQuery('SELECT details FROM Users WHERE ke=? AND uid=?',[d.ke,d.uid],function(err,r){ - if(r&&r[0]){ - r=r[0]; - d.d=JSON.parse(r.details); - if(!d.d.sub || d.d.user_change !== "0"){ + s.accountSettingsEdit = function(d,dontRunExtensions){ + s.knexQuery({ + action: "select", + columns: "details", + table: "Users", + where: [ + ['ke','=',d.ke], + ['uid','=',d.uid], + ] + },(err,r) => { + if(r && r[0]){ + r = r[0]; + const details = JSON.parse(r.details); + if(!details.sub || details.user_change !== "0"){ if(d.cnid){ - if(d.d.get_server_log==='1'){ + if(details.get_server_log === '1'){ s.clientSocketConnection[d.cnid].join('GRPLOG_'+d.ke) }else{ s.clientSocketConnection[d.cnid].leave('GRPLOG_'+d.ke) } } ///unchangeable from client side, so reset them in case they did. - d.form.details=JSON.parse(d.form.details) - s.beforeAccountSaveExtensions.forEach(function(extender){ - extender(d) - }) + var form = d.form + var formDetails = JSON.parse(form.details) + if(!dontRunExtensions){ + s.beforeAccountSaveExtensions.forEach(function(extender){ + extender({ + form: form, + formDetails: formDetails, + d: details + }) + }) + } //admin permissions - d.form.details.permissions=d.d.permissions - d.form.details.edit_size=d.d.edit_size - d.form.details.edit_days=d.d.edit_days - d.form.details.use_admin=d.d.use_admin - d.form.details.use_ldap=d.d.use_ldap - d.form.details.landing_page=d.d.landing_page + formDetails.permissions = details.permissions + formDetails.edit_size = details.edit_size + formDetails.edit_days = details.edit_days + formDetails.use_admin = details.use_admin + formDetails.use_ldap = details.use_ldap + formDetails.landing_page = details.landing_page //check - if(d.d.edit_days == "0"){ - d.form.details.days = d.d.days; + if(details.edit_days == "0"){ + formDetails.days = details.days; } - if(d.d.edit_size == "0"){ - d.form.details.size = d.d.size; + if(details.edit_size == "0"){ + formDetails.size = details.size; + formDetails.addStorage = details.addStorage; } - if(d.d.sub){ - d.form.details.sub=d.d.sub; - if(d.d.monitors){d.form.details.monitors=d.d.monitors;} - if(d.d.allmonitors){d.form.details.allmonitors=d.d.allmonitors;} - if(d.d.monitor_create){d.form.details.monitor_create=d.d.monitor_create;} - if(d.d.video_delete){d.form.details.video_delete=d.d.video_delete;} - if(d.d.video_view){d.form.details.video_view=d.d.video_view;} - if(d.d.monitor_edit){d.form.details.monitor_edit=d.d.monitor_edit;} - if(d.d.size){d.form.details.size=d.d.size;} - if(d.d.days){d.form.details.days=d.d.days;} - delete(d.form.details.mon_groups) + if(details.sub){ + formDetails.sub = details.sub; + if(details.monitors){formDetails.monitors = details.monitors;} + if(details.allmonitors){formDetails.allmonitors = details.allmonitors;} + if(details.monitor_create){formDetails.monitor_create = details.monitor_create;} + if(details.video_delete){formDetails.video_delete = details.video_delete;} + if(details.video_view){formDetails.video_view = details.video_view;} + if(details.monitor_edit){formDetails.monitor_edit = details.monitor_edit;} + if(details.size){formDetails.size = details.size;} + if(details.days){formDetails.days = details.days;} + delete(formDetails.mon_groups) } - var newSize = parseFloat(d.form.details.size) || 10000 + var newSize = parseFloat(formDetails.size) || 10000 //load addStorageUse var currentStorageNumber = 0 var readStorageArray = function(){ @@ -578,7 +332,7 @@ module.exports = function(s,config,lang){ readStorageArray() return } - var detailContainer = d.form.details || s.group[r.ke].init + var detailContainer = formDetails || s.group[r.ke].init var storageId = path var detailsContainerAddStorage = s.parseJSON(detailContainer.addStorage) if(!s.group[d.ke].addStorageUse[storageId])s.group[d.ke].addStorageUse[storageId] = {} @@ -594,30 +348,43 @@ module.exports = function(s,config,lang){ } readStorageArray() /// - d.form.details = JSON.stringify(d.form.details) + formDetails = JSON.stringify(s.mergeDeep(details,formDetails)) /// - d.set=[],d.ar=[]; - if(d.form.pass&&d.form.pass!==''){d.form.pass=s.createHash(d.form.pass);}else{delete(d.form.pass)}; - delete(d.form.password_again); - d.for=Object.keys(d.form); - d.for.forEach(function(v){ - d.set.push(v+'=?'),d.ar.push(d.form[v]); - }); - d.ar.push(d.ke),d.ar.push(d.uid); - s.sqlQuery('UPDATE Users SET '+d.set.join(',')+' WHERE ke=? AND uid=?',d.ar,function(err,r){ - if(!d.d.sub){ - var user = Object.assign(d.form,{ke : d.ke}) - var userDetails = JSON.parse(d.form.details) + const updateQuery = {} + if(form.pass && form.pass !== ''){ + form.pass = s.createHash(form.pass) + }else{ + delete(form.pass) + } + delete(form.password_again) + Object.keys(form).forEach(function(key){ + const value = form[key] + updateQuery[key] = value + }) + s.knexQuery({ + action: "update", + table: "Users", + update: updateQuery, + where: [ + ['ke','=',d.ke], + ['uid','=',d.uid], + ] + },() => { + if(!details.sub){ + var user = Object.assign(form,{ke : d.ke}) + var userDetails = JSON.parse(formDetails) s.group[d.ke].sizeLimit = parseFloat(newSize) - s.onAccountSaveExtensions.forEach(function(extender){ - extender(s.group[d.ke],userDetails,user) - }) - s.unloadGroupAppExtensions.forEach(function(extender){ - extender(user) - }) - s.loadGroupApps(d) + if(!dontRunExtensions){ + s.onAccountSaveExtensions.forEach(function(extender){ + extender(s.group[d.ke],userDetails,user) + }) + s.unloadGroupAppExtensions.forEach(function(extender){ + extender(user) + }) + s.loadGroupApps(d) + } } - if(d.cnid)s.tx({f:'user_settings_change',uid:d.uid,ke:d.ke,form:d.form},d.cnid) + if(d.cnid)s.tx({f:'user_settings_change',uid:d.uid,ke:d.ke,form:form},d.cnid) }) } } @@ -625,7 +392,17 @@ module.exports = function(s,config,lang){ } s.findPreset = function(presetQueryVals,callback){ //presetQueryVals = [ke, type, name] - s.sqlQuery("SELECT * FROM Presets WHERE ke=? AND type=? AND name=? LIMIT 1",presetQueryVals,function(err,presets){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Presets", + where: [ + ['ke','=',presetQueryVals[0]], + ['type','=',presetQueryVals[1]], + ['name','=',presetQueryVals[2]], + ], + limit: 1 + },function(err,presets) { var preset var notFound = false if(presets && presets[0]){ diff --git a/libs/user/utils.js b/libs/user/utils.js new file mode 100644 index 00000000..8495fb27 --- /dev/null +++ b/libs/user/utils.js @@ -0,0 +1,463 @@ +var fs = require('fs'); +module.exports = (s,config,lang) => { + const deleteSetOfVideos = function(options,callback){ + const groupKey = options.groupKey + const err = options.err + const videos = options.videos + const storageIndex = options.storageIndex + const reRunCheck = options.reRunCheck + var completedCheck = 0 + var whereGroup = [] + var whereQuery = [ + ['ke','=',groupKey], + ] + if(videos){ + videos.forEach(function(video){ + video.dir = s.getVideoDirectory(video) + s.formattedTime(video.time) + '.' + video.ext + const queryGroup = { + mid: video.mid, + time: video.time, + } + if(whereGroup.length > 0)queryGroup.__separator = 'or' + whereGroup.push(queryGroup) + fs.chmod(video.dir,0o777,function(err){ + fs.unlink(video.dir,function(err){ + ++completedCheck + if(err){ + fs.stat(video.dir,function(err){ + if(!err){ + s.file('delete',video.dir) + } + }) + } + const whereGroupLength = whereGroup.length + if(whereGroupLength > 0 && whereGroupLength === completedCheck){ + whereQuery[1] = whereGroup + s.knexQuery({ + action: "delete", + table: "Videos", + where: whereQuery + },(err,info) => { + setTimeout(reRunCheck,1000) + }) + } + }) + }) + if(storageIndex){ + s.setDiskUsedForGroupAddStorage(groupKey,{ + size: -(video.size/1048576), + storageIndex: storageIndex + }) + }else{ + s.setDiskUsedForGroup(groupKey,-(video.size/1048576)) + } + s.tx({ + f: 'video_delete', + ff: 'over_max', + filename: s.formattedTime(video.time)+'.'+video.ext, + mid: video.mid, + ke: video.ke, + time: video.time, + end: s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss') + },'GRP_'+groupKey) + }) + }else{ + console.log(err) + } + if(whereGroup.length === 0){ + if(callback)callback() + } + } + const deleteSetOfTimelapseFrames = function(options,callback){ + const groupKey = options.groupKey + const err = options.err + const frames = options.frames + const storageIndex = options.storageIndex + var whereGroup = [] + var whereQuery = [ + ['ke','=',groupKey], + [] + ] + var completedCheck = 0 + if(frames){ + frames.forEach(function(frame){ + var selectedDate = frame.filename.split('T')[0] + var dir = s.getTimelapseFrameDirectory(frame) + var fileLocationMid = `${dir}` + frame.filename + const queryGroup = { + mid: video.mid, + time: video.time, + } + if(whereGroup.length > 0)queryGroup.__separator = 'or' + whereGroup.push(queryGroup) + fs.unlink(fileLocationMid,function(err){ + ++completedCheck + if(err){ + fs.stat(fileLocationMid,function(err){ + if(!err){ + s.file('delete',fileLocationMid) + } + }) + } + const whereGroupLength = whereGroup.length + if(whereGroupLength > 0 && whereGroupLength === completedCheck){ + whereQuery[1] = whereGroup + s.knexQuery({ + action: "delete", + table: "Timelapse Frames", + where: whereQuery + },() => { + deleteTimelapseFrames(groupKey,callback) + }) + } + }) + if(storageIndex){ + s.setDiskUsedForGroupAddStorage(groupKey,{ + size: -(frame.size/1048576), + storageIndex: storageIndex + },'timelapeFrames') + }else{ + s.setDiskUsedForGroup(groupKey,-(frame.size/1048576),'timelapeFrames') + } + // s.tx({ + // f: 'timelapse_frame_delete', + // ff: 'over_max', + // filename: s.formattedTime(video.time)+'.'+video.ext, + // mid: video.mid, + // ke: video.ke, + // time: video.time, + // end: s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss') + // },'GRP_'+groupKey) + }) + }else{ + console.log(err) + } + if(whereGroup.length === 0){ + if(callback)callback() + } + } + const deleteSetOfFileBinFiles = function(options,callback){ + const groupKey = options.groupKey + const err = options.err + const frames = options.frames + const storageIndex = options.storageIndex + var whereGroup = [] + var whereQuery = [ + ['ke','=',groupKey], + [] + ] + var completedCheck = 0 + if(files){ + files.forEach(function(file){ + var dir = s.getFileBinDirectory(file) + var fileLocationMid = `${dir}` + file.name + const queryGroup = { + mid: file.mid, + name: file.name, + } + if(whereGroup.length > 0)queryGroup.__separator = 'or' + whereGroup.push(queryGroup) + fs.unlink(fileLocationMid,function(err){ + ++completedCheck + if(err){ + fs.stat(fileLocationMid,function(err){ + if(!err){ + s.file('delete',fileLocationMid) + } + }) + } + const whereGroupLength = whereGroup.length + if(whereGroupLength > 0 && whereGroupLength === completedCheck){ + whereQuery[1] = whereGroup + s.knexQuery({ + action: "delete", + table: "Files", + where: whereQuery + },() => { + deleteFileBinFiles(groupKey,callback) + }) + } + }) + if(storageIndex){ + s.setDiskUsedForGroupAddStorage(groupKey,{ + size: -(file.size/1048576), + storageIndex: storageIndex + },'fileBin') + }else{ + s.setDiskUsedForGroup(groupKey,-(file.size/1048576),'fileBin') + } + }) + }else{ + console.log(err) + } + if(whereGroup.length === 0){ + if(callback)callback() + } + } + const deleteAddStorageVideos = function(groupKey,callback){ + reRunCheck = function(){ + s.debugLog('deleteAddStorageVideos') + return deleteAddStorageVideos(groupKey,callback) + } + var currentStorageNumber = 0 + var readStorageArray = function(){ + setTimeout(function(){ + reRunCheck = readStorageArray + var storage = s.listOfStorage[currentStorageNumber] + if(!storage){ + //done all checks, move on to next user + callback() + return + } + var storageId = storage.value + if(storageId === '' || !s.group[groupKey].addStorageUse[storageId]){ + ++currentStorageNumber + readStorageArray() + return + } + var storageIndex = s.group[groupKey].addStorageUse[storageId] + //run purge command + if(storageIndex.usedSpace > (storageIndex.sizeLimit * (storageIndex.deleteOffset || config.cron.deleteOverMaxOffset))){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Videos", + where: [ + ['ke','=',groupKey], + ['status','!=','0'], + ['details','NOT LIKE',`%"archived":"1"%`], + ['details','LIKE',`%"dir":"${storage.value}"%`], + ], + orderBy: ['time','asc'], + limit: 3 + },(err,rows) => { + deleteSetOfVideos({ + groupKey: groupKey, + err: err, + videos: rows, + storageIndex: storageIndex, + reRunCheck: () => { + return readStorageArray() + } + },callback) + }) + }else{ + ++currentStorageNumber + readStorageArray() + } + }) + } + readStorageArray() + } + const deleteMainVideos = function(groupKey,callback){ + // //run purge command + // s.debugLog('!!!!!!!!!!!deleteMainVideos') + // s.debugLog('s.group[groupKey].usedSpaceVideos > (s.group[groupKey].sizeLimit * (s.group[groupKey].sizeLimitVideoPercent / 100) * config.cron.deleteOverMaxOffset)') + // s.debugLog(s.group[groupKey].usedSpaceVideos > (s.group[groupKey].sizeLimit * (s.group[groupKey].sizeLimitVideoPercent / 100) * config.cron.deleteOverMaxOffset)) + // s.debugLog('s.group[groupKey].usedSpaceVideos') + // s.debugLog(s.group[groupKey].usedSpaceVideos) + // s.debugLog('s.group[groupKey].sizeLimit * (s.group[groupKey].sizeLimitVideoPercent / 100) * config.cron.deleteOverMaxOffset') + // s.debugLog(s.group[groupKey].sizeLimit * (s.group[groupKey].sizeLimitVideoPercent / 100) * config.cron.deleteOverMaxOffset) + // s.debugLog('s.group[groupKey].sizeLimitVideoPercent / 100') + // s.debugLog(s.group[groupKey].sizeLimitVideoPercent / 100) + // s.debugLog('s.group[groupKey].sizeLimit') + // s.debugLog(s.group[groupKey].sizeLimit) + if(s.group[groupKey].usedSpaceVideos > (s.group[groupKey].sizeLimit * (s.group[groupKey].sizeLimitVideoPercent / 100) * config.cron.deleteOverMaxOffset)){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Videos", + where: [ + ['ke','=',groupKey], + ['status','!=','0'], + ['details','NOT LIKE',`%"archived":"1"%`], + ['details','NOT LIKE',`%"dir"%`], + ], + orderBy: ['time','asc'], + limit: 3 + },(err,rows) => { + deleteSetOfVideos({ + groupKey: groupKey, + err: err, + videos: rows, + storageIndex: null, + reRunCheck: () => { + return deleteMainVideos(groupKey,callback) + } + },callback) + }) + }else{ + callback() + } + } + const deleteTimelapseFrames = function(groupKey,callback){ + //run purge command + if(s.group[groupKey].usedSpaceTimelapseFrames > (s.group[groupKey].sizeLimit * (s.group[groupKey].sizeLimitTimelapseFramesPercent / 100) * config.cron.deleteOverMaxOffset)){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Timelapse Frames", + where: [ + ['ke','=',groupKey], + ['details','NOT LIKE',`%"archived":"1"%`], + ], + orderBy: ['time','asc'], + limit: 3 + },(err,frames) => { + deleteSetOfTimelapseFrames({ + groupKey: groupKey, + err: err, + frames: frames, + storageIndex: null + },callback) + }) + }else{ + callback() + } + } + const deleteFileBinFiles = function(groupKey,callback){ + if(config.deleteFileBinsOverMax === true){ + //run purge command + if(s.group[groupKey].usedSpaceFileBin > (s.group[groupKey].sizeLimit * (s.group[groupKey].sizeLimitFileBinPercent / 100) * config.cron.deleteOverMaxOffset)){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Files", + where: [ + ['ke','=',groupKey], + ], + orderBy: ['time','asc'], + limit: 1 + },(err,frames) => { + deleteSetOfFileBinFiles({ + groupKey: groupKey, + err: err, + frames: frames, + storageIndex: null + },callback) + }) + }else{ + callback() + } + }else{ + callback() + } + } + const deleteCloudVideos = function(groupKey,storageType,storagePoint,callback){ + const whereGroup = [] + const cloudDisk = s.group[groupKey].cloudDiskUse[storageType] + //run purge command + if(cloudDisk.sizeLimitCheck && cloudDisk.usedSpace > (cloudDisk.sizeLimit * config.cron.deleteOverMaxOffset)){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Cloud Videos", + where: [ + ['status','!=','0'], + ['ke','=',groupKey], + ['details','LIKE',`%"type":"${storageType}"%`], + ], + orderBy: ['time','asc'], + limit: 2 + },function(err,videos) { + if(!videos)return console.log(err) + var whereQuery = [ + ['ke','=',groupKey], + ] + var didOne = false + videos.forEach(function(video){ + video.dir = s.getVideoDirectory(video) + s.formattedTime(video.time) + '.' + video.ext + const queryGroup = { + mid: video.mid, + time: video.time, + } + if(whereGroup.length > 0)queryGroup.__separator = 'or' + whereGroup.push(queryGroup) + s.setCloudDiskUsedForGroup(e.ke,{ + amount : -(video.size/1048576), + storageType : storageType + }) + s.deleteVideoFromCloudExtensionsRunner(e,storageType,video) + }) + const whereGroupLength = whereGroup.length + if(whereGroupLength > 0){ + whereQuery[1] = whereGroup + s.knexQuery({ + action: "delete", + table: "Cloud Videos", + where: whereQuery + },() => { + deleteCloudVideos(groupKey,storageType,storagePoint,callback) + }) + }else{ + callback() + } + }) + }else{ + callback() + } + } + const deleteCloudTimelapseFrames = function(groupKey,storageType,storagePoint,callback){ + const whereGroup = [] + var cloudDisk = s.group[e.ke].cloudDiskUse[storageType] + //run purge command + if(cloudDisk.usedSpaceTimelapseFrames > (cloudDisk.sizeLimit * (s.group[e.ke].sizeLimitTimelapseFramesPercent / 100) * config.cron.deleteOverMaxOffset)){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Cloud Timelapse Frames", + where: [ + ['ke','=',e.ke], + ['details','NOT LIKE',`%"archived":"1"%`], + ], + orderBy: ['time','asc'], + limit: 3 + },(err,frames) => { + if(!frames)return console.log(err) + var whereQuery = [ + ['ke','=',e.ke], + ] + frames.forEach(function(frame){ + frame.dir = s.getVideoDirectory(frame) + s.formattedTime(frame.time) + '.' + frame.ext + const queryGroup = { + mid: frame.mid, + time: frame.time, + } + if(whereGroup.length > 0)queryGroup.__separator = 'or' + whereGroup.push(queryGroup) + s.setCloudDiskUsedForGroup(e.ke,{ + amount : -(frame.size/1048576), + storageType : storageType + }) + s.deleteVideoFromCloudExtensionsRunner(e,storageType,frame) + }) + const whereGroupLength = whereGroup.length + if(whereGroupLength > 0){ + whereQuery[1] = whereGroup + s.knexQuery({ + action: "delete", + table: "Cloud Timelapse Frames", + where: whereQuery + },() => { + deleteCloudTimelapseFrames(groupKey,storageType,storagePoint,callback) + }) + }else{ + callback() + } + }) + }else{ + callback() + } + } + return { + deleteSetOfVideos: deleteSetOfVideos, + deleteSetOfTimelapseFrames: deleteSetOfTimelapseFrames, + deleteSetOfFileBinFiles: deleteSetOfFileBinFiles, + deleteAddStorageVideos: deleteAddStorageVideos, + deleteMainVideos: deleteMainVideos, + deleteTimelapseFrames: deleteTimelapseFrames, + deleteFileBinFiles: deleteFileBinFiles, + deleteCloudVideos: deleteCloudVideos, + deleteCloudTimelapseFrames: deleteCloudTimelapseFrames, + } +} diff --git a/libs/videoDropInServer.js b/libs/videoDropInServer.js index 00ac5327..744e758a 100644 --- a/libs/videoDropInServer.js +++ b/libs/videoDropInServer.js @@ -31,8 +31,19 @@ module.exports = function(s,config,lang,app,io){ details.dir = monitor.details.dir } var timeNow = new Date(s.nameToTime(filename)) - s.sqlQuery('INSERT INTO `Timelapse Frames` (ke,mid,details,filename,size,time) VALUES (?,?,?,?,?,?)',[ke,mid,s.s(details),filename,fileStats.size,timeNow]) - s.setDiskUsedForGroup(monitor,fileStats.size / 1000000) + s.knexQuery({ + action: "insert", + table: "Timelapse Frames", + insert: { + ke: ke, + mid: mid, + details: s.s(details), + filename: filename, + size: fileStats.size, + time: timeNow, + } + }) + s.setDiskUsedForGroup(monitor.ke,fileStats.size / 1048576) } // else{ // s.insertDatabaseRow( diff --git a/libs/videos.js b/libs/videos.js index 9c6550ac..ef5da5cb 100644 --- a/libs/videos.js +++ b/libs/videos.js @@ -64,17 +64,20 @@ module.exports = function(s,config,lang){ k.details.dir = e.details.dir } if(config.useUTC === true)k.details.isUTC = config.useUTC; - var save = [ - e.mid, - e.ke, - k.startTime, - e.ext, - 1, - s.s(k.details), - k.filesize, - k.endTime, - ] - s.sqlQuery('INSERT INTO Videos (mid,ke,time,ext,status,details,size,end) VALUES (?,?,?,?,?,?,?,?)',save,function(err){ + s.knexQuery({ + action: "insert", + table: "Videos", + insert: { + ke: e.ke, + mid: e.mid, + time: k.startTime, + ext: e.ext, + status: 1, + details: s.s(k.details), + size: k.filesize, + end: k.endTime, + } + },(err) => { if(callback)callback(err) fs.chmod(k.dir+k.file,0o777,function(err){ @@ -90,7 +93,11 @@ module.exports = function(s,config,lang){ e.dir = s.getVideoDirectory(e) k.dir = e.dir.toString() if(s.group[e.ke].activeMonitors[e.id].childNode){ - s.cx({f:'insertCompleted',d:s.group[e.ke].rawMonitorConfigurations[e.id],k:k},s.group[e.ke].activeMonitors[e.id].childNodeId); + s.cx({ + f: 'insertCompleted', + d: s.group[e.ke].rawMonitorConfigurations[e.id], + k: k + },s.group[e.ke].activeMonitors[e.id].childNodeId); }else{ //get file directory k.fileExists = fs.existsSync(k.dir+k.file) @@ -108,10 +115,10 @@ module.exports = function(s,config,lang){ } if(k.fileExists===true){ //close video row - k.details = {} + k.details = k.details && k.details instanceof Object ? k.details : {} k.stat = fs.statSync(k.dir+k.file) k.filesize = k.stat.size - k.filesizeMB = parseFloat((k.filesize/1000000).toFixed(2)) + k.filesizeMB = parseFloat((k.filesize/1048576).toFixed(2)) k.startTime = new Date(s.nameToTime(k.file)) k.endTime = new Date(k.endTime || k.stat.mtime) @@ -126,58 +133,54 @@ module.exports = function(s,config,lang){ if(!e.ext){e.ext = k.filename.split('.')[1]} //send event for completed recording if(config.childNodes.enabled === true && config.childNodes.mode === 'child' && config.childNodes.host){ + const response = { + mid: e.mid, + ke: e.ke, + filename: k.filename, + d: s.cleanMonitorObject(e), + filesize: k.filesize, + time: s.timeObject(k.startTime).format('YYYY-MM-DD HH:mm:ss'), + end: s.timeObject(k.endTime).format('YYYY-MM-DD HH:mm:ss') + } fs.createReadStream(k.dir+k.filename,{ highWaterMark: 500 }) .on('data',function(data){ - s.cx({ + s.cx(Object.assign(response,{ f:'created_file_chunk', - mid:e.mid, - ke:e.ke, - chunk:data, - filename:k.filename, - d:s.cleanMonitorObject(e), - filesize:e.filesize, - time:s.timeObject(k.startTime).format(), - end:s.timeObject(k.endTime).format() - }) + chunk: data, + })) }) .on('close',function(){ clearTimeout(s.group[e.ke].activeMonitors[e.id].recordingChecker) clearTimeout(s.group[e.ke].activeMonitors[e.id].streamChecker) - s.cx({ + s.cx(Object.assign(response,{ f:'created_file', - mid:e.id, - ke:e.ke, - filename:k.filename, - d:s.cleanMonitorObject(e), - filesize:k.filesize, - time:s.timeObject(k.startTime).format(), - end:s.timeObject(k.endTime).format() - }) + })) }) }else{ var href = '/videos/'+e.ke+'/'+e.mid+'/'+k.filename if(config.useUTC === true)href += '?isUTC=true'; s.txWithSubPermissions({ - f:'video_build_success', - hrefNoAuth:href, - filename:k.filename, - mid:e.mid, - ke:e.ke, - time:k.startTime, - size:k.filesize, - end:k.endTime + f: 'video_build_success', + hrefNoAuth: href, + filename: k.filename, + mid: e.mid, + ke: e.ke, + time: k.startTime, + size: k.filesize, + end: k.endTime, + events: k.events && k.events.length > 0 ? k.events : null },'GRP_'+e.ke,'video_view') //purge over max - s.purgeDiskForGroup(e) + s.purgeDiskForGroup(e.ke) //send new diskUsage values var storageIndex = s.getVideoStorageIndex(e) if(storageIndex){ - s.setDiskUsedForGroupAddStorage(e,{ + s.setDiskUsedForGroupAddStorage(e.ke,{ size: k.filesizeMB, storageIndex: storageIndex }) }else{ - s.setDiskUsedForGroup(e,k.filesizeMB) + s.setDiskUsedForGroup(e.ke,k.filesizeMB) } s.onBeforeInsertCompletedVideoExtensions.forEach(function(extender){ extender(e,k) @@ -210,8 +213,17 @@ module.exports = function(s,config,lang){ time = e.time } time = new Date(time) - var queryValues = [e.id,e.ke,time]; - s.sqlQuery('SELECT * FROM Videos WHERE `mid`=? AND `ke`=? AND `time`=?',queryValues,function(err,r){ + const whereQuery = { + ke: e.ke, + mid: e.id, + time: time, + } + s.knexQuery({ + action: "select", + columns: "*", + table: "Videos", + where: whereQuery + },(err,r) => { if(r && r[0]){ r = r[0] fs.chmod(e.dir+filename,0o777,function(err){ @@ -225,14 +237,18 @@ module.exports = function(s,config,lang){ },'GRP_'+e.ke); var storageIndex = s.getVideoStorageIndex(e) if(storageIndex){ - s.setDiskUsedForGroupAddStorage(e,{ - size: -(r.size / 1000000), + s.setDiskUsedForGroupAddStorage(e.ke,{ + size: -(r.size / 1048576), storageIndex: storageIndex }) }else{ - s.setDiskUsedForGroup(e,-(r.size / 1000000)) + s.setDiskUsedForGroup(e.ke,-(r.size / 1048576)) } - s.sqlQuery('DELETE FROM Videos WHERE `mid`=? AND `ke`=? AND `time`=?',queryValues,function(err){ + s.knexQuery({ + action: "delete", + table: "Videos", + where: whereQuery + },(err) => { if(err){ s.systemLog(lang['File Delete Error'] + ' : '+e.ke+' : '+' : '+e.id,err) } @@ -253,9 +269,8 @@ module.exports = function(s,config,lang){ } s.deleteListOfVideos = function(videos){ var deleteSetOfVideos = function(videos){ - var query = 'DELETE FROM Videos WHERE ' - var videoQuery = [] - var queryValues = [] + const whereQuery = [] + var didOne = false; videos.forEach(function(video){ s.checkDetails(video) //e = video object @@ -276,37 +291,45 @@ module.exports = function(s,config,lang){ time = video.time } time = new Date(time) - fs.chmod(video.dir+filename,0o777,function(err){ + fs.chmod(video.dir + filename,0o777,function(err){ s.tx({ f: 'video_delete', filename: filename, - mid: video.id, + mid: video.mid, ke: video.ke, time: s.nameToTime(filename), end: s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss') },'GRP_'+video.ke); var storageIndex = s.getVideoStorageIndex(video) if(storageIndex){ - s.setDiskUsedForGroupAddStorage(video,{ - size: -(video.size / 1000000), + s.setDiskUsedForGroupAddStorage(video.ke,{ + size: -(video.size / 1048576), storageIndex: storageIndex }) }else{ - s.setDiskUsedForGroup(video,-(video.size / 1000000)) + s.setDiskUsedForGroup(video.ke,-(video.size / 1048576)) } - fs.unlink(video.dir+filename,function(err){ - fs.stat(video.dir+filename,function(err){ + fs.unlink(video.dir + filename,function(err){ + fs.stat(video.dir + filename,function(err){ if(!err){ - s.file('delete',video.dir+filename) + s.file('delete',video.dir + filename) } }) }) }) - videoQuery.push('(`mid`=? AND `ke`=? AND `time`=?)') - queryValues = queryValues.concat([video.id,video.ke,time]) + const queryGroup = { + ke: video.ke, + mid: video.mid, + time: time, + } + if(whereQuery.length > 0)queryGroup.__separator = 'or' + whereQuery.push(queryGroup) }) - query += videoQuery.join(' OR ') - s.sqlQuery(query,queryValues,function(err){ + s.knexQuery({ + action: "delete", + table: "Videos", + where: whereQuery + },(err) => { if(err){ s.systemLog(lang['List of Videos Delete Error'],err) } @@ -338,11 +361,24 @@ module.exports = function(s,config,lang){ s.deleteVideoFromCloud = function(e){ // e = video object s.checkDetails(e) - var videoSelector = [e.id,e.ke,new Date(e.time)] - s.sqlQuery('SELECT * FROM `Cloud Videos` WHERE `mid`=? AND `ke`=? AND `time`=?',videoSelector,function(err,r){ + const whereQuery = { + ke: e.ke, + mid: e.mid, + time: new Date(e.time), + } + s.knexQuery({ + action: "select", + columns: "*", + table: "Cloud Videos", + where: whereQuery + },(err,r) => { if(r&&r[0]){ r = r[0] - s.sqlQuery('DELETE FROM `Cloud Videos` WHERE `mid`=? AND `ke`=? AND `time`=?',videoSelector,function(){ + s.knexQuery({ + action: "delete", + table: "Cloud Videos", + where: whereQuery + },(err,r) => { s.deleteVideoFromCloudExtensionsRunner(e,r) }) }else{ @@ -374,18 +410,23 @@ module.exports = function(s,config,lang){ } fiveRecentFiles.forEach(function(filename){ if(/T[0-9][0-9]-[0-9][0-9]-[0-9][0-9]./.test(filename)){ - var queryValues = [ - monitor.ke, - monitor.mid, - s.nameToTime(filename) - ] - s.sqlQuery('SELECT * FROM Videos WHERE ke=? AND mid=? AND time=? LIMIT 1',queryValues,function(err,rows){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Videos", + where: [ + ['ke','=',monitor.ke], + ['mid','=',monitor.mid], + ['time','=',s.nameToTime(filename)], + ], + limit: 1 + },(err,rows) => { if(!err && (!rows || !rows[0])){ ++orphanedFilesCount var video = rows[0] s.insertCompletedVideo(monitor,{ file : filename - },function(){ + },() => { fileComplete() }) }else{ @@ -463,14 +504,14 @@ module.exports = function(s,config,lang){ createLocation = fileLocationMid } }) - if(concatFiles.length > 30){ + if(concatFiles.length > framesPerSecond){ var commandTempLocation = `${s.dir.streams}${ke}/${mid}/mergeJpegs_${finalFileName}.sh` var finalMp4OutputLocation = `${s.dir.fileBin}${ke}/${mid}/${finalFileName}.mp4` if(!s.group[ke].activeMonitors[mid].buildingTimelapseVideo){ if(!fs.existsSync(finalMp4OutputLocation)){ var currentFile = 0 var completionTimeout - var commandString = `ffmpeg -y -pattern_type glob -f image2pipe -vcodec mjpeg -r ${framesPerSecond} -analyzeduration 10 -i - -q:v 1 -c:v libx264 -r ${framesPerSecond} "${finalMp4OutputLocation}"` + var commandString = `ffmpeg -y -f image2pipe -vcodec mjpeg -r ${framesPerSecond} -analyzeduration 10 -i - -q:v 1 -c:v libx264 -r ${framesPerSecond} "${finalMp4OutputLocation}"` fs.writeFileSync(commandTempLocation,commandString) var videoBuildProcess = spawn('sh',[commandTempLocation]) videoBuildProcess.stderr.on('data',function(data){ @@ -486,8 +527,19 @@ module.exports = function(s,config,lang){ var timeNow = new Date() var fileStats = fs.statSync(finalMp4OutputLocation) var details = {} - s.sqlQuery('INSERT INTO `Files` (ke,mid,details,name,size,time) VALUES (?,?,?,?,?,?)',[ke,mid,s.s(details),finalFileName + '.mp4',fileStats.size,timeNow]) - s.setDiskUsedForGroup({ke: ke},fileStats.size / 1000000,'fileBin') + s.knexQuery({ + action: "insert", + table: "Files", + insert: { + ke: ke, + mid: mid, + details: s.s(details), + name: finalFileName + '.mp4', + size: fileStats.size, + time: timeNow, + } + }) + s.setDiskUsedForGroup(ke,fileStats.size / 1048576,'fileBin') fs.unlink(commandTempLocation,function(){ }) diff --git a/libs/webServer.js b/libs/webServer.js index 26069382..a335c955 100644 --- a/libs/webServer.js +++ b/libs/webServer.js @@ -57,7 +57,9 @@ module.exports = function(s,config,lang,io){ //SSL options var wellKnownDirectory = s.mainDirectory + '/web/.well-known' if(fs.existsSync(wellKnownDirectory))app.use('/.well-known',express.static(wellKnownDirectory)) + config.sslEnabled = false if(config.ssl&&config.ssl.key&&config.ssl.cert){ + config.sslEnabled = true config.ssl.key=fs.readFileSync(s.checkRelativePath(config.ssl.key),'utf8') config.ssl.cert=fs.readFileSync(s.checkRelativePath(config.ssl.cert),'utf8') if(config.ssl.port === undefined){ @@ -93,6 +95,12 @@ module.exports = function(s,config,lang,io){ path:s.checkCorrectPathEnding(config.webPaths.super)+'socket.io', transports: ['websocket'] }) + app.use(function(req, res, next) { + if(!req.secure) { + return res.redirect(['https://', req.hostname,":",config.ssl.port, req.url].join('')); + } + next(); + }) } //start HTTP var server = http.createServer(app); diff --git a/libs/webServerAdminPaths.js b/libs/webServerAdminPaths.js index 10b2733a..986fc85b 100644 --- a/libs/webServerAdminPaths.js +++ b/libs/webServerAdminPaths.js @@ -25,15 +25,18 @@ module.exports = function(s,config,lang,app){ var mail = form.mail || s.getPostData(req,'mail',false) if(form){ var keys = ['details'] - var condition = [] - var value = [] - keys.forEach(function(v){ - condition.push(v+'=?') - if(form[v] instanceof Object)form[v] = JSON.stringify(form[v]) - value.push(form[v]) + const updateQuery = { + details: s.stringJSON(form.details) + } + s.knexQuery({ + action: "update", + table: "Users", + update: updateQuery, + where: [ + ['ke','=',req.params.ke], + ['uid','=',uid], + ] }) - value = value.concat([req.params.ke,uid]) - s.sqlQuery("UPDATE Users SET "+condition.join(',')+" WHERE ke=? AND uid=?",value) s.tx({ f: 'edit_sub_account', ke: req.params.ke, @@ -42,7 +45,15 @@ module.exports = function(s,config,lang,app){ form: form },'ADM_'+req.params.ke) endData.ok = true - s.sqlQuery("SELECT * FROM API WHERE ke=? AND uid=?",[req.params.ke,uid],function(err,rows){ + s.knexQuery({ + action: "select", + columns: "*", + table: "API", + where: [ + ['ke','=',req.params.ke], + ['uid','=',uid], + ] + },function(err,rows){ if(rows && rows[0]){ rows.forEach(function(row){ delete(s.api[row.code]) @@ -71,13 +82,36 @@ module.exports = function(s,config,lang,app){ var form = s.getPostData(req) || {} var uid = form.uid || s.getPostData(req,'uid',false) var mail = form.mail || s.getPostData(req,'mail',false) - s.sqlQuery('DELETE FROM Users WHERE uid=? AND ke=? AND mail=?',[uid,req.params.ke,mail]) - s.sqlQuery("SELECT * FROM API WHERE ke=? AND uid=?",[req.params.ke,uid],function(err,rows){ + s.knexQuery({ + action: "delete", + table: "Users", + where: { + ke: req.params.ke, + uid: uid, + mail: mail, + } + }) + s.knexQuery({ + action: "select", + columns: "*", + table: "API", + where: [ + ['ke','=',req.params.ke], + ['uid','=',uid], + ] + },function(err,rows){ if(rows && rows[0]){ rows.forEach(function(row){ delete(s.api[row.code]) }) - s.sqlQuery('DELETE FROM API WHERE uid=? AND ke=?',[uid,req.params.ke]) + s.knexQuery({ + action: "delete", + table: "API", + where: { + ke: req.params.ke, + uid: uid, + } + }) } }) s.tx({ @@ -112,8 +146,15 @@ module.exports = function(s,config,lang,app){ var form = s.getPostData(req) if(form.mail !== '' && form.pass !== ''){ if(form.pass === form.password_again || form.pass === form.pass_again){ - s.sqlQuery('SELECT * FROM Users WHERE mail=?',[form.mail],function(err,r) { - if(r&&r[0]){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Users", + where: [ + ['mail','=',form.mail], + ] + },function(err,r){ + if(r && r[0]){ //found one exist endData.msg = 'Email address is in use.' }else{ @@ -125,7 +166,17 @@ module.exports = function(s,config,lang,app){ sub: "1", allmonitors: "1" }) - s.sqlQuery('INSERT INTO Users (ke,uid,mail,pass,details) VALUES (?,?,?,?,?)',[req.params.ke,newId,form.mail,s.createHash(form.pass),details]) + s.knexQuery({ + action: "insert", + table: "Users", + insert: { + ke: req.params.ke, + uid: newId, + mail: form.mail, + pass: s.createHash(form.pass), + details: details, + } + }) s.tx({ f: 'add_sub_account', details: details, @@ -199,8 +250,22 @@ module.exports = function(s,config,lang,app){ s.userLog(s.group[req.params.ke].rawMonitorConfigurations[req.params.id],{type:'Monitor Deleted',msg:'by user : '+user.uid}); req.params.delete=1;s.camera('stop',req.params); s.tx({f:'monitor_delete',uid:user.uid,mid:req.params.id,ke:req.params.ke},'GRP_'+req.params.ke); - s.sqlQuery('DELETE FROM Monitors WHERE ke=? AND mid=?',[req.params.ke,req.params.id]) - // s.sqlQuery('DELETE FROM Files WHERE ke=? AND mid=?',[req.params.ke,req.params.id]) + s.knexQuery({ + action: "delete", + table: "Monitors", + where: { + ke: req.params.ke, + mid: req.params.id, + } + }) + // s.knexQuery({ + // action: "delete", + // table: "Files", + // where: { + // ke: req.params.ke, + // mid: req.params.id, + // } + // }) if(req.query.deleteFiles === 'true'){ //videos s.dir.addStorage.forEach(function(v,n){ @@ -250,28 +315,28 @@ module.exports = function(s,config,lang,app){ } var form = s.getPostData(req) if(form){ - var insert = { + const insertQuery = { ke : req.params.ke, uid : user.uid, code : s.gid(30), ip : form.ip, details : s.stringJSON(form.details) } - var escapes = [] - Object.keys(insert).forEach(function(column){ - escapes.push('?') - }); - s.sqlQuery('INSERT INTO API ('+Object.keys(insert).join(',')+') VALUES ('+escapes.join(',')+')',Object.values(insert),function(err,r){ - insert.time = s.formattedTime(new Date,'YYYY-DD-MM HH:mm:ss'); + s.knexQuery({ + action: "insert", + table: "API", + insert: insertQuery + },(err,r) => { + insertQuery.time = s.formattedTime(new Date,'YYYY-DD-MM HH:mm:ss'); if(!err){ s.tx({ f: 'api_key_added', uid: user.uid, - form: insert + form: insertQuery },'GRP_' + req.params.ke) endData.ok = true } - endData.api = insert + endData.api = insertQuery s.closeJsonResponse(res,endData) }) }else{ @@ -305,16 +370,15 @@ module.exports = function(s,config,lang,app){ s.closeJsonResponse(res,endData) return } - var row = { - ke : req.params.ke, - uid : user.uid, - code : form.code - } - var where = [] - Object.keys(row).forEach(function(column){ - where.push(column+'=?') - }) - s.sqlQuery('DELETE FROM API WHERE '+where.join(' AND '),Object.values(row),function(err,r){ + s.knexQuery({ + action: "delete", + table: "API", + where: { + ke: req.params.ke, + uid: user.uid, + code: form.code, + } + },(err,r) => { if(!err){ s.tx({ f: 'api_key_deleted', @@ -345,15 +409,16 @@ module.exports = function(s,config,lang,app){ var endData = { ok : false } - var row = { + const whereQuery = { ke : req.params.ke, uid : user.uid } - var where = [] - Object.keys(row).forEach(function(column){ - where.push(column+'=?') - }) - s.sqlQuery('SELECT * FROM API WHERE '+where.join(' AND '),Object.values(row),function(err,rows){ + s.knexQuery({ + action: "select", + columns: "*", + table: "API", + where: whereQuery + },function(err,rows) { if(rows && rows[0]){ rows.forEach(function(row){ row.details = JSON.parse(row.details) @@ -383,16 +448,22 @@ module.exports = function(s,config,lang,app){ s.closeJsonResponse(res,endData) return } - s.sqlQuery("SELECT * FROM Presets WHERE ke=? AND type=?",[req.params.ke,'monitorStates'],function(err,presets){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Presets", + where: [ + ['ke','=',req.params.ke], + ['type','=','monitorStates'], + ] + },function(err,presets) { if(presets && presets[0]){ endData.ok = true presets.forEach(function(preset){ preset.details = JSON.parse(preset.details) }) - endData.presets = presets - }else{ - endData.msg = user.lang['State Configuration Not Found'] } + endData.presets = presets || [] s.closeJsonResponse(res,endData) }) }) @@ -437,7 +508,11 @@ module.exports = function(s,config,lang,app){ details: s.s(details), type: 'monitorStates' } - s.sqlQuery('INSERT INTO Presets ('+Object.keys(insertData).join(',')+') VALUES (?,?,?,?)',Object.values(insertData)) + s.knexQuery({ + action: "insert", + table: "Presets", + insert: insertData + }) s.tx({ f: 'add_group_state', details: details, @@ -449,7 +524,17 @@ module.exports = function(s,config,lang,app){ var details = Object.assign(preset.details,{ monitors : form.monitors }) - s.sqlQuery('UPDATE Presets SET details=? WHERE ke=? AND name=?',[s.s(details),req.params.ke,req.params.stateName]) + s.knexQuery({ + action: "update", + table: "Presets", + update: { + details: s.s(details) + }, + where: [ + ['ke','=',req.params.ke], + ['name','=',req.params.stateName], + ] + }) s.tx({ f: 'edit_group_state', details: details, @@ -467,7 +552,14 @@ module.exports = function(s,config,lang,app){ endData.msg = user.lang['State Configuration Not Found'] s.closeJsonResponse(res,endData) }else{ - s.sqlQuery('DELETE FROM Presets WHERE ke=? AND name=?',[req.params.ke,req.params.stateName],function(err){ + s.knexQuery({ + action: "delete", + table: "Presets", + where: { + ke: req.params.ke, + name: req.params.stateName, + } + },(err) => { if(!err){ endData.msg = lang["Deleted State Configuration"] endData.ok = true diff --git a/libs/webServerPaths.js b/libs/webServerPaths.js index 44ba1d8b..1e86d1f4 100644 --- a/libs/webServerPaths.js +++ b/libs/webServerPaths.js @@ -8,12 +8,15 @@ var execSync = require('child_process').execSync; var exec = require('child_process').exec; var spawn = require('child_process').spawn; var httpProxy = require('http-proxy'); -var onvif = require('node-onvif'); var proxy = httpProxy.createProxyServer({}) var ejs = require('ejs'); var fileupload = require("express-fileupload"); module.exports = function(s,config,lang,app,io){ - if(config.productType==='Pro'){ + const { + ptzControl, + setPresetForCurrentPosition + } = require('./control/ptz.js')(s,config,lang,app,io) + if(config.productType === 'Pro'){ var LdapAuth = require('ldapauth-fork'); } s.renderPage = function(req,res,paths,passables,callback){ @@ -29,8 +32,8 @@ module.exports = function(s,config,lang,app,io){ //cb = callback //res = response, only needed for express (http server) //request = request, only needed for express (http server) - s.checkChildProxy = function(params,cb,res,req){ - if(s.group[params.ke] && s.group[params.ke].activeMonitors[params.id] && s.group[params.ke].activeMonitors[params.id].childNode){ + s.checkChildProxy = function(params,cb,res,req) { + if(s.group[params.ke] && s.group[params.ke].activeMonitors && s.group[params.ke].activeMonitors[params.id] && s.group[params.ke].activeMonitors[params.id].childNode){ var url = 'http://' + s.group[params.ke].activeMonitors[params.id].childNode// + req.originalUrl proxy.web(req, res, { target: url }) }else{ @@ -71,7 +74,7 @@ module.exports = function(s,config,lang,app,io){ app.use(bodyParser.json()); app.use(bodyParser.urlencoded({extended: true})); app.use(function (req,res,next){ - res.header("Access-Control-Allow-Origin",req.headers.origin); + res.header("Access-Control-Allow-Origin",'*'); next() }) app.set('views', s.mainDirectory + '/web'); @@ -86,7 +89,18 @@ module.exports = function(s,config,lang,app,io){ if(s.group[req.params.ke]&&s.group[req.params.ke].users[req.params.auth]){ delete(s.api[req.params.auth]); delete(s.group[req.params.ke].users[req.params.auth]); - s.sqlQuery("UPDATE Users SET auth=? WHERE auth=? AND ke=? AND uid=?",['',req.params.auth,req.params.ke,req.params.id]) + s.knexQuery({ + action: "update", + table: "Users", + update: { + auth: '', + }, + where: [ + ['auth','=',req.params.auth], + ['ke','=',req.params.ke], + ['uid','=',req.params.id], + ] + }) res.end(s.prettyPrint({ok:true,msg:'You have been logged out, session key is now inactive.'})) }else{ res.end(s.prettyPrint({ok:false,msg:'This group key does not exist or this user is not logged in.'})) @@ -118,12 +132,12 @@ module.exports = function(s,config,lang,app,io){ * API : Get User Info */ app.get(config.webPaths.apiPrefix+':auth/userInfo/:ke',function (req,res){ - req.ret={ok:false}; + var response = {ok:false}; res.setHeader('Content-Type', 'application/json'); s.auth(req.params,function(user){ - req.ret.ok=true - req.ret.user=user - res.end(s.prettyPrint(req.ret)); + response.ok = true + response.user = user + res.end(s.prettyPrint(response)); },res,req); }) //login function @@ -144,6 +158,7 @@ module.exports = function(s,config,lang,app,io){ s.checkCorrectPathEnding(config.webPaths.admin)+':screen', s.checkCorrectPathEnding(config.webPaths.super)+':screen', ],function (req,res){ + var response = {ok: false}; req.ip = s.getClientIp(req) var screenChooser = function(screen){ var search = function(screen){ @@ -187,7 +202,7 @@ module.exports = function(s,config,lang,app,io){ } if(req.query.json=='true'){ delete(data.config) - data.ok=true; + data.ok = true; res.setHeader('Content-Type', 'application/json'); res.end(s.prettyPrint(data)) }else{ @@ -237,16 +252,23 @@ module.exports = function(s,config,lang,app,io){ ip: req.ip } } - if(board==='super'){ + if(board === 'super'){ s.userLog(logTo,logData) }else{ - s.sqlQuery('SELECT ke,uid,details FROM Users WHERE mail=?',[req.body.mail],function(err,r) { - if(r&&r[0]){ + s.knexQuery({ + action: "select", + columns: "ke,uid,details", + table: "Users", + where: [ + ['mail','=',req.body.mail], + ] + },(err,r) => { + if(r && r[0]){ r = r[0] - r.details=JSON.parse(r.details); - r.lang=s.getLanguageFile(r.details.lang) - logData.id=r.uid - logData.type=r.lang['Authentication Failed'] + r.details = JSON.parse(r.details) + r.lang = s.getLanguageFile(r.details.lang) + logData.id = r.uid + logData.type = r.lang['Authentication Failed'] logTo.ke = r.ke } s.userLog(logTo,logData) @@ -256,11 +278,19 @@ module.exports = function(s,config,lang,app,io){ checkRoute = function(r){ switch(req.body.function){ case'cam': - s.sqlQuery('SELECT * FROM Monitors WHERE ke=? AND type=?',[r.ke,"dashcam"],function(err,rr){ - req.resp.mons=rr; + s.knexQuery({ + action: "select", + columns: "*", + table: "Monitors", + where: [ + ['ke','=',r.ke], + ['type','=','dashcam'], + ] + },(err,rr) => { + response.mons = rr renderPage(config.renderPaths.dashcam,{ // config: s.getConfigWithBranding(req.hostname), - $user: req.resp, + $user: response, lang: r.lang, define: s.getDefinitonFile(r.details.lang), customAutoLoad: s.customAutoLoadTree @@ -268,11 +298,19 @@ module.exports = function(s,config,lang,app,io){ }) break; case'streamer': - s.sqlQuery('SELECT * FROM Monitors WHERE ke=? AND type=?',[r.ke,"socket"],function(err,rr){ - req.resp.mons=rr; + s.knexQuery({ + action: "select", + columns: "*", + table: "Monitors", + where: [ + ['ke','=',r.ke], + ['type','=','socket'], + ] + },(err,rr) => { + response.mons=rr; renderPage(config.renderPaths.streamer,{ // config: s.getConfigWithBranding(req.hostname), - $user: req.resp, + $user: response, lang: r.lang, define: s.getDefinitonFile(r.details.lang), customAutoLoad: s.customAutoLoadTree @@ -281,11 +319,26 @@ module.exports = function(s,config,lang,app,io){ break; case'admin': if(!r.details.sub){ - s.sqlQuery('SELECT uid,mail,details FROM Users WHERE ke=? AND details LIKE \'%"sub"%\'',[r.ke],function(err,rr) { - s.sqlQuery('SELECT * FROM Monitors WHERE ke=?',[r.ke],function(err,rrr) { + s.knexQuery({ + action: "select", + columns: "uid,mail,details", + table: "Users", + where: [ + ['ke','=',r.ke], + ['details','LIKE','%"sub"%'], + ] + },(err,rr) => { + s.knexQuery({ + action: "select", + columns: "*", + table: "Monitors", + where: [ + ['ke','=',r.ke], + ] + },(err,rrr) => { renderPage(config.renderPaths.admin,{ config: s.getConfigWithBranding(req.hostname), - $user: req.resp, + $user: response, $subs: rr, $mons: rrr, lang: r.lang, @@ -301,7 +354,7 @@ module.exports = function(s,config,lang,app,io){ chosenRender = r.details.landing_page } renderPage(config.renderPaths[chosenRender],{ - $user:req.resp, + $user:response, config: s.getConfigWithBranding(req.hostname), lang:r.lang, define:s.getDefinitonFile(r.details.lang), @@ -318,7 +371,7 @@ module.exports = function(s,config,lang,app,io){ chosenRender = r.details.landing_page } renderPage(config.renderPaths[chosenRender],{ - $user:req.resp, + $user: response, config: s.getConfigWithBranding(req.hostname), lang:r.lang, define:s.getDefinitonFile(r.details.lang), @@ -334,15 +387,40 @@ module.exports = function(s,config,lang,app,io){ } if(req.body.mail&&req.body.pass){ req.default=function(){ - s.sqlQuery('SELECT * FROM Users WHERE mail=? AND pass=?',[req.body.mail,s.createHash(req.body.pass)],function(err,r) { - req.resp={ok:false}; - if(!err&&r&&r[0]){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Users", + where: [ + ['mail','=',req.body.mail], + ['pass','=',s.createHash(req.body.pass)], + ], + limit: 1 + },(err,r) => { + if(!err && r && r[0]){ r=r[0];r.auth=s.md5(s.gid()); - s.sqlQuery("UPDATE Users SET auth=? WHERE ke=? AND uid=?",[r.auth,r.ke,r.uid]) - req.resp={ok:true,auth_token:r.auth,ke:r.ke,uid:r.uid,mail:r.mail,details:r.details}; - r.details=JSON.parse(r.details); - r.lang=s.getLanguageFile(r.details.lang) - req.factorAuth=function(cb){ + s.knexQuery({ + action: "update", + table: "Users", + update: { + auth: r.auth + }, + where: [ + ['ke','=',r.ke], + ['uid','=',r.uid], + ] + }) + response = { + ok: true, + auth_token: r.auth, + ke: r.ke, + uid: r.uid, + mail: r.mail, + details: r.details + }; + r.details = JSON.parse(r.details); + r.lang = s.getLanguageFile(r.details.lang) + const factorAuth = function(cb){ req.params.auth = r.auth req.params.ke = r.ke if(r.details.factorAuth === "1"){ @@ -352,12 +430,16 @@ module.exports = function(s,config,lang,app,io){ if(!r.details.acceptedMachines[req.body.machineID]){ req.complete=function(){ s.factorAuth[r.ke][r.uid].function = req.body.function - s.factorAuth[r.ke][r.uid].info = req.resp + s.factorAuth[r.ke][r.uid].info = response clearTimeout(s.factorAuth[r.ke][r.uid].expireAuth) - s.factorAuth[r.ke][r.uid].expireAuth=setTimeout(function(){ + s.factorAuth[r.ke][r.uid].expireAuth = setTimeout(function(){ s.deleteFactorAuth(r) },1000*60*15) - renderPage(config.renderPaths.factorAuth,{$user:req.resp,lang:r.lang}) + renderPage(config.renderPaths.factorAuth,{$user:{ + ke:r.ke, + uid:r.uid, + mail:r.mail + },lang:r.lang}) } if(!s.factorAuth[r.ke]){s.factorAuth[r.ke]={}} if(!s.factorAuth[r.ke][r.uid]){ @@ -377,19 +459,27 @@ module.exports = function(s,config,lang,app,io){ } } if(r.details.sub){ - s.sqlQuery('SELECT details FROM Users WHERE ke=? AND details NOT LIKE ?',[r.ke,'%"sub"%'],function(err,rr) { + s.knexQuery({ + action: "select", + columns: "details", + table: "Users", + where: [ + ['ke','=',r.ke], + ['details','NOT LIKE','%"sub"%'], + ], + },function(err,rr) { if(rr && rr[0]){ rr=rr[0]; - rr.details=JSON.parse(rr.details); - r.details.mon_groups=rr.details.mon_groups; - req.resp.details=JSON.stringify(r.details); - req.factorAuth() + rr.details = JSON.parse(rr.details); + r.details.mon_groups = rr.details.mon_groups; + response.details = JSON.stringify(r.details); + factorAuth() }else{ failedAuthentication(req.body.function) } }) }else{ - req.factorAuth() + factorAuth() } }else{ failedAuthentication(req.body.function) @@ -397,7 +487,15 @@ module.exports = function(s,config,lang,app,io){ }) } if(LdapAuth&&req.body.function==='ldap'&&req.body.key!==''){ - s.sqlQuery('SELECT * FROM Users WHERE ke=? AND details NOT LIKE ?',[req.body.key,'%"sub"%'],function(err,r) { + s.knexQuery({ + action: "select", + columns: "*", + table: "Users", + where: [ + ['ke','=',req.body.key], + ['details','NOT LIKE','%"sub"%'], + ], + },(err,r) => { if(r&&r[0]){ r=r[0] r.details=JSON.parse(r.details) @@ -446,7 +544,7 @@ module.exports = function(s,config,lang,app,io){ if(!user.uid){ user.uid=s.gid() } - req.resp={ + response = { ke:req.body.key, uid:user.uid, auth:s.createHash(s.gid()), @@ -459,30 +557,48 @@ module.exports = function(s,config,lang,app,io){ filter: {} }) } - user.post=[] - Object.keys(req.resp).forEach(function(v){ - user.post.push(req.resp[v]) - }) s.userLog({ke:req.body.key,mid:'$USER'},{type:r.lang['LDAP Success'],msg:{user:user}}) - s.sqlQuery('SELECT * FROM Users WHERE ke=? AND mail=?',[req.body.key,user.cn],function(err,rr){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Users", + where: [ + ['ke','=',req.body.key], + ['mail','=',user.cn], + ], + },function(err,rr) { if(rr&&rr[0]){ //already registered - rr=rr[0] - req.resp=rr; - rr.details=JSON.parse(rr.details) - req.resp.lang=s.getLanguageFile(rr.details.lang) + rr = rr[0] + response = rr; + rr.details = JSON.parse(rr.details) + response.lang = s.getLanguageFile(rr.details.lang) s.userLog({ke:req.body.key,mid:'$USER'},{type:r.lang['LDAP User Authenticated'],msg:{user:user,shinobiUID:rr.uid}}) - s.sqlQuery("UPDATE Users SET auth=? WHERE ke=? AND uid=?",[req.resp.auth,req.resp.ke,rr.uid]) + s.knexQuery({ + action: "update", + table: "Users", + update: { + auth: response.auth + }, + where: [ + ['ke','=',response.ke], + ['uid','=',rr.uid], + ] + }) }else{ //new ldap login s.userLog({ke:req.body.key,mid:'$USER'},{type:r.lang['LDAP User is New'],msg:{info:r.lang['Creating New Account'],user:user}}) - req.resp.lang=r.lang - s.sqlQuery('INSERT INTO Users (ke,uid,auth,mail,pass,details) VALUES (?,?,?,?,?,?)',user.post) + response.lang = r.lang + s.knexQuery({ + action: "insert", + table: "Users", + insert: response, + }) } - req.resp.details = JSON.stringify(req.resp.details) - req.resp.auth_token = req.resp.auth - req.resp.ok=true - checkRoute(req.resp) + response.details = JSON.stringify(response.details) + response.auth_token = response.auth + response.ok = true + checkRoute(response) }) return } @@ -513,7 +629,16 @@ module.exports = function(s,config,lang,app,io){ users: true, md5: true },function(data){ - s.sqlQuery('SELECT * FROM Logs WHERE ke=? ORDER BY `time` DESC LIMIT 30',['$'],function(err,r) { + s.knexQuery({ + action: "select", + columns: "*", + table: "Logs", + where: [ + ['ke','=','$'], + ], + orderBy: ['time','desc'], + limit: 30 + },(err,r) => { if(!r){ r=[] } @@ -545,14 +670,35 @@ module.exports = function(s,config,lang,app,io){ } if(!req.details.acceptedMachines[req.body.machineID]){ req.details.acceptedMachines[req.body.machineID]={} - s.sqlQuery("UPDATE Users SET details=? WHERE ke=? AND uid=?",[s.prettyPrint(req.details),req.body.ke,req.body.id]) + s.knexQuery({ + action: "update", + table: "Users", + update: { + details: s.prettyPrint(req.details) + }, + where: [ + ['ke','=',req.body.ke], + ['uid','=',req.body.id], + ] + }) } } req.body.function = s.factorAuth[req.body.ke][req.body.id].function - req.resp = s.factorAuth[req.body.ke][req.body.id].info - checkRoute(s.factorAuth[req.body.ke][req.body.id].user) + response = s.factorAuth[req.body.ke][req.body.id].info + response.lang = req.lang || s.getLanguageFile(JSON.parse(s.factorAuth[req.body.ke][req.body.id].info.details).lang) + checkRoute(s.factorAuth[req.body.ke][req.body.id].info) + clearTimeout(s.factorAuth[req.body.ke][req.body.id].expireAuth) + s.deleteFactorAuth({ + ke: req.body.ke, + uid: req.body.id, + }) }else{ - renderPage(config.renderPaths.factorAuth,{$user:s.factorAuth[req.body.ke][req.body.id].info,lang:req.lang}); + var info = s.factorAuth[req.body.ke][req.body.id].info + renderPage(config.renderPaths.factorAuth,{$user:{ + ke: info.ke, + id: info.uid, + mail: info.mail, + },lang:req.lang}); res.end(); } }else{ @@ -575,178 +721,166 @@ module.exports = function(s,config,lang,app,io){ res.end(s.prettyPrint({ok:true})) }) }) - /** - * Page : Montage - stand alone squished view with gridstackjs - */ - app.get([ - config.webPaths.apiPrefix+':auth/grid/:ke', - config.webPaths.apiPrefix+':auth/grid/:ke/:group', - config.webPaths.apiPrefix+':auth/cycle/:ke', - config.webPaths.apiPrefix+':auth/cycle/:ke/:group' - ], function(req,res) { - s.auth(req.params,function(user){ - if(user.permissions.get_monitors==="0"){ - res.end(user.lang['Not Permitted']) - return - } - - req.params.protocol=req.protocol; - req.sql='SELECT * FROM Monitors WHERE mode!=? AND mode!=? AND ke=?';req.ar=['stop','idle',req.params.ke]; - if(!req.params.id){ - if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ - try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} - req.or=[]; - user.details.monitors.forEach(function(v,n){ - req.or.push('mid=?');req.ar.push(v) - }) - req.sql+=' AND ('+req.or.join(' OR ')+')' - } - }else{ - if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){ - req.sql+=' and mid=?';req.ar.push(req.params.id) - }else{ - res.end(user.lang['There are no monitors that you can view with this account.']); - return; - } - } - s.sqlQuery(req.sql,req.ar,function(err,r){ - if(req.params.group){ - var filteredByGroupCheck = {}; - var filteredByGroup = []; - r.forEach(function(v,n){ - var details = JSON.parse(r[n].details); - try{ - req.params.group.split('|').forEach(function(group){ - var groups = JSON.parse(details.groups); - if(groups.indexOf(group) > -1 && !filteredByGroupCheck[v.mid]){ - filteredByGroupCheck[v.mid] = true; - filteredByGroup.push(v) - } - }) - }catch(err){ - - } - }) - r = filteredByGroup; - } - r.forEach(function(v,n){ - if(s.group[v.ke]&&s.group[v.ke].activeMonitors[v.mid]&&s.group[v.ke].activeMonitors[v.mid].watch){ - r[n].currentlyWatching=Object.keys(s.group[v.ke].activeMonitors[v.mid].watch).length - } - r[n].subStream={} - var details = JSON.parse(r[n].details) - if(details.snap==='1'){ - r[n].subStream.jpeg = '/'+req.params.auth+'/jpeg/'+v.ke+'/'+v.mid+'/s.jpg' - } - if(details.stream_channels&&details.stream_channels!==''){ - try{ - details.stream_channels=JSON.parse(details.stream_channels) - r[n].channels=[] - details.stream_channels.forEach(function(b,m){ - var streamURL - switch(b.stream_type){ - case'mjpeg': - streamURL='/'+req.params.auth+'/mjpeg/'+v.ke+'/'+v.mid+'/'+m - break; - case'hls': - streamURL='/'+req.params.auth+'/hls/'+v.ke+'/'+v.mid+'/'+m+'/s.m3u8' - break; - case'h264': - streamURL='/'+req.params.auth+'/h264/'+v.ke+'/'+v.mid+'/'+m - break; - case'flv': - streamURL='/'+req.params.auth+'/flv/'+v.ke+'/'+v.mid+'/'+m+'/s.flv' - break; - case'mp4': - streamURL='/'+req.params.auth+'/mp4/'+v.ke+'/'+v.mid+'/'+m+'/s.mp4' - break; - } - r[n].channels.push(streamURL) - }) - }catch(err){ - s.userLog(req.params,{type:'Broken Monitor Object',msg:'Stream Channels Field is damaged. Skipping.'}) - } - } - }) - var page = config.renderPaths.grid - if(req.path.indexOf('/cycle/') > -1){ - page = config.renderPaths.cycle - } - s.renderPage(req,res,page,{ - data:Object.assign(req.params,req.query), - baseUrl:req.protocol+'://'+req.hostname, - config: s.getConfigWithBranding(req.hostname), - lang:user.lang, - $user:user, - monitors:r, - query:req.query - }); - }) - },res,req) - }); + // /** + // * Page : Montage - stand alone squished view with gridstackjs + // */ + // app.get([ + // config.webPaths.apiPrefix+':auth/grid/:ke', + // config.webPaths.apiPrefix+':auth/grid/:ke/:group', + // config.webPaths.apiPrefix+':auth/cycle/:ke', + // config.webPaths.apiPrefix+':auth/cycle/:ke/:group' + // ], function(req,res) { + // s.auth(req.params,function(user){ + // if(user.permissions.get_monitors==="0"){ + // res.end(user.lang['Not Permitted']) + // return + // } + // + // req.params.protocol=req.protocol; + // req.sql='SELECT * FROM Monitors WHERE mode!=? AND mode!=? AND ke=?';req.ar=['stop','idle',req.params.ke]; + // if(!req.params.id){ + // if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ + // try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} + // req.or=[]; + // user.details.monitors.forEach(function(v,n){ + // req.or.push('mid=?');req.ar.push(v) + // }) + // req.sql+=' AND ('+req.or.join(' OR ')+')' + // } + // }else{ + // if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){ + // req.sql+=' and mid=?';req.ar.push(req.params.id) + // }else{ + // res.end(user.lang['There are no monitors that you can view with this account.']); + // return; + // } + // } + // s.sqlQuery(req.sql,req.ar,function(err,r){ + // if(req.params.group){ + // var filteredByGroupCheck = {}; + // var filteredByGroup = []; + // r.forEach(function(v,n){ + // var details = JSON.parse(r[n].details); + // try{ + // req.params.group.split('|').forEach(function(group){ + // var groups = JSON.parse(details.groups); + // if(groups.indexOf(group) > -1 && !filteredByGroupCheck[v.mid]){ + // filteredByGroupCheck[v.mid] = true; + // filteredByGroup.push(v) + // } + // }) + // }catch(err){ + // + // } + // }) + // r = filteredByGroup; + // } + // r.forEach(function(v,n){ + // if(s.group[v.ke]&&s.group[v.ke].activeMonitors[v.mid]&&s.group[v.ke].activeMonitors[v.mid].watch){ + // r[n].currentlyWatching=Object.keys(s.group[v.ke].activeMonitors[v.mid].watch).length + // } + // r[n].subStream={} + // var details = JSON.parse(r[n].details) + // if(details.snap==='1'){ + // r[n].subStream.jpeg = '/'+req.params.auth+'/jpeg/'+v.ke+'/'+v.mid+'/s.jpg' + // } + // if(details.stream_channels&&details.stream_channels!==''){ + // try{ + // details.stream_channels=JSON.parse(details.stream_channels) + // r[n].channels=[] + // details.stream_channels.forEach(function(b,m){ + // var streamURL + // switch(b.stream_type){ + // case'mjpeg': + // streamURL='/'+req.params.auth+'/mjpeg/'+v.ke+'/'+v.mid+'/'+m + // break; + // case'hls': + // streamURL='/'+req.params.auth+'/hls/'+v.ke+'/'+v.mid+'/'+m+'/s.m3u8' + // break; + // case'h264': + // streamURL='/'+req.params.auth+'/h264/'+v.ke+'/'+v.mid+'/'+m + // break; + // case'flv': + // streamURL='/'+req.params.auth+'/flv/'+v.ke+'/'+v.mid+'/'+m+'/s.flv' + // break; + // case'mp4': + // streamURL='/'+req.params.auth+'/mp4/'+v.ke+'/'+v.mid+'/'+m+'/s.mp4' + // break; + // } + // r[n].channels.push(streamURL) + // }) + // }catch(err){ + // s.userLog(req.params,{type:'Broken Monitor Object',msg:'Stream Channels Field is damaged. Skipping.'}) + // } + // } + // }) + // var page = config.renderPaths.grid + // if(req.path.indexOf('/cycle/') > -1){ + // page = config.renderPaths.cycle + // } + // s.renderPage(req,res,page,{ + // data:Object.assign(req.params,req.query), + // baseUrl:req.protocol+'://'+req.hostname, + // config: s.getConfigWithBranding(req.hostname), + // lang:user.lang, + // $user:user, + // monitors:r, + // query:req.query + // }); + // }) + // },res,req) + // }); /** * API : Get TV Channels (Monitor Streams) json */ app.get([config.webPaths.apiPrefix+':auth/tvChannels/:ke',config.webPaths.apiPrefix+':auth/tvChannels/:ke/:id','/get.php'], function (req,res){ - req.ret={ok:false}; + var response = {ok:false}; if(req.query.username&&req.query.password){ req.params.username = req.query.username req.params.password = req.query.password } var output = ['h264','hls','mp4'] - if(req.query.output&&req.query.output!==''){ + if( + req.query.output && + req.query.output !== '' + ){ output = req.query.output.split(',') output.forEach(function(type,n){ - if(type==='ts'){ - output[n]='h264' - if(output.indexOf('hls')===-1){ + if(type === 'ts'){ + output[n] = 'h264' + if(output.indexOf('hls') === -1){ output.push('hls') } } }) } - var isM3u8 = false; - if(req.query.type==='m3u8'||req.query.type==='m3u_plus'){ - //is m3u8 - isM3u8 = true; - }else{ - res.setHeader('Content-Type', 'application/json'); - } - req.fn=function(user){ - if(user.permissions.get_monitors==="0"){ - res.end(s.prettyPrint([])) + const isM3u8 = req.query.type === 'm3u8' || req.query.type === 'm3u_plus' + s.auth(req.params,function(user){ + const groupKey = req.params.ke + const monitorId = req.params.id + const monitorRestrictions = s.getMonitorRestrictions(user.details,monitorId) + if(user.details.sub && user.details.allmonitors === '0' && (user.permissions.get_monitors === "0" || monitorRestrictions.length === 0)){ + s.closeJsonResponse(res,[]); return } - if(!req.params.ke){ - req.params.ke = user.ke; - } - if(req.query.id&&!req.params.id){ - req.params.id = req.query.id; - } - req.sql='SELECT * FROM Monitors WHERE mode!=? AND ke=?';req.ar=['stop',req.params.ke]; - if(!req.params.id){ - if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ - try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} - req.or=[]; - user.details.monitors.forEach(function(v,n){ - req.or.push('mid=?');req.ar.push(v) - }) - req.sql+=' AND ('+req.or.join(' OR ')+')' - } - }else{ - if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){ - req.sql+=' and mid=?';req.ar.push(req.params.id) - }else{ - res.end('[]'); - return; - } - } - s.sqlQuery(req.sql,req.ar,function(err,r){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Monitors", + where: [ + ['ke','=',groupKey], + ['mode','!=','stop'], + monitorRestrictions + ] + },(err,r) => { var tvChannelMonitors = []; r.forEach(function(v,n){ var buildStreamURL = function(channelRow,type,channelNumber){ var streamURL - if(channelNumber){channelNumber = '/'+channelNumber}else{channelNumber=''} + if(req.query.streamtype && req.query.streamtype != type){ + return + } + if(channelNumber){channelNumber = '/' + channelNumber}else{channelNumber = ''} switch(type){ case'mjpeg': streamURL='/'+req.params.auth+'/mjpeg/'+v.ke+'/'+v.mid+channelNumber @@ -803,7 +937,7 @@ module.exports = function(s,config,lang,app,io){ tvChannelMonitors.forEach(function(channelRow,n){ output.forEach(function(type){ if(channelRow.streamsSortedByType[type]){ - if(req.query.type==='m3u_plus'){ + if(req.query.type === 'm3u_plus'){ m3u8 +='#EXTINF-1 tvg-id="'+channelRow.mid+'" tvg-name="'+channelRow.channel+'" tvg-logo="'+req.protocol+'://'+req.headers.host+channelRow.snapshot+'" group-title="'+channelRow.groupTitle+'",'+channelRow.channel+'\n' }else{ m3u8 +='#EXTINF:-1,'+channelRow.channel+' ('+type.toUpperCase()+') \n' @@ -814,43 +948,35 @@ module.exports = function(s,config,lang,app,io){ }) res.end(m3u8) }else{ - if(tvChannelMonitors.length===1){tvChannelMonitors=tvChannelMonitors[0];} - res.end(s.prettyPrint(tvChannelMonitors)); + if(tvChannelMonitors.length === 1)tvChannelMonitors=tvChannelMonitors[0]; + s.closeJsonResponse(res,tvChannelMonitors) } }) - } - s.auth(req.params,req.fn,res,req); + },res,req); }); /** * API : Get Monitors */ app.get([config.webPaths.apiPrefix+':auth/monitor/:ke',config.webPaths.apiPrefix+':auth/monitor/:ke/:id'], function (req,res){ - req.ret={ok:false}; + var response = {ok:false}; res.setHeader('Content-Type', 'application/json'); - req.fn=function(user){ - if(user.permissions.get_monitors==="0"){ - res.end(s.prettyPrint([])) - return - } - req.sql='SELECT * FROM Monitors WHERE ke=?';req.ar=[req.params.ke]; - if(!req.params.id){ - if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ - try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} - req.or=[]; - user.details.monitors.forEach(function(v,n){ - req.or.push('mid=?');req.ar.push(v) - }) - req.sql+=' AND ('+req.or.join(' OR ')+')' - } - }else{ - if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){ - req.sql+=' and mid=?';req.ar.push(req.params.id) - }else{ - res.end('[]'); - return; - } + s.auth(req.params,(user) => { + const groupKey = req.params.ke + const monitorId = req.params.id + const monitorRestrictions = s.getMonitorRestrictions(user.details,monitorId) + if(user.details.sub && user.details.allmonitors === '0' && (user.permissions.get_monitors === "0" || monitorRestrictions.length === 0)){ + s.closeJsonResponse(res,[]); + return } - s.sqlQuery(req.sql,req.ar,function(err,r){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Monitors", + where: [ + ['ke','=',groupKey], + monitorRestrictions + ] + },(err,r) => { r.forEach(function(v,n){ if(s.group[v.ke] && s.group[v.ke].activeMonitors[v.mid]){ r[n].currentlyWatching = Object.keys(s.group[v.ke].activeMonitors[v.mid].watch).length @@ -901,11 +1027,9 @@ module.exports = function(s,config,lang,app,io){ }) } }) - if(r.length===1){r=r[0];} - res.end(s.prettyPrint(r)); + s.closeJsonResponse(res,r); }) - } - s.auth(req.params,req.fn,res,req); + },res,req); }); /** * API : Merge Recorded Videos into one file @@ -918,21 +1042,31 @@ module.exports = function(s,config,lang,app,io){ if(req.query.videos && req.query.videos !== ''){ s.auth(req.params,function(user){ var videosSelected = JSON.parse(req.query.videos) - var where = [] - var values = [] + const whereQuery = [] + var didOne = false videosSelected.forEach(function(video){ - where.push("(ke=? AND mid=? AND `time`=?)") - if(!video.ke)video.ke = req.params.ke - values.push(video.ke) - values.push(video.mid) var time = s.nameToTime(video.filename) if(req.query.isUTC === 'true'){ time = s.utcToLocal(time) } - time = new Date(time) - values.push(time) + if(didOne){ + whereQuery.push(['or','ke','=',req.params.ke]) + }else{ + didOne = true + whereQuery.push(['ke','=',req.params.ke]) + } + whereQuery.push( + ['mid','=',video.mid], + ['time','=',time], + ) + }) - s.sqlQuery('SELECT * FROM Videos WHERE '+where.join(' OR '),values,function(err,r){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Videos", + where: whereQuery + },(err,r) => { var resp = {ok: false} if(r && r[0]){ s.mergeRecordedVideos(r,req.params.ke,function(fullPath,filename){ @@ -966,14 +1100,10 @@ module.exports = function(s,config,lang,app,io){ ], function (req,res){ res.setHeader('Content-Type', 'application/json'); s.auth(req.params,function(user){ - var hasRestrictions = user.details.sub && user.details.allmonitors !== '1' - if( - user.permissions.watch_videos==="0" || - hasRestrictions && (!user.details.video_view || user.details.video_view.indexOf(req.params.id)===-1) - ){ - res.end(s.prettyPrint([])) - return - } + const userDetails = user.details + const monitorId = req.params.id + const groupKey = req.params.ke + const hasRestrictions = userDetails.sub && userDetails.allmonitors !== '1'; var origURL = req.originalUrl.split('/') var videoParam = origURL[origURL.indexOf(req.params.auth) + 1] var videoSet = 'Videos' @@ -982,100 +1112,35 @@ module.exports = function(s,config,lang,app,io){ videoSet = 'Cloud Videos' break; } - req.sql='SELECT * FROM `'+videoSet+'` WHERE ke=?';req.ar=[req.params.ke]; - req.count_sql='SELECT COUNT(*) FROM `'+videoSet+'` WHERE ke=?';req.count_ar=[req.params.ke]; - if(req.query.archived=='1'){ - req.sql+=' AND details LIKE \'%"archived":"1"\'' - req.count_sql+=' AND details LIKE \'%"archived":"1"\'' - } - if(!req.params.id){ - if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ - try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} - req.or=[]; - user.details.monitors.forEach(function(v,n){ - req.or.push('mid=?');req.ar.push(v) - }) - req.sql+=' AND ('+req.or.join(' OR ')+')' - req.count_sql+=' AND ('+req.or.join(' OR ')+')' - } - }else{ - if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){ - req.sql+=' and mid=?';req.ar.push(req.params.id) - req.count_sql+=' and mid=?';req.count_ar.push(req.params.id) - }else{ - res.end('[]'); - return; - } - } - if(req.query.start||req.query.end){ - if(req.query.start && req.query.start !== ''){ - req.query.start = s.stringToSqlTime(req.query.start) - } - if(req.query.end && req.query.end !== ''){ - req.query.end = s.stringToSqlTime(req.query.end) - } - if(!req.query.startOperator||req.query.startOperator==''){ - req.query.startOperator='>=' - } - if(!req.query.endOperator||req.query.endOperator==''){ - req.query.endOperator='<=' - } - var endIsStartTo - var theEndParameter = '`end`' - if(req.query.endIsStartTo){ - endIsStartTo = true - theEndParameter = '`time`' - } - switch(true){ - case(req.query.start&&req.query.start!==''&&req.query.end&&req.query.end!==''): - req.sql+=' AND `time` '+req.query.startOperator+' ? AND '+theEndParameter+' '+req.query.endOperator+' ?'; - req.count_sql+=' AND `time` '+req.query.startOperator+' ? AND '+theEndParameter+' '+req.query.endOperator+' ?'; - req.ar.push(req.query.start) - req.ar.push(req.query.end) - req.count_ar.push(req.query.start) - req.count_ar.push(req.query.end) - break; - case(req.query.start&&req.query.start!==''): - req.sql+=' AND `time` '+req.query.startOperator+' ?'; - req.count_sql+=' AND `time` '+req.query.startOperator+' ?'; - req.ar.push(req.query.start) - req.count_ar.push(req.query.start) - break; - case(req.query.end&&req.query.end!==''): - req.sql+=' AND '+theEndParameter+' '+req.query.endOperator+' ?'; - req.count_sql+=' AND '+theEndParameter+' '+req.query.endOperator+' ?'; - req.ar.push(req.query.end) - req.count_ar.push(req.query.end) - break; - } - } - req.sql+=' ORDER BY `time` DESC'; - if(!req.query.limit||req.query.limit==''){ - req.query.limit='100' - } - if(req.query.limit!=='0'){ - req.sql+=' LIMIT '+req.query.limit - } - s.sqlQuery(req.sql,req.ar,function(err,r){ - if(!r){ - res.end(s.prettyPrint({total:0,limit:req.query.limit,skip:0,videos:[]})); - return - } - s.sqlQuery(req.count_sql,req.count_ar,function(err,count){ - s.buildVideoLinks(r,{ + s.sqlQueryBetweenTimesWithPermissions({ + table: videoSet, + user: user, + noCount: true, + groupKey: req.params.ke, + monitorId: req.params.id, + startTime: req.query.start, + endTime: req.query.end, + startTimeOperator: req.query.startOperator, + endTimeOperator: req.query.endOperator, + limit: req.query.limit, + archived: req.query.archived, + endIsStartTo: !!req.query.endIsStartTo, + parseRowDetails: false, + rowName: 'videos', + preliminaryValidationFailed: ( + user.permissions.watch_videos === "0" || + hasRestrictions && + (!userDetails.video_view || userDetails.video_view.indexOf(monitorId)===-1) + ) + },(response) => { + if(response && response.videos){ + s.buildVideoLinks(response.videos,{ auth : req.params.auth, videoParam : videoParam, - hideRemote : config.hideCloudSaveUrls + hideRemote : config.hideCloudSaveUrls, }) - if(req.query.limit.indexOf(',')>-1){ - req.skip=parseInt(req.query.limit.split(',')[0]) - req.query.limit=parseInt(req.query.limit.split(',')[1]) - }else{ - req.skip=0 - req.query.limit=parseInt(req.query.limit) - } - res.end(s.prettyPrint({isUTC:config.useUTC,total:count[0]['COUNT(*)'],limit:req.query.limit,skip:req.skip,videos:r,endIsStartTo:endIsStartTo})); - }) + } + res.end(s.prettyPrint(response)) }) },res,req); }); @@ -1086,201 +1151,141 @@ module.exports = function(s,config,lang,app,io){ config.webPaths.apiPrefix+':auth/events/:ke', config.webPaths.apiPrefix+':auth/events/:ke/:id' ], function (req,res){ - req.ret={ok:false}; res.setHeader('Content-Type', 'application/json'); s.auth(req.params,function(user){ - if(user.permissions.watch_videos==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.video_view.indexOf(req.params.id)===-1){ - res.end(s.prettyPrint([])) - return - } - req.sql='SELECT * FROM Events WHERE ke=?';req.ar=[req.params.ke]; - if(!req.params.id){ - if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ - try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} - req.or=[]; - user.details.monitors.forEach(function(v,n){ - req.or.push('mid=?');req.ar.push(v) - }) - req.sql+=' AND ('+req.or.join(' OR ')+')' - } - }else{ - if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){ - req.sql+=' and mid=?';req.ar.push(req.params.id) - }else{ - res.end('[]'); - return; - } - } - if(req.query.start||req.query.end){ - if(req.query.start && req.query.start !== ''){ - req.query.start = s.stringToSqlTime(req.query.start) - } - if(req.query.end && req.query.end !== ''){ - req.query.end = s.stringToSqlTime(req.query.end) - } - if(!req.query.startOperator||req.query.startOperator==''){ - req.query.startOperator='>=' - } - if(!req.query.endOperator||req.query.endOperator==''){ - req.query.endOperator='<=' - } - switch(true){ - case(req.query.start&&req.query.start!==''&&req.query.end&&req.query.end!==''): - req.sql+=' AND `time` '+req.query.startOperator+' ? AND `time` '+req.query.endOperator+' ?'; - req.ar.push(req.query.start) - req.ar.push(req.query.end) - break; - case(req.query.start&&req.query.start!==''): - req.sql+=' AND `time` '+req.query.startOperator+' ?'; - req.ar.push(req.query.start) - break; - case(req.query.end&&req.query.end!==''): - req.sql+=' AND `time` '+req.query.endOperator+' ?'; - req.ar.push(req.query.end) - break; - } - } - req.sql+=' ORDER BY `time` DESC'; - if(!req.query.limit||req.query.limit==''){ - req.query.limit='100' - } - if(req.query.limit!=='0'){ - req.sql+=' LIMIT '+req.query.limit - } - s.sqlQuery(req.sql,req.ar,function(err,r){ - if(err){ - err.sql=req.sql; - res.end(s.prettyPrint(err)); - return - } - if(!r){r=[]} - r.forEach(function(v,n){ - r[n].details=JSON.parse(v.details); - }) - res.end(s.prettyPrint(r)); + const userDetails = user.details + const monitorId = req.params.id + const groupKey = req.params.ke + const hasRestrictions = userDetails.sub && userDetails.allmonitors !== '1'; + s.sqlQueryBetweenTimesWithPermissions({ + table: 'Events', + user: user, + groupKey: req.params.ke, + monitorId: req.params.id, + startTime: req.query.start, + endTime: req.query.end, + startTimeOperator: req.query.startOperator, + endTimeOperator: req.query.endOperator, + limit: req.query.limit, + endIsStartTo: true, + parseRowDetails: true, + noFormat: true, + noCount: true, + rowName: 'events', + preliminaryValidationFailed: ( + user.permissions.watch_videos === "0" || + hasRestrictions && + (!userDetails.video_view || userDetails.video_view.indexOf(monitorId)===-1) + ) + },(response) => { + res.end(s.prettyPrint(response)) }) - },res,req); - }); + }) + }) /** * API : Get Logs */ - app.get([config.webPaths.apiPrefix+':auth/logs/:ke',config.webPaths.apiPrefix+':auth/logs/:ke/:id'], function (req,res){ - req.ret={ok:false}; + app.get([ + config.webPaths.apiPrefix+':auth/logs/:ke', + config.webPaths.apiPrefix+':auth/logs/:ke/:id' + ], function (req,res){ res.setHeader('Content-Type', 'application/json'); s.auth(req.params,function(user){ - if(user.permissions.get_logs==="0" || user.details.sub && user.details.view_logs !== '1'){ - res.end(s.prettyPrint([])) - return - } - req.sql='SELECT * FROM Logs WHERE ke=?';req.ar=[req.params.ke]; - if(!req.params.id){ - if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ - try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} - req.or=[]; - user.details.monitors.forEach(function(v,n){ - req.or.push('mid=?');req.ar.push(v) - }) - req.sql+=' AND ('+req.or.join(' OR ')+')' - } - }else{ - if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1||req.params.id.indexOf('$')>-1){ - req.sql+=' and mid=?';req.ar.push(req.params.id) - }else{ - res.end('[]'); - return; - } - } - if(req.query.start||req.query.end){ - if(!req.query.startOperator||req.query.startOperator==''){ - req.query.startOperator='>=' - } - if(!req.query.endOperator||req.query.endOperator==''){ - req.query.endOperator='<=' - } - if(req.query.start && req.query.start !== '' && req.query.end && req.query.end !== ''){ - req.query.start = s.stringToSqlTime(req.query.start) - req.query.end = s.stringToSqlTime(req.query.end) - req.sql+=' AND `time` '+req.query.startOperator+' ? AND `time` '+req.query.endOperator+' ?'; - req.ar.push(req.query.start) - req.ar.push(req.query.end) - }else if(req.query.start && req.query.start !== ''){ - req.query.start = s.stringToSqlTime(req.query.start) - req.sql+=' AND `time` '+req.query.startOperator+' ?'; - req.ar.push(req.query.start) - } - } - if(!req.query.limit||req.query.limit==''){req.query.limit=50} - req.sql+=' ORDER BY `time` DESC LIMIT '+req.query.limit+''; - s.sqlQuery(req.sql,req.ar,function(err,r){ - if(err){ - err.sql=req.sql; - res.end(s.prettyPrint(err)); - return - } - if(!r){r=[]} - r.forEach(function(v,n){ - r[n].info=JSON.parse(v.info) + const userDetails = user.details + const monitorId = req.params.id + const groupKey = req.params.ke + const hasRestrictions = userDetails.sub && userDetails.allmonitors !== '1'; + s.sqlQueryBetweenTimesWithPermissions({ + table: 'Logs', + user: user, + groupKey: req.params.ke, + monitorId: req.params.id, + startTime: req.query.start, + endTime: req.query.end, + startTimeOperator: req.query.startOperator, + endTimeOperator: req.query.endOperator, + limit: req.query.limit || 50, + endIsStartTo: true, + noFormat: true, + noCount: true, + rowName: 'logs', + preliminaryValidationFailed: ( + user.permissions.get_logs === "0" || userDetails.sub && userDetails.view_logs !== '1' + ) + },(response) => { + response.forEach(function(v,n){ + v.info = JSON.parse(v.info) }) - res.end(s.prettyPrint(r)); + res.end(s.prettyPrint(response)) }) - },res,req); + },res,req) }) /** * API : Get Monitors Online */ app.get(config.webPaths.apiPrefix+':auth/smonitor/:ke', function (req,res){ - req.ret={ok:false}; + var response = {ok:false}; res.setHeader('Content-Type', 'application/json'); - req.fn=function(user){ - if(user.permissions.get_monitors==="0"){ - res.end(s.prettyPrint([])) + s.auth(req.params,(user) => { + const groupKey = req.params.ke + const monitorId = req.params.id + const monitorRestrictions = s.getMonitorRestrictions(user.details,monitorId) + if(user.details.sub && user.details.allmonitors === '0' && (user.permissions.get_monitors === "0" || monitorRestrictions.length === 0)){ + s.closeJsonResponse(res,[]); return } - req.sql='SELECT * FROM Monitors WHERE ke=?';req.ar=[req.params.ke]; - if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ - try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} - req.or=[]; - user.details.monitors.forEach(function(v,n){ - req.or.push('mid=?');req.ar.push(v) + s.knexQuery({ + action: "select", + columns: "*", + table: "Monitors", + where: [ + ['ke','=',groupKey], + monitorRestrictions + ] + },(err,r) => { + const startedMonitors = [] + r.forEach(function(v){ + if( + s.group[groupKey] && + s.group[groupKey].activeMonitors[v.mid] && + s.group[groupKey].activeMonitors[v.mid].isStarted === true + ){ + startedMonitors.push(v) + } }) - req.sql+=' AND ('+req.or.join(' OR ')+')' - } - s.sqlQuery(req.sql,req.ar,function(err,r){ - if(r&&r[0]){ - req.ar=[]; - r.forEach(function(v){ - if(s.group[req.params.ke]&&s.group[req.params.ke].activeMonitors[v.mid]&&s.group[req.params.ke].activeMonitors[v.mid].isStarted === true){ - req.ar.push(v) - } - }) - }else{ - req.ar=[]; - } - res.end(s.prettyPrint(req.ar)); + s.closeJsonResponse(res,startedMonitors) }) - } - s.auth(req.params,req.fn,res,req); + },res,req); }); /** * API : Monitor Mode Controller */ app.get([config.webPaths.apiPrefix+':auth/monitor/:ke/:id/:f',config.webPaths.apiPrefix+':auth/monitor/:ke/:id/:f/:ff',config.webPaths.apiPrefix+':auth/monitor/:ke/:id/:f/:ff/:fff'], function (req,res){ - req.ret={ok:false}; + var response = {ok:false}; res.setHeader('Content-Type', 'application/json'); s.auth(req.params,function(user){ if(user.permissions.control_monitors==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitor_edit.indexOf(req.params.id)===-1){ res.end(user.lang['Not Permitted']) return } - if(req.params.f===''){req.ret.msg=user.lang.monitorGetText1;res.end(s.prettyPrint(req.ret));return} + if(req.params.f===''){response.msg = user.lang.monitorGetText1;res.end(s.prettyPrint(response));return} if(req.params.f!=='stop'&&req.params.f!=='start'&&req.params.f!=='record'){ - req.ret.msg='Mode not recognized.'; - res.end(s.prettyPrint(req.ret)); + response.msg = 'Mode not recognized.'; + res.end(s.prettyPrint(response)); return; } - s.sqlQuery('SELECT * FROM Monitors WHERE ke=? AND mid=?',[req.params.ke,req.params.id],function(err,r){ - if(r&&r[0]){ - r=r[0]; + s.knexQuery({ + action: "select", + columns: "*", + table: "Monitors", + where: [ + ['ke','=',req.params.ke], + ['mid','=',req.params.id], + ], + limit: 1 + },(err,r) => { + if(r && r[0]){ + r = r[0]; if(req.query.reset==='1'||(s.group[r.ke]&&s.group[r.ke].rawMonitorConfigurations[r.mid].mode!==req.params.f)||req.query.fps&&(!s.group[r.ke].activeMonitors[r.mid].currentState||!s.group[r.ke].activeMonitors[r.mid].currentState.trigger_on)){ if(req.query.reset!=='1'||!s.group[r.ke].activeMonitors[r.mid].trigger_timer){ if(!s.group[r.ke].activeMonitors[r.mid].currentState)s.group[r.ke].activeMonitors[r.mid].currentState={} @@ -1298,7 +1303,17 @@ module.exports = function(s,config,lang,app,io){ s.group[r.ke].activeMonitors[r.mid].currentState.detector_trigger_record_fps=r.fps } r.id=r.mid; - s.sqlQuery('UPDATE Monitors SET mode=? WHERE ke=? AND mid=?',[r.mode,r.ke,r.mid]); + s.knexQuery({ + action: "update", + table: "Monitors", + update: { + mode: r.mode + }, + where: [ + ['ke','=',r.ke], + ['mid','=',r.mid], + ] + }) s.group[r.ke].rawMonitorConfigurations[r.mid]=r; s.tx({f:'monitor_edit',mid:r.mid,ke:r.ke,mon:r},'GRP_'+r.ke); s.tx({f:'monitor_edit',mid:r.mid,ke:r.ke,mon:r},'STR_'+r.ke); @@ -1306,12 +1321,12 @@ module.exports = function(s,config,lang,app,io){ if(req.params.f!=='stop'){ s.camera(req.params.f,s.cleanMonitorObject(r)); } - req.ret.msg=user.lang['Monitor mode changed']+' : '+req.params.f; + response.msg = user.lang['Monitor mode changed']+' : '+req.params.f; }else{ - req.ret.msg=user.lang['Reset Timer']; + response.msg = user.lang['Reset Timer']; } - req.ret.cmd_at=s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss'); - req.ret.ok=true; + response.cmd_at=s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss'); + response.ok = true; if(req.params.ff&&req.params.f!=='stop'){ req.params.ff=parseFloat(req.params.ff); clearTimeout(s.group[r.ke].activeMonitors[r.mid].trigger_timer) @@ -1331,7 +1346,17 @@ module.exports = function(s,config,lang,app,io){ } s.group[r.ke].activeMonitors[r.mid].trigger_timer=setTimeout(function(){ delete(s.group[r.ke].activeMonitors[r.mid].trigger_timer) - s.sqlQuery('UPDATE Monitors SET mode=? WHERE ke=? AND mid=?',[s.group[r.ke].activeMonitors[r.mid].currentState.mode,r.ke,r.mid]); + s.knexQuery({ + action: "update", + table: "Monitors", + update: { + mode: s.group[r.ke].activeMonitors[r.mid].currentState.mode + }, + where: [ + ['ke','=',r.ke], + ['mid','=',r.mid], + ] + }) r.neglectTriggerTimer=1; r.mode=s.group[r.ke].activeMonitors[r.mid].currentState.mode; r.fps=s.group[r.ke].activeMonitors[r.mid].currentState.fps; @@ -1344,301 +1369,31 @@ module.exports = function(s,config,lang,app,io){ s.tx({f:'monitor_edit',mid:r.mid,ke:r.ke,mon:r},'GRP_'+r.ke); s.tx({f:'monitor_edit',mid:r.mid,ke:r.ke,mon:r},'STR_'+r.ke); },req.timeout); - // req.ret.end_at=s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss').add(req.timeout,'milliseconds'); + // response.end_at=s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss').add(req.timeout,'milliseconds'); } }else{ - req.ret.msg=user.lang['Monitor mode is already']+' : '+req.params.f; + response.msg = user.lang['Monitor mode is already']+' : '+req.params.f; } }else{ - req.ret.msg=user.lang['Monitor or Key does not exist.']; + response.msg = user.lang['Monitor or Key does not exist.']; } - res.end(s.prettyPrint(req.ret)); + res.end(s.prettyPrint(response)); }) },res,req); }) /** - * API : Get fileBin files - */ - app.get([config.webPaths.apiPrefix+':auth/fileBin/:ke',config.webPaths.apiPrefix+':auth/fileBin/:ke/:id'],function (req,res){ - res.setHeader('Content-Type', 'application/json'); - req.fn=function(user){ - req.sql='SELECT * FROM Files WHERE ke=?';req.ar=[req.params.ke]; - if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ - try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} - req.or=[]; - user.details.monitors.forEach(function(v,n){ - req.or.push('mid=?');req.ar.push(v) - }) - req.sql+=' AND ('+req.or.join(' OR ')+')' - }else{ - if(req.params.id&&(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1)){ - req.sql+=' and mid=?';req.ar.push(req.params.id) - } - } - s.sqlQuery(req.sql,req.ar,function(err,r){ - if(!r){ - r=[] - }else{ - r.forEach(function(v){ - v.details=JSON.parse(v.details) - v.href='/'+req.params.auth+'/fileBin/'+req.params.ke+'/'+req.params.id+'/'+v.details.year+'/'+v.details.month+'/'+v.details.day+'/'+v.name; - }) - } - res.end(s.prettyPrint(r)); - }) - } - s.auth(req.params,req.fn,res,req); - }); - /** - * API : Get fileBin file - */ - app.get(config.webPaths.apiPrefix+':auth/fileBin/:ke/:id/:year/:month/:day/:file', function (req,res){ - s.auth(req.params,function(user){ - var failed = function(){ - res.end(user.lang['File Not Found']) - } - if (!s.group[req.params.ke].fileBin[req.params.id+'/'+req.params.file]){ - s.sqlQuery('SELECT * FROM Files WHERE ke=? AND mid=? AND name=?',[req.params.ke,req.params.id,req.params.file],function(err,r){ - if(r&&r[0]){ - r = r[0] - r.details = JSON.parse(r.details) - req.dir = s.dir.fileBin+req.params.ke+'/'+req.params.id+'/'+r.details.year+'/'+r.details.month+'/'+r.details.day+'/'+req.params.file; - fs.stat(req.dir,function(err,stats){ - if(!err){ - res.on('finish',function(){res.end()}) - fs.createReadStream(req.dir).pipe(res) - }else{ - failed() - } - }) - }else{ - failed() - } - }) - }else{ - res.end(user.lang['Please Wait for Completion']) - } - },res,req); - }); - // /** - // * API : Zip Videos and Get Link from fileBin - // */ - // app.get(config.webPaths.apiPrefix+':auth/zipVideos/:ke', function (req,res){ - // var failed = function(resp){ - // res.setHeader('Content-Type', 'application/json'); - // res.end(s.prettyPrint(resp)) - // } - // if(req.query.videos && req.query.videos !== ''){ - // s.auth(req.params,function(user){ - // var videosSelected = JSON.parse(req.query.videos) - // var where = [] - // var values = [] - // videosSelected.forEach(function(video){ - // where.push("(ke=? AND mid=? AND `time`=?)") - // if(!video.ke)video.ke = req.params.ke - // values.push(video.ke) - // values.push(video.mid) - // var time = s.nameToTime(video.filename) - // if(req.query.isUTC === 'true'){ - // time = s.utcToLocal(time) - // } - // time = new Date(time) - // values.push(time) - // }) - // s.sqlQuery('SELECT * FROM Videos WHERE '+where.join(' OR '),values,function(err,r){ - // var resp = {ok: false} - // if(r && r[0]){ - // resp.ok = true - // var zipDownload = null - // var tempFiles = [] - // var fileId = s.gid() - // var fileBinDir = s.dir.fileBin+req.params.ke+'/' - // var tempScript = s.dir.streams+req.params.ke+'/'+fileId+'.sh' - // var zippedFilename = s.formattedTime()+'-'+fileId+'-Shinobi_Recordings.zip' - // var zippedFile = fileBinDir+zippedFilename - // var script = 'cd '+fileBinDir+' && zip -9 -r '+zippedFile - // res.on('close', () => { - // if(zipDownload && zipDownload.destroy){ - // zipDownload.destroy() - // } - // fs.unlink(zippedFile); - // }) - // fs.mkdir(fileBinDir,function(err){ - // s.handleFolderError(err) - // r.forEach(function(video){ - // var timeFormatted = s.formattedTime(video.time) - // video.filename = timeFormatted+'.'+video.ext - // var dir = s.getVideoDirectory(video)+video.filename - // var tempVideoFile = timeFormatted+' - '+video.mid+'.'+video.ext - // fs.writeFileSync(fileBinDir+tempVideoFile, fs.readFileSync(dir)) - // tempFiles.push(fileBinDir+tempVideoFile) - // script += ' "'+tempVideoFile+'"' - // }) - // fs.writeFileSync(tempScript,script,'utf8') - // var zipCreate = spawn('sh',(tempScript).split(' '),{detached: true}) - // zipCreate.stderr.on('data',function(data){ - // s.userLog({ke:req.params.ke,mid:'$USER'},{title:'Zip Create Error',msg:data.toString()}) - // }) - // zipCreate.on('exit',function(data){ - // fs.unlinkSync(tempScript) - // tempFiles.forEach(function(file){ - // fs.unlink(file,function(){}) - // }) - // res.setHeader('Content-Disposition', 'attachment; filename="'+zippedFilename+'"') - // var zipDownload = fs.createReadStream(zippedFile) - // zipDownload.pipe(res) - // zipDownload.on('error', function (error) { - // var errorString = error.toString() - // s.userLog({ - // ke: req.params.ke, - // mid: '$USER' - // },{ - // title: 'Zip Download Error', - // msg: errorString - // }) - // if(zipDownload && zipDownload.destroy){ - // zipDownload.destroy() - // } - // res.end(s.prettyPrint({ - // ok: false, - // msg: errorString - // })) - // }) - // zipDownload.on('close', function () { - // res.end() - // zipDownload.destroy() - // fs.unlinkSync(zippedFile) - // }) - // }) - // }) - // }else{ - // failed({ok:false,msg:'No Videos Found'}) - // } - // }) - // },res,req); - // }else{ - // failed({ok:false,msg:'"videos" query variable is missing from request.'}) - // } - // }) - // /** - // * API : Zip Cloud Videos and Get Link from fileBin - // */ - // app.get(config.webPaths.apiPrefix+':auth/zipCloudVideos/:ke', function (req,res){ - // var failed = function(resp){ - // res.setHeader('Content-Type', 'application/json'); - // res.end(s.prettyPrint(resp)) - // } - // if(req.query.videos && req.query.videos !== ''){ - // s.auth(req.params,function(user){ - // var videosSelected = JSON.parse(req.query.videos) - // var where = [] - // var values = [] - // videosSelected.forEach(function(video){ - // where.push("(ke=? AND mid=? AND `time`=?)") - // if(!video.ke)video.ke = req.params.ke - // values.push(video.ke) - // values.push(video.mid) - // var time = s.nameToTime(video.filename) - // if(req.query.isUTC === 'true'){ - // time = s.utcToLocal(time) - // } - // time = new Date(time) - // values.push(time) - // }) - // s.sqlQuery('SELECT * FROM `Cloud Videos` WHERE '+where.join(' OR '),values,function(err,r){ - // var resp = {ok: false} - // if(r && r[0]){ - // resp.ok = true - // var zipDownload = null - // var tempFiles = [] - // var fileId = s.gid() - // var fileBinDir = s.dir.fileBin+req.params.ke+'/' - // var tempScript = s.dir.streams+req.params.ke+'/'+fileId+'.sh' - // var zippedFilename = s.formattedTime()+'-'+fileId+'-Shinobi_Cloud_Backed_Recordings.zip' - // var zippedFile = fileBinDir+zippedFilename - // var script = 'cd '+fileBinDir+' && zip -9 -r '+zippedFile - // res.on('close', () => { - // if(zipDownload && zipDownload.destroy){ - // zipDownload.destroy() - // } - // fs.unlink(zippedFile); - // }) - // fs.mkdir(fileBinDir,function(err){ - // var cloudDownloadCount = 0 - // var getFile = function(video,completed){ - // if(!video)completed(); - // s.checkDetails(video) - // var filename = video.href.split('/') - // filename = filename[filename.length - 1] - // var timeFormatted = s.formattedTime(video.time) - // var tempVideoFile = video.details.type + '-' + video.mid + '-' + filename - // var tempFileWriteStream = fs.createWriteStream(fileBinDir+tempVideoFile) - // tempFileWriteStream.on('finish', function() { - // ++cloudDownloadCount - // getFile(r[cloudDownloadCount],completed) - // }) - // var cloudVideoDownload = request(video.href) - // cloudVideoDownload.on('response', function (res) { - // res.pipe(tempFileWriteStream) - // }) - // tempFiles.push(fileBinDir+tempVideoFile) - // script += ' "'+tempVideoFile+'"' - // } - // getFile(r[cloudDownloadCount],function(){ - // fs.writeFileSync(tempScript,script,'utf8') - // var zipCreate = spawn('sh',(tempScript).split(' '),{detached: true}) - // zipCreate.stderr.on('data',function(data){ - // s.userLog({ke:req.params.ke,mid:'$USER'},{title:'Zip Create Error',msg:data.toString()}) - // }) - // zipCreate.on('exit',function(data){ - // fs.unlinkSync(tempScript) - // tempFiles.forEach(function(file){ - // fs.unlink(file,function(){}) - // }) - // res.setHeader('Content-Disposition', 'attachment; filename="' + zippedFilename + '"') - // var zipDownload = fs.createReadStream(zippedFile) - // zipDownload.pipe(res) - // zipDownload.on('error', function (error) { - // var errorString = error.toString() - // s.userLog({ - // ke: req.params.ke, - // mid: '$USER' - // },{ - // title: 'Zip Download Error', - // msg: errorString - // }) - // if(zipDownload && zipDownload.destroy){ - // zipDownload.destroy() - // } - // res.end(s.prettyPrint({ - // ok: false, - // msg: errorString - // })) - // }) - // zipDownload.on('close', function () { - // res.end() - // zipDownload.destroy() - // fs.unlinkSync(zippedFile) - // }) - // }) - // }) - // }) - // }else{ - // failed({ok:false,msg:'No Videos Found'}) - // } - // }) - // },res,req); - // }else{ - // failed({ok:false,msg:'"videos" query variable is missing from request.'}) - // } - // }) - /** * API : Get Cloud Video File (proxy) */ app.get(config.webPaths.apiPrefix+':auth/cloudVideos/:ke/:id/:file', function (req,res){ s.auth(req.params,function(user){ - if(user.permissions.watch_videos==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitors.indexOf(req.params.id)===-1){ - res.end(user.lang['Not Permitted']) + const groupKey = req.params.ke + const monitorId = req.params.id + const monitorRestrictions = s.getMonitorRestrictions(user.details,monitorId) + if(user.details.sub && user.details.allmonitors === '0' && (user.permissions.watch_videos === "0" || monitorRestrictions.length === 0)){ + s.closeJsonResponse(res,{ + ok: false, + msg: lang['Not Permitted'] + }) return } var time = s.nameToTime(req.params.file) @@ -1646,10 +1401,29 @@ module.exports = function(s,config,lang,app,io){ time = s.utcToLocal(time) } time = new Date(time) - s.sqlQuery('SELECT * FROM `Cloud Videos` WHERE ke=? AND mid=? AND `time`=? LIMIT 1',[req.params.ke,req.params.id,time],function(err,r){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Cloud Videos", + where: [ + ['ke','=',groupKey], + ['mid','=',req.params.id], + ['time','=',time] + ], + limit: 1 + },(err,r) => { if(r&&r[0]){ r = r[0] - req.pipe(request(r.href)).pipe(res) + if(JSON.parse(r.details).type === 'googd' && s.cloudDiskUseOnGetVideoDataExtensions['googd']){ + s.cloudDiskUseOnGetVideoDataExtensions['googd'](r).then((dataPipe) => { + dataPipe.pipe(res) + }).catch((err) => { + console.log(err) + res.end(user.lang['File Not Found in Database']) + }) + }else{ + req.pipe(request(r.href)).pipe(res) + } }else{ res.end(user.lang['File Not Found in Database']) } @@ -1659,10 +1433,18 @@ module.exports = function(s,config,lang,app,io){ /** * API : Get Video File */ + const videoRowCaches = {} + const videoRowCacheTimeouts = {} app.get(config.webPaths.apiPrefix+':auth/videos/:ke/:id/:file', function (req,res){ s.auth(req.params,function(user){ - if(user.permissions.watch_videos==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitors.indexOf(req.params.id)===-1){ - res.end(user.lang['Not Permitted']) + const groupKey = req.params.ke + const monitorId = req.params.id + const monitorRestrictions = s.getMonitorRestrictions(user.details,monitorId) + if(user.details.sub && user.details.allmonitors === '0' && (user.permissions.watch_videos === "0" || monitorRestrictions.length === 0)){ + s.closeJsonResponse(res,{ + ok: false, + msg: lang['Not Permitted'] + }) return } var time = s.nameToTime(req.params.file) @@ -1670,20 +1452,51 @@ module.exports = function(s,config,lang,app,io){ time = s.utcToLocal(time) } time = new Date(time) - s.sqlQuery('SELECT * FROM Videos WHERE ke=? AND mid=? AND `time`=? LIMIT 1',[req.params.ke,req.params.id,time],function(err,r){ - if(r&&r[0]){ - req.dir=s.getVideoDirectory(r[0])+req.params.file - fs.stat(req.dir,function(err,stats){ - if (!err){ - s.streamMp4FileOverHttp(req.dir,req,res) + const cacheName = Object.values(req.params).join('_') + const cacheVideoRow = (videoRow) => { + videoRowCaches[cacheName] = videoRow + clearTimeout(videoRowCacheTimeouts[cacheName]) + videoRowCacheTimeouts[cacheName] = setTimeout(() => { + delete(videoRowCaches[cacheName]) + },60000) + } + const sendVideo = (videoRow) => { + cacheVideoRow(videoRow) + const filePath = s.getVideoDirectory(videoRow) + req.params.file + fs.stat(filePath,function(err,stats){ + if (!err){ + if(req.query.json === 'true'){ + s.closeJsonResponse(res,videoRow) }else{ - res.end(user.lang['File Not Found in Filesystem']) + s.streamMp4FileOverHttp(filePath,req,res) } - }) - }else{ - res.end(user.lang['File Not Found in Database']) - } - }) + }else{ + res.end(user.lang['File Not Found in Filesystem']) + } + }) + } + if(videoRowCaches[cacheName]){ + sendVideo(videoRowCaches[cacheName]) + }else{ + s.knexQuery({ + action: "select", + columns: "*", + table: "Videos", + where: [ + ['ke','=',groupKey], + ['mid','=',req.params.id], + ['time','=',time] + ], + limit: 1 + },(err,r) => { + const videoRow = r[0] + if(videoRow){ + sendVideo(videoRow) + }else{ + res.end(user.lang['File Not Found in Database']) + } + }) + } },res,req); }); /** @@ -1691,6 +1504,16 @@ module.exports = function(s,config,lang,app,io){ */ app.get(config.webPaths.apiPrefix+':auth/motion/:ke/:id', function (req,res){ s.auth(req.params,function(user){ + const groupKey = req.params.ke + const monitorId = req.params.id + const monitorRestrictions = s.getMonitorRestrictions(user.details,monitorId) + if(user.details.sub && user.details.allmonitors === '0' && monitorRestrictions.length === 0){ + s.closeJsonResponse(res,{ + ok: false, + msg: lang['Not Permitted'] + }) + return + } if(req.query.data){ try{ var d = { @@ -1756,6 +1579,7 @@ module.exports = function(s,config,lang,app,io){ } break; } + d.doObjectDetection = (!d.details.matrices || d.details.matrices.length === 0) && (s.isAtleatOneDetectorPluginConnected && details.detector_use_detect_object === '1') s.triggerEvent(d) s.closeJsonResponse(res,{ ok: true, @@ -1774,18 +1598,91 @@ module.exports = function(s,config,lang,app,io){ },res,req); }) /** + * API : Object Detection Counter Status + */ + app.get(config.webPaths.apiPrefix+':auth/eventCountStatus/:ke/:id', function (req,res){ + res.setHeader('Content-Type', 'application/json'); + s.auth(req.params,function(user){ + if(user.permissions.watch_videos==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitors.indexOf(req.params.id)===-1){ + res.end(user.lang['Not Permitted']) + return + } + var selectedObject = s.group[req.params.ke].activeMonitors[req.params.id].eventsCounted + res.end(s.prettyPrint({ + ok: true, + counted: Object.keys(selectedObject).length, + tags: selectedObject, + })) + },res,req) + }) + /** + * API : Object Detection Counter Status + */ + app.get([ + config.webPaths.apiPrefix+':auth/eventCounts/:ke', + config.webPaths.apiPrefix+':auth/eventCounts/:ke/:id' + ], function (req,res){ + res.setHeader('Content-Type', 'application/json') + s.auth(req.params,function(user){ + const userDetails = user.details + const monitorId = req.params.id + const groupKey = req.params.ke + var hasRestrictions = userDetails.sub && userDetails.allmonitors !== '1'; + s.sqlQueryBetweenTimesWithPermissions({ + table: 'Events Counts', + user: user, + noCount: true, + groupKey: req.params.ke, + monitorId: req.params.id, + startTime: req.query.start, + endTime: req.query.end, + startTimeOperator: req.query.startOperator, + endTimeOperator: req.query.endOperator, + limit: req.query.limit, + archived: req.query.archived, + endIsStartTo: !!req.query.endIsStartTo, + parseRowDetails: true, + rowName: 'counts', + preliminaryValidationFailed: ( + user.permissions.watch_videos === "0" || + hasRestrictions && + (!userDetails.video_view || userDetails.video_view.indexOf(monitorId)===-1) + ) + },(response) => { + res.end(s.prettyPrint(response)) + }) + },res,req) + }) + /** * API : Camera PTZ Controller */ app.get(config.webPaths.apiPrefix+':auth/control/:ke/:id/:direction', function (req,res){ res.setHeader('Content-Type', 'application/json'); s.auth(req.params,function(user){ - s.cameraControl(req.params,function(resp){ - res.end(s.prettyPrint(resp)) - }); + if(req.params.direction === 'setHome'){ + setPresetForCurrentPosition({ + id: req.params.id, + ke: req.params.ke, + },(response) => { + res.end(s.prettyPrint(response)) + }) + }else{ + ptzControl(req.params,function(msg){ + s.userLog({ + id: req.params.id, + ke: req.params.ke, + },{ + msg: msg, + direction: req.params.direction, + }) + res.end(s.prettyPrint(msg)) + }) + } },res,req); }) /** * API : Upload Video File + * API : Add "streamIn" query string to Push to "Dashcam (Streamer v2)" FFMPEG Process */ app.post(config.webPaths.apiPrefix+':auth/videos/:ke/:id',fileupload(), async (req,res) => { var response = {ok:false} @@ -1795,57 +1692,49 @@ module.exports = function(s,config,lang,app,io){ res.end(user.lang['Not Permitted']) return } - var origURL = req.originalUrl.split('/') - var videoParam = origURL[origURL.indexOf(req.params.auth) + 1] - var videoSet = 'Videos' - req.sql='SELECT * FROM `Monitors` WHERE ke=? AND mid=?'; - req.ar=[req.params.ke,req.params.id]; - s.sqlQuery(req.sql,req.ar,function(err,r){ - if(r && r[0]){ - var monitor = r[0] - // req.query.overwrite === '1' - if(s.group[req.params.ke] && s.group[req.params.ke].activeMonitors[req.params.id]){ - try { - if(!req.files) { - res.send({ - status: false, - message: 'No file uploaded' - }); - } else { - let video = req.files.video; - var time = new Date(parseInt(video.name.split('.')[0])) - var filename = s.formattedTime(time) + '.' + monitor.ext - video.mv(s.getVideoDirectory(monitor) + filename,function(){ - s.insertCompletedVideo(monitor,{ - file : filename - },function(){ - response.ok = true - response.filename = filename - res.end(s.prettyPrint({ - ok: true, - message: 'File is uploaded', - data: { - name: video.name, - mimetype: video.mimetype, - size: video.size - } - })) - }) - }); - } - } catch (err) { - response.err = err - res.status(500).end(response) - } - }else{ - response.error = 'Non Existant Monitor' - res.end(s.prettyPrint(response)) + var groupKey = req.params.ke + var monitorId = req.params.id + // req.query.overwrite === '1' + if(s.group[groupKey] && s.group[groupKey].activeMonitors && s.group[groupKey].activeMonitors[monitorId]){ + var monitor = s.group[groupKey].rawMonitorConfigurations[monitorId] + try { + if(!req.files) { + res.send({ + status: false, + message: 'No file uploaded' + }); + } else { + let video = req.files.video; + var time = new Date(parseInt(video.name.split('.')[0])) + var filename = s.formattedTime(time) + '.' + monitor.ext + video.mv(s.getVideoDirectory(monitor) + filename,function(){ + s.insertCompletedVideo(monitor,{ + file: filename, + events: s.group[groupKey].activeMonitors[monitorId].detector_motion_count, + endTime: req.body.endTime.indexOf('-') > -1 ? s.nameToTime(req.body.endTime) : parseInt(req.body.endTime) || null, + },function(){ + response.ok = true + response.filename = filename + res.end(s.prettyPrint({ + ok: true, + message: 'File is uploaded', + data: { + name: video.name, + mimetype: video.mimetype, + size: video.size + } + })) + }) + }); } - }else{ - response.msg = user.lang['No such file'] - res.end(s.prettyPrint(response)) + } catch (err) { + response.err = err + res.status(500).end(response) } - }) + }else{ + response.error = 'Non Existant Monitor' + res.end(s.prettyPrint(response)) + } },res,req); }) /** @@ -1857,7 +1746,7 @@ module.exports = function(s,config,lang,app,io){ config.webPaths.apiPrefix+':auth/cloudVideos/:ke/:id/:file/:mode', config.webPaths.apiPrefix+':auth/cloudVideos/:ke/:id/:file/:mode/:f' ], function (req,res){ - req.ret={ok:false}; + var response = {ok:false}; res.setHeader('Content-Type', 'application/json'); s.auth(req.params,function(user){ if(user.permissions.watch_videos==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.video_delete.indexOf(req.params.id)===-1){ @@ -1877,14 +1766,24 @@ module.exports = function(s,config,lang,app,io){ videoSet = 'Cloud Videos' break; } - req.sql='SELECT * FROM `'+videoSet+'` WHERE ke=? AND mid=? AND `time`=?'; - req.ar=[req.params.ke,req.params.id,time]; - s.sqlQuery(req.sql,req.ar,function(err,r){ - if(r&&r[0]){ + const groupKey = req.params.ke + const monitorId = req.params.id + s.knexQuery({ + action: "select", + columns: "*", + table: videoSet, + where: [ + ['ke','=',groupKey], + ['mid','=',req.params.id], + ['time','=',time] + ], + limit: 1 + },(err,r) => { + if(r && r[0]){ r=r[0];r.filename=s.formattedTime(r.time)+'.'+r.ext; switch(req.params.mode){ case'fix': - req.ret.ok=true; + response.ok = true; s.video('fix',r) break; case'status': @@ -1896,15 +1795,26 @@ module.exports = function(s,config,lang,app,io){ } r.status = parseInt(req.params.f) if(isNaN(req.params.f)||req.params.f===0){ - req.ret.msg='Not a valid value.'; + response.msg = 'Not a valid value.'; }else{ - req.ret.ok=true; - s.sqlQuery('UPDATE `'+videoSet+'` SET status=? WHERE ke=? AND mid=? AND `time`=?',[req.params.f,req.params.ke,req.params.id,time]) + response.ok = true; + s.knexQuery({ + action: "update", + table: videoSet, + update: { + status: req.params.f + }, + where: [ + ['ke','=',groupKey], + ['mid','=',req.params.id], + ['time','=',time] + ] + }) s.tx(r,'GRP_'+r.ke); } break; case'delete': - req.ret.ok=true; + response.ok = true; switch(videoParam){ case'cloudVideos': s.deleteVideoFromCloud(r) @@ -1915,214 +1825,51 @@ module.exports = function(s,config,lang,app,io){ } break; default: - req.ret.msg=user.lang.modifyVideoText1; + response.msg = user.lang.modifyVideoText1; break; } }else{ - req.ret.msg=user.lang['No such file']; + response.msg = user.lang['No such file']; } - res.end(s.prettyPrint(req.ret)); + res.end(s.prettyPrint(response)); }) },res,req); }) /** * API : Stream In to push data to ffmpeg by HTTP */ - app.all(['/streamIn/:ke/:id','/streamIn/:ke/:id/:feed'], function (req, res) { - var checkOrigin = function(search){return req.headers.host.indexOf(search)>-1} - if(checkOrigin('127.0.0.1')){ - if(!req.params.feed){req.params.feed='1'} - if(!s.group[req.params.ke].activeMonitors[req.params.id].streamIn[req.params.feed]){ - s.group[req.params.ke].activeMonitors[req.params.id].streamIn[req.params.feed] = new events.EventEmitter().setMaxListeners(0) - } - //req.params.feed = Feed Number + app.all('/:auth/streamIn/:ke/:id', function (req, res) { + s.auth(req.params,function(user){ + const ipAddress = s.getClientIp(req) + const groupKey = req.params.ke + const monitorId = req.params.id + const timeStartedConnection = new Date(); + s.userLog({ + ke: groupKey, + mid: monitorId, + },{ + type: "HTTP streamIn Started", + msg: { + ipAddress: ipAddress, + } + }) res.connection.setTimeout(0); req.on('data', function(buffer){ - s.group[req.params.ke].activeMonitors[req.params.id].streamIn[req.params.feed].emit('data',buffer) + s.group[groupKey].activeMonitors[monitorId].spawn.stdin.write(buffer) }); req.on('end',function(){ - // console.log('streamIn closed',req.params); + s.userLog({ + ke: groupKey, + mid: monitorId, + },{ + type: "HTTP streamIn Closed", + msg: { + timeStartedConnection: timeStartedConnection, + ipAddress: ipAddress, + } + }) }); - }else{ - res.end('Local connection is only allowed.') - } - }) - /** - * API : FFprobe - */ - app.get(config.webPaths.apiPrefix+':auth/probe/:ke',function (req,res){ - req.ret={ok:false}; - res.setHeader('Content-Type', 'application/json'); - s.auth(req.params,function(user){ - switch(req.query.action){ - // case'stop': - // exec('kill -9 '+user.ffprobe.pid,{detatched: true}) - // break; - default: - if(!req.query.url){ - req.ret.error = 'Missing URL' - res.end(s.prettyPrint(req.ret)); - return - } - if(user.ffprobe){ - req.ret.error = 'Account is already probing' - res.end(s.prettyPrint(req.ret)); - return - } - user.ffprobe=1; - if(req.query.flags==='default'){ - req.query.flags = '-v quiet -print_format json -show_format -show_streams' - }else{ - if(!req.query.flags){ - req.query.flags = '' - } - } - req.probeCommand = s.splitForFFPMEG(req.query.flags+' -i '+req.query.url).join(' ') - exec('ffprobe '+req.probeCommand+' | echo ',function(err,stdout,stderr){ - delete(user.ffprobe) - if(err){ - req.ret.error=(err) - }else{ - req.ret.ok=true - req.ret.result = stdout+stderr - } - req.ret.probe = req.probeCommand - res.end(s.prettyPrint(req.ret)); - }) - break; - } - },res,req); - }) - /** - * API : ONVIF Method Controller - */ - app.all([ - config.webPaths.apiPrefix+':auth/onvif/:ke/:id/:action', - config.webPaths.apiPrefix+':auth/onvif/:ke/:id/:service/:action' - ],function (req,res){ - var response = {ok:false}; - res.setHeader('Content-Type', 'application/json'); - s.auth(req.params,function(user){ - var errorMessage = function(msg,error){ - response.ok = false - response.msg = msg - response.error = error - res.end(s.prettyPrint(response)) - } - var actionCallback = function(onvifActionResponse){ - response.ok = true - if(onvifActionResponse.data){ - response.responseFromDevice = onvifActionResponse.data - }else{ - response.responseFromDevice = onvifActionResponse - } - if(onvifActionResponse.soap)response.soap = onvifActionResponse.soap - res.end(s.prettyPrint(response)) - } - var isEmpty = function(obj) { - for(var key in obj) { - if(obj.hasOwnProperty(key)) - return false; - } - return true; - } - var doAction = function(Camera){ - var completeAction = function(command){ - if(command.then){ - command.then(actionCallback).catch(function(error){ - errorMessage('Device Action responded with an error',error) - }) - }else if(command){ - response.ok = true - response.repsonseFromDevice = command - res.end(s.prettyPrint(response)) - }else{ - response.error = 'Big Errors, Please report it to Shinobi Development' - res.end(s.prettyPrint(response)) - } - } - var action - if(req.params.service){ - if(Camera.services[req.params.service] === undefined){ - return errorMessage('This is not an available service. Please use one of the following : '+Object.keys(Camera.services).join(', ')) - } - if(Camera.services[req.params.service] === null){ - return errorMessage('This service is not activated. Maybe you are not connected through ONVIF. You can test by attempting to use the "Control" feature with ONVIF in Shinobi.') - } - action = Camera.services[req.params.service][req.params.action] - }else{ - action = Camera[req.params.action] - } - // console.log(s.parseJSON(req.query.options)) - if(!action || typeof action !== 'function'){ - errorMessage(req.params.action+' is not an available ONVIF function. See https://github.com/futomi/node-onvif for functions.') - }else{ - var argNames = s.getFunctionParamNames(action) - var options - var command - if(argNames[0] === 'options' || argNames[0] === 'params'){ - options = {} - if(req.query.options){ - var jsonRevokedText = 'JSON not formated correctly' - try{ - options = JSON.parse(req.query.options) - }catch(err){ - return errorMessage(jsonRevokedText,err) - } - }else if(req.body.options){ - try{ - options = JSON.parse(req.body.options) - }catch(err){ - return errorMessage(jsonRevokedText,err) - } - }else if(req.query.params){ - try{ - options = JSON.parse(req.query.params) - }catch(err){ - return errorMessage(jsonRevokedText,err) - } - }else if(req.body.params){ - try{ - options = JSON.parse(req.body.params) - }catch(err){ - return errorMessage(jsonRevokedText,err) - } - } - } - if(req.params.service){ - command = Camera.services[req.params.service][req.params.action](options) - }else{ - command = Camera[req.params.action](options) - } - completeAction(command) - } - } - if(!s.group[req.params.ke].activeMonitors[req.params.id].onvifConnection){ - //prepeare onvif connection - var controlURL - var monitorConfig = s.group[req.params.ke].rawMonitorConfigurations[req.params.id] - if(!monitorConfig.details.control_base_url||monitorConfig.details.control_base_url===''){ - controlURL = s.buildMonitorUrl(monitorConfig, true) - }else{ - controlURL = monitorConfig.details.control_base_url - } - var controlURLOptions = s.cameraControlOptionsFromUrl(controlURL,monitorConfig) - //create onvif connection - s.group[req.params.ke].activeMonitors[req.params.id].onvifConnection = new onvif.OnvifDevice({ - xaddr : 'http://' + controlURLOptions.host + ':' + controlURLOptions.port + '/onvif/device_service', - user : controlURLOptions.username, - pass : controlURLOptions.password - }) - var device = s.group[req.params.ke].activeMonitors[req.params.id].onvifConnection - device.init().then((info) => { - if(info)doAction(device) - }).catch(function(error){ - return errorMessage('Device responded with an error',error) - }) - }else{ - doAction(s.group[req.params.ke].activeMonitors[req.params.id].onvifConnection) - } - },res,req); + },res,req) }) /** * API : Account Edit from Dashboard diff --git a/libs/webServerStreamPaths.js b/libs/webServerStreamPaths.js index 25ddd8f1..67c3fa23 100644 --- a/libs/webServerStreamPaths.js +++ b/libs/webServerStreamPaths.js @@ -11,12 +11,18 @@ var httpProxy = require('http-proxy'); var proxy = httpProxy.createProxyServer({}) var ejs = require('ejs'); module.exports = function(s,config,lang,app){ + var noCache = function(res){ + res.setHeader('Cache-Control', 'private, no-cache, no-store, must-revalidate') + res.setHeader('Expires', '-1') + res.setHeader('Pragma', 'no-cache') + } /** * Page : Get Embed Stream */ app.get([config.webPaths.apiPrefix+':auth/embed/:ke/:id',config.webPaths.apiPrefix+':auth/embed/:ke/:id/:addon'], function (req,res){ req.params.protocol=req.protocol; s.auth(req.params,function(user){ + noCache(res) if(user.permissions.watch_stream==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitors.indexOf(req.params.id)===-1){ res.end(user.lang['Not Permitted']) return @@ -117,10 +123,10 @@ module.exports = function(s,config,lang,app){ Emitter = s.group[req.params.ke].activeMonitors[req.params.id].emitterChannel[parseInt(req.params.channel)+config.pipeAddition] } res.writeHead(200, { - 'Content-Type': 'multipart/x-mixed-replace; boundary=shinobi', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - 'Pragma': 'no-cache' + 'Content-Type': 'multipart/x-mixed-replace; boundary=shinobi', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'Pragma': 'no-cache' }); var contentWriter fs.readFile(config.defaultMjpeg,'binary',function(err,content){ @@ -164,6 +170,7 @@ module.exports = function(s,config,lang,app){ app.get([config.webPaths.apiPrefix+':auth/hls/:ke/:id/:file',config.webPaths.apiPrefix+':auth/hls/:ke/:id/:channel/:file'], function (req,res){ req.fn=function(user){ s.checkChildProxy(req.params,function(){ + noCache(res) req.dir=s.dir.streams+req.params.ke+'/'+req.params.id+'/' if(req.params.channel){ req.dir+='channel'+(parseInt(req.params.channel)+config.pipeAddition)+'/'+req.params.file; @@ -186,6 +193,7 @@ module.exports = function(s,config,lang,app){ app.get(config.webPaths.apiPrefix+':auth/jpeg/:ke/:id/s.jpg', function(req,res){ s.auth(req.params,function(user){ s.checkChildProxy(req.params,function(){ + noCache(res) if(user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitors&&user.details.monitors.indexOf(req.params.id)===-1){ res.end(user.lang['Not Permitted']) return @@ -206,11 +214,34 @@ module.exports = function(s,config,lang,app){ },res,req); }); /** + * API : Get JPEG Snapshot + */ + app.get(config.webPaths.apiPrefix+':auth/icon/:ke/:id', function(req,res){ + s.auth(req.params,async (user) => { + if(user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitors&&user.details.monitors.indexOf(req.params.id)===-1){ + res.end(user.lang['Not Permitted']) + return + } + res.writeHead(200, { + 'Content-Type': 'image/jpeg', + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache' + }); + res.end(await s.getCameraSnapshot({ + ke: req.params.ke, + mid: req.params.id, + },{ + useIcon: true + })) + },res,req); + }); + /** * API : Get FLV Stream */ app.get([config.webPaths.apiPrefix+':auth/flv/:ke/:id/s.flv',config.webPaths.apiPrefix+':auth/flv/:ke/:id/:channel/s.flv'], function(req,res) { s.auth(req.params,function(user){ s.checkChildProxy(req.params,function(){ + noCache(res) var Emitter,chunkChannel if(!req.params.channel){ Emitter = s.group[req.params.ke].activeMonitors[req.params.id].emitter @@ -261,6 +292,7 @@ module.exports = function(s,config,lang,app){ app.get([config.webPaths.apiPrefix+':auth/h265/:ke/:id/s.hevc',config.webPaths.apiPrefix+':auth/h265/:ke/:id/:channel/s.hevc'], function(req,res) { s.auth(req.params,function(user){ s.checkChildProxy(req.params,function(){ + noCache(res) var Emitter,chunkChannel if(!req.params.channel){ Emitter = s.group[req.params.ke].activeMonitors[req.params.id].emitter @@ -310,6 +342,7 @@ module.exports = function(s,config,lang,app){ ], function (req, res) { s.auth(req.params,function(user){ s.checkChildProxy(req.params,function(){ + noCache(res) if(!req.query.feed){req.query.feed='1'} var Emitter if(!req.params.feed){ diff --git a/libs/webServerSuperPaths.js b/libs/webServerSuperPaths.js index eeebf9a3..ce631f36 100644 --- a/libs/webServerSuperPaths.js +++ b/libs/webServerSuperPaths.js @@ -7,62 +7,31 @@ var exec = require('child_process').exec; var spawn = require('child_process').spawn; var execSync = require('child_process').execSync; module.exports = function(s,config,lang,app){ + /** * API : Superuser : Get Logs */ - app.all([config.webPaths.supersuperApiPrefix+':auth/logs'], function (req,res){ + app.all([config.webPaths.superApiPrefix+':auth/logs'], function (req,res){ req.ret={ok:false}; s.superAuth(req.params,function(resp){ - req.sql='SELECT * FROM Logs WHERE ke=?';req.ar=['$']; - if(!req.params.id){ - if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ - try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} - req.or=[]; - user.details.monitors.forEach(function(v,n){ - req.or.push('mid=?');req.ar.push(v) - }) - req.sql+=' AND ('+req.or.join(' OR ')+')' - } - }else{ - if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1||req.params.id.indexOf('$')>-1){ - req.sql+=' and mid=?';req.ar.push(req.params.id) - }else{ - res.end('[]'); - return; - } - } - if(req.query.start||req.query.end){ - if(!req.query.startOperator||req.query.startOperator==''){ - req.query.startOperator='>=' - } - if(!req.query.endOperator||req.query.endOperator==''){ - req.query.endOperator='<=' - } - if(req.query.start && req.query.start !== '' && req.query.end && req.query.end !== ''){ - req.query.start = s.stringToSqlTime(req.query.start) - req.query.end = s.stringToSqlTime(req.query.end) - req.sql+=' AND `time` '+req.query.startOperator+' ? AND `time` '+req.query.endOperator+' ?'; - req.ar.push(req.query.start) - req.ar.push(req.query.end) - }else if(req.query.start && req.query.start !== ''){ - req.query.start = s.stringToSqlTime(req.query.start) - req.sql+=' AND `time` '+req.query.startOperator+' ?'; - req.ar.push(req.query.start) - } - } - if(!req.query.limit||req.query.limit==''){req.query.limit=50} - req.sql+=' ORDER BY `time` DESC LIMIT '+req.query.limit+''; - s.sqlQuery(req.sql,req.ar,function(err,r){ - if(err){ - err.sql=req.sql; - res.end(s.prettyPrint(err)); - return - } - if(!r){r=[]} - r.forEach(function(v,n){ - r[n].info=JSON.parse(v.info) + const monitorRestrictions = s.getMonitorRestrictions(user.details,req.params.id) + s.getDatabaseRows({ + monitorRestrictions: monitorRestrictions, + table: 'Logs', + groupKey: req.params.ke, + date: req.query.date, + startDate: req.query.start, + endDate: req.query.end, + startOperator: req.query.startOperator, + endOperator: req.query.endOperator, + limit: req.query.limit, + archived: req.query.archived, + endIsStartTo: true + },(response) => { + response.rows.forEach(function(v,n){ + r[n].info = JSON.parse(v.info) }) - res.end(s.prettyPrint(r)) + s.closeJsonResponse(res,r) }) },res,req) }) @@ -71,11 +40,16 @@ module.exports = function(s,config,lang,app){ */ app.all(config.webPaths.superApiPrefix+':auth/logs/delete', function (req,res){ s.superAuth(req.params,function(resp){ - s.sqlQuery('DELETE FROM Logs WHERE ke=?',['$'],function(){ - var endData = { - ok : true + s.knexQuery({ + action: "delete", + table: "Logs", + where: { + ke: '$' } - res.end(s.prettyPrint(endData)) + },() => { + s.closeJsonResponse(res,{ + ok : true + }) }) },res,req) }) @@ -99,7 +73,7 @@ module.exports = function(s,config,lang,app){ var endData = { ok : true } - res.end(s.prettyPrint(endData)) + s.closeJsonResponse(res,endData) },res,req) }) /** @@ -124,7 +98,7 @@ module.exports = function(s,config,lang,app){ s.systemLog('Flush PM2 Logs',{by:resp.$user.mail,ip:resp.ip}) endData.logsOuput = execSync('pm2 flush') } - res.end(s.prettyPrint(endData)) + s.closeJsonResponse(res,endData) },res,req) }) /** @@ -145,11 +119,23 @@ module.exports = function(s,config,lang,app){ ip: resp.ip, old:jsonfile.readFileSync(s.location.config) }) + try{ + if(config.thisIsDocker){ + const dockerConfigFile = '/config/conf.json' + fs.stat(dockerConfigFile,(err) => { + if(!err){ + fs.writeFile(dockerConfigFile,JSON.stringify(postBody,null,3),function(){}) + } + }) + } + }catch(err){ + console.log(err) + } jsonfile.writeFile(s.location.config,postBody,{spaces: 2},function(){ s.tx({f:'save_configuration'},'$') }) } - res.end(s.prettyPrint(endData)) + s.closeJsonResponse(res,endData) },res,req) }) /** @@ -163,22 +149,23 @@ module.exports = function(s,config,lang,app){ var endData = { ok : true } - searchQuery = 'SELECT ke,uid,auth,mail,details FROM Users' - queryVals = [] + const whereQuery = [] switch(req.params.type){ case'admin':case'administrator': - searchQuery += ' WHERE details NOT LIKE ?' - queryVals.push('%"sub"%') + whereQuery.push(['details','NOT LIKE','%"sub"%']) break; case'sub':case'subaccount': - searchQuery += ' WHERE details LIKE ?' - queryVals.push('%"sub"%') + whereQuery.push(['details','LIKE','%"sub"%']) break; } - // ' WHERE details NOT LIKE ?' - s.sqlQuery(searchQuery,queryVals,function(err,users) { + s.knexQuery({ + action: "select", + columns: "ke,uid,auth,mail,details", + table: "Users", + where: whereQuery + },(err,users) => { endData.users = users - res.end(s.prettyPrint(endData)) + s.closeJsonResponse(res,endData) }) },res,req) }) @@ -192,7 +179,7 @@ module.exports = function(s,config,lang,app){ } var form = s.getPostData(req) if(form){ - var currentSuperUserList = jsonfile.readFileSync(s.location.super) + var currentSuperUserList = JSON.parse(fs.readFileSync(s.location.super)) var currentSuperUser = {} var currentSuperUserPosition = -1 //find this user in current list @@ -230,14 +217,26 @@ module.exports = function(s,config,lang,app){ currentSuperUserList.push(currentSuperUser) } //update master list in system - jsonfile.writeFile(s.location.super,currentSuperUserList,{spaces: 2},function(){ + try{ + if(config.thisIsDocker){ + const dockerSuperFile = '/config/super.json' + fs.stat(dockerSuperFile,(err) => { + if(!err){ + fs.writeFile(dockerSuperFile,JSON.stringify(currentSuperUserList,null,3),function(){}) + } + }) + } + }catch(err){ + console.log(err) + } + fs.writeFile(s.location.super,JSON.stringify(currentSuperUserList,null,3),function(){ s.tx({f:'save_preferences'},'$') }) }else{ endData.ok = false endData.msg = lang.postDataBroken } - res.end(s.prettyPrint(endData)) + s.closeJsonResponse(res,endData) },res,req) }) /** @@ -249,7 +248,7 @@ module.exports = function(s,config,lang,app){ ok : false } var close = function(){ - res.end(s.prettyPrint(endData)) + s.closeJsonResponse(res,endData) } var isCallbacking = false var form = s.getPostData(req) @@ -257,7 +256,14 @@ module.exports = function(s,config,lang,app){ if(form.mail !== '' && form.pass !== ''){ if(form.pass === form.password_again || form.pass === form.pass_again){ isCallbacking = true - s.sqlQuery('SELECT * FROM Users WHERE mail=?',[form.mail],function(err,r) { + s.knexQuery({ + action: "select", + columns: "*", + table: "Users", + where: [ + ['mail','=',form.mail] + ] + },(err,r) => { if(r&&r[0]){ //found address already exists endData.msg = lang['Email address is in use.']; @@ -277,16 +283,17 @@ module.exports = function(s,config,lang,app){ form.details = JSON.stringify(form.details) } //write user to db - s.sqlQuery( - 'INSERT INTO Users (ke,uid,mail,pass,details) VALUES (?,?,?,?,?)', - [ - form.ke, - form.uid, - form.mail, - s.createHash(form.pass), - form.details - ] - ) + s.knexQuery({ + action: "insert", + table: "Users", + insert: { + ke: form.ke, + uid: form.uid, + mail: form.mail, + pass: s.createHash(form.pass), + details: form.details + } + }) s.tx({f:'add_account',details:form.details,ke:form.ke,uid:form.uid,mail:form.mail},'$') endData.user = Object.assign(form,{}) //init user @@ -315,12 +322,19 @@ module.exports = function(s,config,lang,app){ ok : false } var close = function(){ - res.end(s.prettyPrint(endData)) + s.closeJsonResponse(res,endData) } var form = s.getPostData(req) if(form){ var account = s.getPostData(req,'account') - s.sqlQuery('SELECT * FROM Users WHERE mail=?',[account.mail],function(err,r) { + s.knexQuery({ + action: "select", + columns: "*", + table: "Users", + where: [ + ['mail','=',account.mail] + ] + },(err,r) => { if(r && r[0]){ r = r[0] var details = JSON.parse(r.details) @@ -337,25 +351,16 @@ module.exports = function(s,config,lang,app){ } delete(form.password_again); delete(form.pass_again); - var keys = Object.keys(form) - var set = [] - var values = [] - keys.forEach(function(v,n){ - if( - set === 'ke' || - !form[v] - ){ - //skip - return - } - set.push(v+'=?') - if(v === 'details'){ - form[v] = s.stringJSON(Object.assign(details,s.parseJSON(form[v]))) - } - values.push(form[v]) - }) - values.push(account.mail) - s.sqlQuery('UPDATE Users SET '+set.join(',')+' WHERE mail=?',values,function(err,r) { + delete(form.ke); + form.details = s.stringJSON(Object.assign(details,s.parseJSON(form.details))) + s.knexQuery({ + action: "update", + table: "Users", + update: form, + where: [ + ['mail','=',account.mail], + ] + },(err,r) => { if(err){ console.log(err) endData.error = err @@ -388,32 +393,78 @@ module.exports = function(s,config,lang,app){ ok : true } var close = function(){ - res.end(s.prettyPrint(endData)) + s.closeJsonResponse(res,endData) } var account = s.getPostData(req,'account') - s.sqlQuery('DELETE FROM Users WHERE uid=? AND ke=? AND mail=?',[account.uid,account.ke,account.mail]) - s.sqlQuery('DELETE FROM API WHERE uid=? AND ke=?',[account.uid,account.ke]) + s.knexQuery({ + action: "delete", + table: "Users", + where: { + ke: account.ke, + uid: account.uid, + mail: account.mail, + } + }) + s.knexQuery({ + action: "delete", + table: "API", + where: { + ke: account.ke, + uid: account.uid, + } + }) if(s.getPostData(req,'deleteSubAccounts',false) === '1'){ - s.sqlQuery('DELETE FROM Users WHERE ke=?',[account.ke]) + s.knexQuery({ + action: "delete", + table: "Users", + where: { + ke: account.ke, + } + }) } if(s.getPostData(req,'deleteMonitors',false) == '1'){ - s.sqlQuery('SELECT * FROM Monitors WHERE ke=?',[account.ke],function(err,monitors){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Monitors", + where: { + ke: account.ke, + } + },(err,monitors) => { if(monitors && monitors[0]){ monitors.forEach(function(monitor){ s.camera('stop',monitor) }) - s.sqlQuery('DELETE FROM Monitors WHERE ke=?',[account.ke]) + s.knexQuery({ + action: "delete", + table: "Monitors", + where: { + ke: account.ke, + } + }) } }) } if(s.getPostData(req,'deleteVideos',false) == '1'){ - s.sqlQuery('DELETE FROM Videos WHERE ke=?',[account.ke]) + s.knexQuery({ + action: "delete", + table: "Videos", + where: { + ke: account.ke, + } + }) fs.chmod(s.dir.videos+account.ke,0o777,function(err){ fs.unlink(s.dir.videos+account.ke,function(err){}) }) } if(s.getPostData(req,'deleteEvents',false) == '1'){ - s.sqlQuery('DELETE FROM Events WHERE ke=?',[account.ke]) + s.knexQuery({ + action: "delete", + table: "Events", + where: { + ke: account.ke, + } + }) } s.tx({f:'delete_account',ke:account.ke,uid:account.uid,mail:account.mail},'$') close() @@ -449,7 +500,11 @@ module.exports = function(s,config,lang,app){ if(tableName){ var tableIsSelected = s.getPostData(req,tableName) == 1 if(tableIsSelected){ - s.sqlQuery('SELECT * FROM `' + tableName +'`',[],function(err,dataRows){ + s.knexQuery({ + action: "select", + columns: "*", + table: tableName + },(err,dataRows) => { endData.database[tableName] = dataRows ++completedTables tableExportLoop(callback) @@ -551,26 +606,26 @@ module.exports = function(s,config,lang,app){ ]) break; } - var keysToCheck = [] - var valuesToCheck = [] + const whereQuery = [] fieldsToCheck.forEach(function(key){ - keysToCheck.push(key + '= ?') - valuesToCheck.push(row[key]) + whereQuery.push([key,'=',row[key]]) }) - s.sqlQuery('SELECT * FROM ' + tableName + ' WHERE ' + keysToCheck.join(' AND '),valuesToCheck,function(err,selected){ + s.knexQuery({ + action: "select", + columns: "*", + table: tableName, + where: whereQuery + },(err,selected) => { if(selected && selected[0]){ selected = selected[0] rowsExistingAlready[tableName].push(selected) callback() }else{ - var rowKeys = Object.keys(row) - var insertEscapes = [] - var insertValues = [] - rowKeys.forEach(function(key){ - insertEscapes.push('?') - insertValues.push(row[key]) - }) - s.sqlQuery('INSERT INTO ' + tableName + ' (' + rowKeys.join(',') +') VALUES (' + insertEscapes.join(',') + ')',insertValues,function(){ + s.knexQuery({ + action: "insert", + table: tableName, + insert: row + },(err) => { if(!err){ ++countOfRowsInserted[tableName] } @@ -631,7 +686,7 @@ module.exports = function(s,config,lang,app){ ok : true } s.checkForStalePurgeLocks() - res.end(s.prettyPrint(endData)) + s.closeJsonResponse(res,endData) },res,req) }) /** @@ -669,7 +724,7 @@ module.exports = function(s,config,lang,app){ ok : true, childNodes: childNodesJson, } - res.end(s.prettyPrint(endData)) + s.closeJsonResponse(res,endData) },res,req) }) } diff --git a/package.json b/package.json index 946037d2..4f9d5598 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,6 @@ "version": "2.0.0", "description": "CCTV and NVR in Node.js", "main": "camera.js", - "bin": "camera.js", - "scripts": { - "test": "node camera.js test", - "start": "chmod +x INSTALL/start.sh && INSTALL/start.sh" - }, "repository": { "type": "git", "url": "git+https://gitlab.com/Shinobi-Systems/Shinobi.git" @@ -17,51 +12,68 @@ "bugs": { "url": "https://gitlab.com/Shinobi-Systems/Shinobi/issues" }, - "pkg": { - "assets": [ - "libs/**/*", - "libs/**/**/*", - "libs/**/**/**/*", - "libs/**/**/**/**/*", - "languages/*", - "web/*", - "node_modules/ffmpeg-static/*", - "definitions/*" - ] - }, "homepage": "https://gitlab.com/Shinobi-Systems/Shinobi#readme", "dependencies": { "async": "^3.1.0", - "aws-sdk": "^2.279.1", - "backblaze-b2": "^1.0.4", - "body-parser": "^1.18.3", - "connection-tester": "^0.1.1", - "cws": "^1.0.0", - "discord.js": "^11.3.2", + "aws-sdk": "^2.731.0", + "backblaze-b2": "^1.5.0", + "body-parser": "^1.19.0", + "connection-tester": "^0.2.0", + "cws": "^1.2.11", + "discord.js": "^12.2.0", "ejs": "^2.5.5", "express": "^4.16.4", - "ftp-srv": "^4.0.0", + "ftp-srv": "4.3.4", "http-proxy": "^1.17.0", "jsonfile": "^3.0.1", - "knex": "^0.19.5", - "ldapauth-fork": "^4.0.2", - "moment": "^2.17.0", - "mp4frag": "^0.0.22", - "mysql": "^2.16.0", - "node-onvif": "^0.1.4", + "knex": "^0.21.4", + "ldapauth-fork": "^4.3.3", + "moment": "^2.27.0", + "mp4frag": "^0.2.0", + "mysql": "^2.18.1", + "node-onvif": "^0.1.7", "node-ssh": "^5.1.2", - "nodemailer": "^4.0.1", - "pam-diff": "^0.12.1", + "nodemailer": "^6.4.11", + "pam-diff": "^1.0.0", "path": "^0.12.7", "pipe2pam": "^0.6.2", "request": "^2.88.0", "sat": "^0.7.1", - "shinobi-sound-detection": "^0.1.7", + "shinobi-sound-detection": "^0.1.8", "smtp-server": "^3.5.0", - "socket.io": "^2.2.0", - "socket.io-client": "^2.2.0", + "socket.io": "^2.3.0", + "socket.io-client": "^2.3.0", "webdav-fs": "^1.11.0", - "express-fileupload": "^1.1.6-alpha.6" + "express-fileupload": "^1.1.6-alpha.6", + "googleapis": "^39.2.0", + "tree-kill":"1.2.2", + "unzipper":"0.10.11", + "node-fetch":"2.6.0", + "fs-extra": "9.0.1" }, - "devDependencies": {} + "devDependencies": {}, + "bin": "camera.js", + "scripts": { + "test": "node camera.js test", + "start": "chmod +x INSTALL/start.sh && INSTALL/start.sh", + "package": "pkg package.json -t linux,macos,win --out-path dist", + "package-x64": "pkg package.json -t linux-x64,macos-x64,win-x64 --out-path dist/x64", + "package-x86": "pkg package.json -t linux-x86,macos-x86,win-x86 --out-path dist/x86", + "package-armv6": "pkg package.json -t linux-armv6,macos-armv6,win-armv6 --out-path dist/armv6", + "package-armv7": "pkg package.json -t linux-armv7,macos-armv7,win-armv7 --out-path dist/armv7", + "package-all": "npm run package && npm run package-x64 && npm run package-x86 && npm run package-armv6 && npm run package-armv7" + }, + "pkg": { + "targets": [ + "node12" + ], + "scripts": [ + ], + "assets": [ + "definitions/*", + "languages/*", + "web/*", + "test/*" + ] + } } diff --git a/plugins/dlib/INSTALL.sh b/plugins/dlib/INSTALL.sh deleted file mode 100644 index 6d0e7450..00000000 --- a/plugins/dlib/INSTALL.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash -THE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -sudo apt update -y -sudo apt-get install libx11-dev -y -sudo apt-get install libpng-dev -y -sudo apt-get install libopenblas-dev -y -echo "----------------------------------------" -echo "-- Installing Dlib Plugin for Shinobi --" -echo "----------------------------------------" -if ! [ -x "$(command -v nvidia-smi)" ]; then - echo "You need to install NVIDIA Drivers to use this." - echo "inside the Shinobi directory run the following :" - echo "sh INSTALL/cuda.sh" - exit 1 -else - echo "NVIDIA Drivers found..." - echo "$(nvidia-smi |grep 'Driver Version')" -fi -echo "-----------------------------------" -if [ ! -d "/usr/local/cuda" ]; then - echo "You need to install CUDA Toolkit to use this." - echo "inside the Shinobi directory run the following :" - echo "sh INSTALL/cuda.sh" - exit 1 -else - echo "CUDA Toolkit found..." -fi -echo "-----------------------------------" -if [ ! -e "./conf.json" ]; then - echo "Creating conf.json" - sudo cp conf.sample.json conf.json -else - echo "conf.json already exists..." -fi -npm i npm -g -echo "-----------------------------------" -echo "Getting node-gyp to build C++ modules" -npm install node-gyp -g --unsafe-perm -echo "-----------------------------------" -echo "Getting C++ module : face-recognition" -echo "https://gitlab.com/Shinobi-Systems/face-recognition-js-cuda" -npm install --unsafe-perm -npm audit fix --force -cd $THE_DIR -echo "-----------------------------------" -echo "Start the plugin with pm2 like so :" -echo "pm2 start shinobi-dlib.js" -echo "-----------------------------------" -echo "Start the plugin without pm2 :" -echo "node shinobi-dlib.js" diff --git a/plugins/dlib/conf.sample.json b/plugins/dlib/conf.sample.json deleted file mode 100644 index 38e37d86..00000000 --- a/plugins/dlib/conf.sample.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "plug":"Dlib", - "host":"localhost", - "port":8080, - "key":"Dlib123123", - "mode":"client", - "type":"detector", - "connectionType":"websocket" -} diff --git a/plugins/dlib/package.json b/plugins/dlib/package.json deleted file mode 100644 index 3fd5717c..00000000 --- a/plugins/dlib/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "shinobi-dlib", - "version": "1.0.0", - "description": "Dlib plugin for Shinobi that uses C++ functions for detection.", - "main": "shinobi-dlib.js", - "dependencies": { - "socket.io-client": "^1.7.4", - "express": "^4.16.2", - "moment": "^2.19.2", - "socket.io": "^2.0.4", - "face-recognition-cuda": "0.9.3" - }, - "devDependencies": {}, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "Moe Alam", - "license": "ISC" -} diff --git a/plugins/dlib/shinobi-dlib.js b/plugins/dlib/shinobi-dlib.js deleted file mode 100644 index 590131b0..00000000 --- a/plugins/dlib/shinobi-dlib.js +++ /dev/null @@ -1,97 +0,0 @@ -// -// Shinobi - Dlib Plugin -// Copyright (C) 2016-2025 Moe Alam, moeiscool -// -// # Donate -// -// If you like what I am doing here and want me to continue please consider donating :) -// PayPal : paypal@m03.ca -// -// Base Init >> -var fs = require('fs'); -var config = require('./conf.json') -var s -try{ - s = require('../pluginBase.js')(__dirname,config) -}catch(err){ - console.log(err) - try{ - s = require('./pluginBase.js')(__dirname,config) - }catch(err){ - console.log(err) - return console.log(config.plug,'Plugin start has failed. pluginBase.js was not found.') - } -} -// Base Init />> -var fr = require('face-recognition-cuda');//modified "binding.gyp" file for "face-recognition" to build dlib with cuda -const detector = fr.FaceDetector() -s.detectObject=function(buffer,d,tx,frameLocation){ - var detectStuff = function(frame){ - try{ - var buffer = fr.loadImage(frame) - var faceRectangles = detector.locateFaces(buffer) - var matrices = [] - faceRectangles.forEach(function(v){ - var coordinates = [ - {"x" : v.rect.left, "y" : v.rect.top}, - {"x" : v.rect.right, "y" : v.rect.top}, - {"x" : v.rect.right, "y" : v.rect.bottom} - ] - var width = Math.sqrt( Math.pow(coordinates[1].x - coordinates[0].x, 2) + Math.pow(coordinates[1].y - coordinates[0].y, 2)); - var height = Math.sqrt( Math.pow(coordinates[2].x - coordinates[1].x, 2) + Math.pow(coordinates[2].y - coordinates[1].y, 2)) - matrices.push({ - x: coordinates[0].x, - y: coordinates[0].y, - width: width, - height: height, - tag: 'UNKNOWN FACE', - confidence: v.confidence, - }) - }) - if(matrices.length > 0){ - tx({ - f: 'trigger', - id: d.id, - ke: d.ke, - details:{ - plug: config.plug, - name: 'dlib', - reason: 'object', - matrices: matrices, - imgHeight: parseFloat(d.mon.detector_scale_y), - imgWidth: parseFloat(d.mon.detector_scale_x) - } - }) - } - fs.unlink(frame,function(){ - - }) - }catch(err){ - console.log(err) - } - } - if(frameLocation){ - detectStuff(frameLocation) - }else{ - d.tmpFile=s.gid(5)+'.jpg' - if(!fs.existsSync(s.dir.streams)){ - fs.mkdirSync(s.dir.streams); - } - d.dir=s.dir.streams+d.ke+'/' - if(!fs.existsSync(d.dir)){ - fs.mkdirSync(d.dir); - } - d.dir=s.dir.streams+d.ke+'/'+d.id+'/' - if(!fs.existsSync(d.dir)){ - fs.mkdirSync(d.dir); - } - fs.writeFile(d.dir+d.tmpFile,buffer,function(err){ - if(err) return s.systemLog(err); - try{ - detectStuff(d.dir+d.tmpFile) - }catch(error){ - console.error('Catch: ' + error); - } - }) - } -} diff --git a/plugins/dnnCoco/.gitignore b/plugins/dnnCoco/.gitignore deleted file mode 100644 index 342a4b13..00000000 --- a/plugins/dnnCoco/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -conf.json -data \ No newline at end of file diff --git a/plugins/dnnCoco/INSTALL.sh b/plugins/dnnCoco/INSTALL.sh deleted file mode 100644 index 476b40b1..00000000 --- a/plugins/dnnCoco/INSTALL.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -mkdir data -chmod -R 777 data -wget https://cdn.shinobi.video/weights/dnnCocoData.zip -O dnnCocoData.zip -unzip dnnCocoData.zip -d data -if [ $(dpkg-query -W -f='${Status}' opencv_version 2>/dev/null | grep -c "ok installed") -eq 0 ]; then - echo "Shinobi - Do ypu want to let the `opencv4nodejs` npm package install OpenCV? " - echo "Only do this if you do not have OpenCV already or will not use a GPU (Hardware Acceleration)." - echo "(y)es or (N)o" - read nodejsinstall - if [ "$nodejsinstall" = "y" ] || [ "$nodejsinstall" = "Y" ]; then - export OPENCV4NODEJS_DISABLE_AUTOBUILD=0 - else - export OPENCV4NODEJS_DISABLE_AUTOBUILD=1 - fi -else - export OPENCV4NODEJS_DISABLE_AUTOBUILD=1 -fi -npm install opencv4nodejs moment express canvas@1.6 --unsafe-perm \ No newline at end of file diff --git a/plugins/dnnCoco/conf.sample.json b/plugins/dnnCoco/conf.sample.json deleted file mode 100644 index 831ea794..00000000 --- a/plugins/dnnCoco/conf.sample.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "plug":"Coco", - "host":"localhost", - "port":8080, - "hostPort":8082, - "key":"change_this_to_something_very_random____make_sure_to_match__/plugins/opencv/conf.json", - "mode":"client", - "type":"detector" -} \ No newline at end of file diff --git a/plugins/dnnCoco/dnnCocoClassNames.js b/plugins/dnnCoco/dnnCocoClassNames.js deleted file mode 100644 index 6c0e980c..00000000 --- a/plugins/dnnCoco/dnnCocoClassNames.js +++ /dev/null @@ -1,83 +0,0 @@ -module.exports = [ - 'background', - 'person', - 'bicycle', - 'car', - 'motorcycle', - 'airplane', - 'bus', - 'train', - 'truck', - 'boat', - 'traffic light', - 'fire hydrant', - 'stop sign', - 'parking meter', - 'bench', - 'bird', - 'cat', - 'dog', - 'horse', - 'sheep', - 'cow', - 'elephant', - 'bear', - 'zebra', - 'giraffe', - 'backpack', - 'umbrella', - 'handbag', - 'tie', - 'suitcase', - 'frisbee', - 'skis', - 'snowboard', - 'sports ball', - 'kite', - 'baseball bat', - 'baseball glove', - 'skateboard', - 'surfboard', - 'tennis racket', - 'bottle', - 'wine glass', - 'cup', - 'fork', - 'knife', - 'spoon', - 'bowl', - 'banana', - 'apple', - 'sandwich', - 'orange', - 'broccoli', - 'carrot', - 'hot dog', - 'pizza', - 'donut', - 'cake', - 'chair', - 'couch', - 'potted plant', - 'bed', - 'dining table', - 'toilet', - 'tv', - 'laptop', - 'mouse', - 'remote', - 'keyboard', - 'cell phone', - 'microwave', - 'oven', - 'toaster', - 'sink', - 'refrigerator', - 'book', - 'clock', - 'vase', - 'scissors', - 'teddy bear', - 'hair drier', - 'toothbrush' -]; diff --git a/plugins/dnnCoco/openalpr.conf b/plugins/dnnCoco/openalpr.conf deleted file mode 100644 index 070752b1..00000000 --- a/plugins/dnnCoco/openalpr.conf +++ /dev/null @@ -1,94 +0,0 @@ - -; Specify the path to the runtime data directory -runtime_dir = ${CMAKE_INSTALL_PREFIX}/share/openalpr/runtime_data - - -ocr_img_size_percent = 1.33333333 -state_id_img_size_percent = 2.0 - -; Calibrating your camera improves detection accuracy in cases where vehicle plates are captured at a steep angle -; Use the openalpr-utils-calibrate utility to calibrate your fixed camera to adjust for an angle -; Once done, update the prewarp config with the values obtained from the tool -prewarp = - -; detection will ignore plates that are too large. This is a good efficiency technique to use if the -; plates are going to be a fixed distance away from the camera (e.g., you will never see plates that fill -; up the entire image -max_plate_width_percent = 100 -max_plate_height_percent = 100 - -; detection_iteration_increase is the percentage that the LBP frame increases each iteration. -; It must be greater than 1.0. A value of 1.01 means increase by 1%, 1.10 increases it by 10% each time. -; So a 1% increase would be ~10x slower than 10% to process, but it has a higher chance of landing -; directly on the plate and getting a strong detection -detection_iteration_increase = 1.1 - -; The minimum detection strength determines how sure the detection algorithm must be before signaling that -; a plate region exists. Technically this corresponds to LBP nearest neighbors (e.g., how many detections -; are clustered around the same area). For example, 2 = very lenient, 9 = very strict. -detection_strictness = 3 - -; The detection doesn't necessarily need an extremely high resolution image in order to detect plates -; Using a smaller input image should still find the plates and will do it faster -; Tweaking the max_detection_input values will resize the input image if it is larger than these sizes -; max_detection_input_width/height are specified in pixels -max_detection_input_width = 1280 -max_detection_input_height = 720 - -; detector is the technique used to find license plate regions in an image. Value can be set to -; lbpcpu - default LBP-based detector uses the system CPU -; lbpgpu - LBP-based detector that uses Nvidia GPU to increase recognition speed. -; lbpopencl - LBP-based detector that uses OpenCL GPU to increase recognition speed. Requires OpenCV 3.0 -; morphcpu - Experimental detector that detects white rectangles in an image. Does not require training. -detector = lbpgpu - -; If set to true, all results must match a postprocess text pattern if a pattern is available. -; If not, the result is disqualified. -must_match_pattern = 0 - -; Bypasses plate detection. If this is set to 1, the library assumes that each region provided is a likely plate area. -skip_detection = 0 - -; Specifies the full path to an image file that constrains the detection area. Only the plate regions allowed through the mask -; will be analyzed. The mask image must match the resolution of your image to be analyzed. The mask is black and white. -; Black areas will be ignored, white areas will be searched. An empty value means no mask (scan the entire image) -detection_mask_image = - -; OpenALPR can scan the same image multiple times with different randomization. Setting this to a value larger than -; 1 may increase accuracy, but will increase processing time linearly (e.g., analysis_count = 3 is 3x slower) -analysis_count = 1 - -; OpenALPR detects high-contrast plate crops and uses an alternative edge detection technique. Setting this to 0.0 -; would classify ALL images as high-contrast, setting it to 1.0 would classify no images as high-contrast. -contrast_detection_threshold = 0.3 - -max_plate_angle_degrees = 15 - -ocr_min_font_point = 6 - -; Minimum OCR confidence percent to consider. -postprocess_min_confidence = 65 - -; Any OCR character lower than this will also add an equally likely -; chance that the character is incorrect and will be skipped. Value is a confidence percent -postprocess_confidence_skip_level = 80 - - -debug_general = 0 -debug_timing = 0 -debug_detector = 0 -debug_prewarp = 0 -debug_state_id = 0 -debug_plate_lines = 0 -debug_plate_corners = 0 -debug_char_segment = 0 -debug_char_analysis = 0 -debug_color_filter = 0 -debug_ocr = 0 -debug_postprocess = 0 -debug_show_images = 0 -debug_pause_on_frame = 0 - - - - diff --git a/plugins/dnnCoco/shinobi-coco.js b/plugins/dnnCoco/shinobi-coco.js deleted file mode 100644 index 0bb440cd..00000000 --- a/plugins/dnnCoco/shinobi-coco.js +++ /dev/null @@ -1,525 +0,0 @@ -// -// Shinobi - OpenCV Plugin -// Copyright (C) 2016-2025 Moe Alam, moeiscool -// -// # Donate -// -// If you like what I am doing here and want me to continue please consider donating :) -// PayPal : paypal@m03.ca -// -process.on('uncaughtException', function (err) { - console.error('uncaughtException',err); -}); -var fs=require('fs'); -var cv=require('opencv4nodejs'); -var exec = require('child_process').exec; -var moment = require('moment'); -var Canvas = require('canvas'); -var express = require('express'); -const path = require('path'); -var http = require('http'), - app = express(), - server = http.createServer(app); -var config=require('./conf.json'); -if(!config.port){config.port=8080} -if(!config.hostPort){config.hostPort=8082} -if(config.systemLog===undefined){config.systemLog=true} -if(config.cascadesDir===undefined){config.cascadesDir=__dirname+'/cascades/'} -if(config.alprConfig===undefined){config.alprConfig=__dirname+'/openalpr.conf'} - - -const classNames = require(__dirname+'/dnnCocoClassNames.js'); -const extractResults = function (outputBlob, imgDimensions) { - return Array(outputBlob.rows).fill(0) - .map((res, i) => { - const classLabel = outputBlob.at(i, 1); - const confidence = outputBlob.at(i, 2); - const bottomLeft = new cv.Point( - outputBlob.at(i, 3) * imgDimensions.cols, - outputBlob.at(i, 6) * imgDimensions.rows - ); - const topRight = new cv.Point( - outputBlob.at(i, 5) * imgDimensions.cols, - outputBlob.at(i, 4) * imgDimensions.rows - ); - const rect = new cv.Rect( - bottomLeft.x, - topRight.y, - topRight.x - bottomLeft.x, - bottomLeft.y - topRight.y - ); - - return ({ - classLabel, - confidence, - rect - }); - }); -}; -// replace with path where you unzipped inception model -const ssdcocoModelPath = __dirname+'/data'; - -const prototxt = path.resolve(ssdcocoModelPath, 'deploy.prototxt'); -const modelFile = path.resolve(ssdcocoModelPath, 'VGG_coco_SSD_300x300_iter_400000.caffemodel'); - -if (!fs.existsSync(prototxt) || !fs.existsSync(modelFile)) { - console.log('could not find ssdcoco model'); - console.log('download the model from: https://cdn.shinobi.video/weights/dnnCocoData.zip'); - throw new Error('exiting: could not find ssdcoco model'); -} - -// initialize ssdcoco model from prototxt and modelFile -const net = cv.readNetFromCaffe(prototxt, modelFile); - -function classifyImg(img) { - // ssdcoco model works with 300 x 300 images - const imgResized = img.resize(300, 300); - - // network accepts blobs as input - const inputBlob = cv.blobFromImage(imgResized); - net.setInput(inputBlob); - - // forward pass input through entire network, will return - // classification result as 1x1xNxM Mat - let outputBlob = net.forward(); - // extract NxM Mat - outputBlob = outputBlob.flattenFloat(outputBlob.sizes[2], outputBlob.sizes[3]); - - return extractResults(outputBlob, img) - .map(r => Object.assign({}, r.rect, { confidence : r.confidence, tag: classNames[r.classLabel] })); -} - -const makeDrawClassDetections = predictions => (drawImg, className, getColor, thickness = 2) => { - predictions - .filter(p => classNames[p.classLabel] === className) - .forEach(p => console.log(p)); -}; - - -s={ - group:{}, - dir:{ - cascades : config.cascadesDir - }, - isWin:(process.platform==='win32'), - foundCascades : { - - } -} -//default stream folder check -if(!config.streamDir){ - if(s.isWin===false){ - config.streamDir='/dev/shm' - }else{ - config.streamDir=config.windowsTempDir - } - if(!fs.existsSync(config.streamDir)){ - config.streamDir=__dirname+'/streams/' - }else{ - config.streamDir+='/streams/' - } -} -s.dir.streams=config.streamDir; -//streams dir -if(!fs.existsSync(s.dir.streams)){ - fs.mkdirSync(s.dir.streams); -} -//streams dir -if(!fs.existsSync(s.dir.cascades)){ - fs.mkdirSync(s.dir.cascades); -} -s.gid=function(x){ - if(!x){x=10};var t = "";var p = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - for( var i=0; i < x; i++ ) - t += p.charAt(Math.floor(Math.random() * p.length)); - return t; -}; -s.findCascades=function(callback){ - var tmp={}; - tmp.foundCascades=[]; - fs.readdir(s.dir.cascades,function(err,files){ - files.forEach(function(cascade,n){ - if(cascade.indexOf('.xml')>-1){ - tmp.foundCascades.push(cascade.replace('.xml','')) - } - }) - s.cascadesInDir=tmp.foundCascades; - callback(tmp.foundCascades) - }) -} -s.findCascades(function(){ - //get cascades -}) -s.detectLicensePlate=function(buffer,d,tx){ - if(!d.mon.detector_lisence_plate_country||d.mon.detector_lisence_plate_country===''){ - d.mon.detector_lisence_plate_country='us' - } - d.tmpFile=s.gid(5)+'.jpg' - if(!fs.existsSync(s.dir.streams)){ - fs.mkdirSync(s.dir.streams); - } - d.dir=s.dir.streams+d.ke+'/' - if(!fs.existsSync(d.dir)){ - fs.mkdirSync(d.dir); - } - d.dir=s.dir.streams+d.ke+'/'+d.id+'/' - if(!fs.existsSync(d.dir)){ - fs.mkdirSync(d.dir); - } - fs.writeFile(d.dir+d.tmpFile,buffer,function(err){ - if(err) return s.systemLog(err); - exec('alpr -j --config '+config.alprConfig+' -c '+d.mon.detector_lisence_plate_country+' '+d.dir+d.tmpFile,{encoding:'utf8'},(err, scan, stderr) => { - if(err){ - s.systemLog(err); - }else{ - try{ - scan=JSON.parse(scan.replace('--(!)Loaded CUDA classifier','').trim()) - }catch(err){ - if(!scan||!scan.results){ - return s.systemLog(scan,err); - } - } - if(scan.results.length>0){ - scan.plates=[] - scan.mats=[] - scan.results.forEach(function(v){ - v.candidates.forEach(function(g,n){ - if(v.candidates[n].matches_template) - delete(v.candidates[n].matches_template) - }) - scan.plates.push({coordinates:v.coordinates,candidates:v.candidates,confidence:v.confidence,plate:v.plate}) - var width = Math.sqrt( Math.pow(v.coordinates[1].x - v.coordinates[0].x, 2) + Math.pow(v.coordinates[1].y - v.coordinates[0].y, 2)); - var height = Math.sqrt( Math.pow(v.coordinates[2].x - v.coordinates[1].x, 2) + Math.pow(v.coordinates[2].y - v.coordinates[1].y, 2)) - scan.mats.push({ - x:v.coordinates[0].x, - y:v.coordinates[0].y, - width:width, - height:height, - tag:v.plate - }) - }) - tx({f:'trigger',id:d.id,ke:d.ke,details:{split:true,plug:config.plug,name:'licensePlate',reason:'object',matrices:scan.mats,imgHeight:d.mon.detector_scale_y,imgWidth:d.mon.detector_scale_x,frame:d.base64}}) - } - } - exec('rm -rf '+d.dir+d.tmpFile,{encoding:'utf8'}) - }) - }) -} -s.detectObject=function(buffer,d,tx){ - //detect license plate? - if(d.mon.detector_lisence_plate==="1"){ - s.detectLicensePlate(buffer,d,tx) - } - cv.imdecodeAsync(buffer,(err,im) => { - if(err){ - console.log(err) - return - } - - if (!cv.xmodules.dnn) { - throw new Error('exiting: opencv4nodejs compiled without dnn module'); - } - - - const minConfidence = 0.5; - - const predictions = classifyImg(im).filter(res => res.confidence > minConfidence); -// console.log(predictions) - if(predictions.length > 0) { - s.cx({ - f:'trigger', - id:d.id, - ke:d.ke, - name:'coco', - details:{ - plug:'coco', - name:'coco', - reason:'object', - matrices : predictions - // confidence:d.average - }, - imgHeight:d.mon.detector_scale_y, - imgWidth:d.mon.detector_scale_x - }) - } - }) -} -s.systemLog=function(q,w,e){ - if(!w){w=''} - if(!e){e=''} - if(config.systemLog===true){ - return console.log(moment().format(),q,w,e) - } -} - -s.blenderRegion=function(d,cord,tx){ - d.width = d.image.width; - d.height = d.image.height; - if(!s.group[d.ke][d.id].canvas[cord.name]){ - if(!cord.sensitivity||isNaN(cord.sensitivity)){ - cord.sensitivity=d.mon.detector_sensitivity; - } - s.group[d.ke][d.id].canvas[cord.name] = new Canvas(d.width,d.height); - s.group[d.ke][d.id].canvasContext[cord.name] = s.group[d.ke][d.id].canvas[cord.name].getContext('2d'); - s.group[d.ke][d.id].canvasContext[cord.name].fillStyle = '#000'; - s.group[d.ke][d.id].canvasContext[cord.name].fillRect( 0, 0,d.width,d.height); - if(cord.points&&cord.points.length>0){ - s.group[d.ke][d.id].canvasContext[cord.name].beginPath(); - for (var b = 0; b < cord.points.length; b++){ - cord.points[b][0]=parseFloat(cord.points[b][0]); - cord.points[b][1]=parseFloat(cord.points[b][1]); - if(b===0){ - s.group[d.ke][d.id].canvasContext[cord.name].moveTo(cord.points[b][0],cord.points[b][1]); - }else{ - s.group[d.ke][d.id].canvasContext[cord.name].lineTo(cord.points[b][0],cord.points[b][1]); - } - } - s.group[d.ke][d.id].canvasContext[cord.name].clip(); - } - } - if(!s.group[d.ke][d.id].canvasContext[cord.name]){ - return - } - s.group[d.ke][d.id].canvasContext[cord.name].drawImage(d.image, 0, 0, d.width, d.height); - if(!s.group[d.ke][d.id].blendRegion[cord.name]){ - s.group[d.ke][d.id].blendRegion[cord.name] = new Canvas(d.width, d.height); - s.group[d.ke][d.id].blendRegionContext[cord.name] = s.group[d.ke][d.id].blendRegion[cord.name].getContext('2d'); - } - var sourceData = s.group[d.ke][d.id].canvasContext[cord.name].getImageData(0, 0, d.width, d.height); - // create an image if the previous image doesn�t exist - if (!s.group[d.ke][d.id].lastRegionImageData[cord.name]) s.group[d.ke][d.id].lastRegionImageData[cord.name] = s.group[d.ke][d.id].canvasContext[cord.name].getImageData(0, 0, d.width, d.height); - // create a ImageData instance to receive the blended result - var blendedData = s.group[d.ke][d.id].canvasContext[cord.name].createImageData(d.width, d.height); - // blend the 2 images - s.differenceAccuracy(blendedData.data,sourceData.data,s.group[d.ke][d.id].lastRegionImageData[cord.name].data); - // draw the result in a canvas - s.group[d.ke][d.id].blendRegionContext[cord.name].putImageData(blendedData, 0, 0); - // store the current webcam image - s.group[d.ke][d.id].lastRegionImageData[cord.name] = sourceData; - blendedData = s.group[d.ke][d.id].blendRegionContext[cord.name].getImageData(0, 0, d.width, d.height); - var i = 0; - d.average = 0; - while (i < (blendedData.data.length * 0.25)) { - d.average += (blendedData.data[i * 4] + blendedData.data[i * 4 + 1] + blendedData.data[i * 4 + 2]); - ++i; - } - d.average = (d.average / (blendedData.data.length * 0.25))*10; - if (d.average > parseFloat(cord.sensitivity)){ - if(d.mon.detector_use_detect_object==="1"&&d.mon.detector_second!=='1'){ - var buffer=s.group[d.ke][d.id].canvas[cord.name].toBuffer(); - s.detectObject(buffer,d,tx) - }else{ - tx({f:'trigger',id:d.id,ke:d.ke,details:{split:true,plug:config.plug,name:cord.name,reason:'motion',confidence:d.average,frame:d.base64}}) - } - } - s.group[d.ke][d.id].canvasContext[cord.name].clearRect(0, 0, d.width, d.height); - s.group[d.ke][d.id].blendRegionContext[cord.name].clearRect(0, 0, d.width, d.height); -} -function blobToBuffer (blob, cb) { - if (typeof Blob === 'undefined' || !(blob instanceof Blob)) { - throw new Error('first argument must be a Blob') - } - if (typeof cb !== 'function') { - throw new Error('second argument must be a function') - } - - var reader = new FileReader() - - function onLoadEnd (e) { - reader.removeEventListener('loadend', onLoadEnd, false) - if (e.error) cb(e.error) - else cb(null, Buffer.from(reader.result)) - } - - reader.addEventListener('loadend', onLoadEnd, false) - reader.readAsArrayBuffer(blob) -} -function fastAbs(value) { - return (value ^ (value >> 31)) - (value >> 31); -} - -function threshold(value) { - return (value > 0x15) ? 0xFF : 0; -} -s.differenceAccuracy=function(target, data1, data2) { - if (data1.length != data2.length) return null; - var i = 0; - while (i < (data1.length * 0.25)) { - var average1 = (data1[4 * i] + data1[4 * i + 1] + data1[4 * i + 2]) / 3; - var average2 = (data2[4 * i] + data2[4 * i + 1] + data2[4 * i + 2]) / 3; - var diff = threshold(fastAbs(average1 - average2)); - target[4 * i] = diff; - target[4 * i + 1] = diff; - target[4 * i + 2] = diff; - target[4 * i + 3] = 0xFF; - ++i; - } -} -s.checkAreas=function(d,tx){ - if(!s.group[d.ke][d.id].cords){ - if(!d.mon.cords){d.mon.cords={}} - s.group[d.ke][d.id].cords=Object.values(d.mon.cords); - } - if(d.mon.detector_frame==='1'){ - d.mon.cords.frame={name:'FULL_FRAME',s:d.mon.detector_sensitivity,points:[[0,0],[0,d.image.height],[d.image.width,d.image.height],[d.image.width,0]]}; - s.group[d.ke][d.id].cords.push(d.mon.cords.frame); - } - for (var b = 0; b < s.group[d.ke][d.id].cords.length; b++){ - if(!s.group[d.ke][d.id].cords[b]){return} - s.blenderRegion(d,s.group[d.ke][d.id].cords[b],tx) - } - delete(d.image) -} - -s.MainEventController=function(d,cn,tx){ - switch(d.f){ - case'refreshPlugins': - s.findCascades(function(cascades){ - s.cx({f:'s.tx',data:{f:'detector_cascade_list',cascades:cascades},to:'GRP_'+d.ke}) - }) - break; - case'readPlugins': - s.cx({f:'s.tx',data:{f:'detector_cascade_list',cascades:s.cascadesInDir},to:'GRP_'+d.ke}) - break; - case'init_plugin_as_host': - if(!cn){ - console.log('No CN',d) - return - } - if(d.key!==config.key){ - console.log(new Date(),'Plugin Key Mismatch',cn.request.connection.remoteAddress,d) - cn.emit('init',{ok:false}) - cn.disconnect() - }else{ - console.log(new Date(),'Plugin Connected to Client',cn.request.connection.remoteAddress) - cn.emit('init',{ok:true,plug:config.plug,notice:config.notice,type:config.type}) - } - break; - case'init_monitor': - if(s.group[d.ke]&&s.group[d.ke][d.id]){ - s.group[d.ke][d.id].canvas={} - s.group[d.ke][d.id].canvasContext={} - s.group[d.ke][d.id].blendRegion={} - s.group[d.ke][d.id].blendRegionContext={} - s.group[d.ke][d.id].lastRegionImageData={} - s.group[d.ke][d.id].numberOfTriggers=0 - delete(s.group[d.ke][d.id].cords) - delete(s.group[d.ke][d.id].buffer) - } - break; - case'init_aws_push': -// console.log('init_aws') - s.group[d.ke][d.id].aws={links:[],complete:0,total:d.total,videos:[],tx:tx} - break; - case'frame': - try{ - if(!s.group[d.ke]){ - s.group[d.ke]={} - } - if(!s.group[d.ke][d.id]){ - s.group[d.ke][d.id]={ - canvas:{}, - canvasContext:{}, - lastRegionImageData:{}, - blendRegion:{}, - blendRegionContext:{}, - } - } - if(!s.group[d.ke][d.id].buffer){ - s.group[d.ke][d.id].buffer=[d.frame]; - }else{ - s.group[d.ke][d.id].buffer.push(d.frame) - } - if(d.frame[d.frame.length-2] === 0xFF && d.frame[d.frame.length-1] === 0xD9){ - s.group[d.ke][d.id].buffer=Buffer.concat(s.group[d.ke][d.id].buffer); - try{ - d.mon.detector_cascades=JSON.parse(d.mon.detector_cascades) - }catch(err){ - - } - if(d.mon.detector_frame_save==="1"){ - d.base64=s.group[d.ke][d.id].buffer.toString('base64') - } - if(d.mon.detector_second==='1'&&d.objectOnly===true){ - s.detectObject(s.group[d.ke][d.id].buffer,d,tx) - }else{ - if((d.mon.detector_pam !== '1' && d.mon.detector_use_motion === "1") || d.mon.detector_use_detect_object !== "1"){ - if((typeof d.mon.cords ==='string')&&d.mon.cords.trim()===''){ - d.mon.cords=[] - }else{ - try{ - d.mon.cords=JSON.parse(d.mon.cords) - }catch(err){ - // console.log('d.mon.cords',err,d) - } - } - s.group[d.ke][d.id].cords=Object.values(d.mon.cords); - d.mon.cords=d.mon.cords; - d.image = new Canvas.Image; - if(d.mon.detector_scale_x===''||d.mon.detector_scale_y===''){ - s.systemLog('Must set detector image size') - return - }else{ - d.image.width=d.mon.detector_scale_x; - d.image.height=d.mon.detector_scale_y; - } - d.width=d.image.width; - d.height=d.image.height; - d.image.onload = function() { - s.checkAreas(d,tx); - } - d.image.src = s.group[d.ke][d.id].buffer; - }else{ - s.detectObject(s.group[d.ke][d.id].buffer,d,tx) - } - } - s.group[d.ke][d.id].buffer=null; - } - }catch(err){ - if(err){ - s.systemLog(err) - delete(s.group[d.ke][d.id].buffer) - } - } - break; - } -} -server.listen(config.hostPort); -//web pages and plugin api -app.get('/', function (req, res) { - res.end(''+config.plug+' for Shinobi is running') -}); -//Conector to Shinobi -if(config.mode==='host'){ - //start plugin as host - var io = require('socket.io')(server); - io.attach(server); - s.connectedClients={}; - io.on('connection', function (cn) { - s.connectedClients[cn.id]={id:cn.id} - s.connectedClients[cn.id].tx = function(data){ - data.pluginKey=config.key;data.plug=config.plug; - return io.to(cn.id).emit('ocv',data); - } - cn.on('f',function(d){ - s.MainEventController(d,cn,s.connectedClients[cn.id].tx) - }); - cn.on('disconnect',function(d){ - delete(s.connectedClients[cn.id]) - }) - }); -}else{ - //start plugin as client - if(!config.host){config.host='localhost'} - var io = require('socket.io-client')('ws://'+config.host+':'+config.port);//connect to master - s.cx=function(x){x.pluginKey=config.key;x.plug=config.plug;return io.emit('ocv',x)} - io.on('connect',function(d){ - s.cx({f:'init',plug:config.plug,notice:config.notice,type:config.type}); - }) - io.on('disconnect',function(d){ - io.connect(); - }) - io.on('f',function(d){ - s.MainEventController(d,null,s.cx) - }) -} diff --git a/plugins/face/.env b/plugins/face/.env new file mode 100644 index 00000000..09483c0a --- /dev/null +++ b/plugins/face/.env @@ -0,0 +1,2 @@ +TF_FORCE_GPU_ALLOW_GROWTH=true +#CUDA_VISIBLE_DEVICES=0,2 diff --git a/plugins/face/INSTALL.sh b/plugins/face/INSTALL.sh index 208c395d..e0591018 100644 --- a/plugins/face/INSTALL.sh +++ b/plugins/face/INSTALL.sh @@ -1,33 +1,42 @@ #!/bin/bash -THE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -sudo apt update -y +DIR=`dirname $0` +if [ -x "$(command -v apt)" ]; then + sudo apt update -y +fi +# Check if Cent OS +if [ -x "$(command -v yum)" ]; then + sudo yum update -y +fi +INSTALL_WITH_GPU="0" +INSTALL_FOR_ARM64="0" +INSTALL_FOR_ARM="0" +TFJS_SUFFIX="" echo "----------------------------------------" echo "-- Installing Face Plugin for Shinobi --" echo "----------------------------------------" -if ! [ -x "$(command -v nvidia-smi)" ]; then - echo "You need to install NVIDIA Drivers to use this." - echo "inside the Shinobi directory run the following :" - echo "sh INSTALL/cuda.sh" - exit 1 -else - echo "NVIDIA Drivers found..." - echo "$(nvidia-smi |grep 'Driver Version')" +echo "Are you Installing on an ARM CPU?" +echo "like Jetson Nano or Raspberry Pi Model 3 B+. Default is No." +echo "(y)es or (N)o" +read useArm +if [ "$useArm" = "y" ] || [ "$useArm" = "Y" ] || [ "$useArm" = "YES" ] || [ "$useArm" = "yes" ] || [ "$useArm" = "Yes" ]; then + INSTALL_FOR_ARM="1" + echo "Are you Installing on an ARM64 CPU?" + echo "like Jetson Nano. Default is No (64/32-bit)" + echo "(y)es or (N)o" + read useArm64 + if [ "$useArm64" = "y" ] || [ "$useArm64" = "Y" ] || [ "$useArm64" = "YES" ] || [ "$useArm64" = "yes" ] || [ "$useArm64" = "Yes" ]; then + INSTALL_FOR_ARM64="1" + fi fi -echo "-----------------------------------" -if [ ! -d "/usr/local/cuda-9.0" ]; then - echo "Tensorflow requires CUDA Toolkit 9.0" - echo "Installing CUDA Toolki 9.0..." - wget https://developer.nvidia.com/compute/cuda/9.0/Prod/local_installers/cuda-repo-ubuntu1704-9-0-local_9.0.176-1_amd64-deb -O cuda.deb - sudo dpkg -i cuda.deb - sudo apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1704/x86_64/7fa2af80.pub - sudo apt update -y - sudo apt install cuda-toolkit-9-0 -y - wget https://cdn.shinobi.video/installers/libcudnn7_7.5.1.10-1+cuda9.0_amd64.deb -O cuda-dnn.deb - sudo dpkg -i cuda-dnn.deb - wget https://cdn.shinobi.video/installers/libcudnn7-dev_7.5.1.10-1+cuda9.0_amd64.deb -O cuda-dnn-dev.deb - sudo dpkg -i cuda-dnn-dev.deb -else - echo "CUDA Toolkit 9.0 found..." +if [ -d "/usr/local/cuda" ]; then + echo "Do you want to install the plugin with CUDA support?" + echo "Do this if you installed NVIDIA Drivers, CUDA Toolkit, and CuDNN" + echo "(y)es or (N)o" + read usecuda + if [ "$usecuda" = "y" ] || [ "$usecuda" = "Y" ] || [ "$usecuda" = "YES" ] || [ "$usecuda" = "yes" ] || [ "$usecuda" = "Yes" ]; then + INSTALL_WITH_GPU="1" + TFJS_SUFFIX="-gpu" + fi fi echo "-----------------------------------" if [ ! -d "./faces" ]; then @@ -35,7 +44,16 @@ if [ ! -d "./faces" ]; then fi if [ ! -d "./weights" ]; then mkdir weights - sudo apt install wget -y + if [ ! -x "$(command -v wget)" ]; then + # Check if Ubuntu + if [ -x "$(command -v apt)" ]; then + sudo apt install wget -y + fi + # Check if Cent OS + if [ -x "$(command -v yum)" ]; then + sudo yum install wget -y + fi + fi cdnUrl="https://cdn.shinobi.video/weights/plugin-face-weights" wget -O weights/face_landmark_68_model-shard1 $cdnUrl/face_landmark_68_model-shard1 wget -O weights/face_landmark_68_model-weights_manifest.json $cdnUrl/face_landmark_68_model-weights_manifest.json @@ -61,19 +79,70 @@ if [ ! -e "./conf.json" ]; then else echo "conf.json already exists..." fi -sudo npm i npm -g +if [ ! -e "$DIR/../../libs/customAutoLoad/faceManagerCustomAutoLoadLibrary" ]; then + echo "Installing Face Manager customAutoLoad Module..." + sudo cp -r $DIR/faceManagerCustomAutoLoadLibrary $DIR/../../libs/customAutoLoad/faceManagerCustomAutoLoadLibrary +else + echo "Face Manager customAutoLoad Module already installed..." +fi +tfjsBuildVal="cpu" +if [ "$INSTALL_WITH_GPU" = "1" ]; then + tfjsBuildVal="gpu" +fi + +echo "-----------------------------------" +echo "Adding Random Plugin Key to Main Configuration" +node $DIR/../../tools/modifyConfigurationForPlugin.js face key=$(head -c 64 < /dev/urandom | sha256sum | awk '{print substr($1,1,60)}') tfjsBuild=$tfjsBuildVal +echo "-----------------------------------" +echo "Updating Node Package Manager" +sudo npm install npm -g --unsafe-perm echo "-----------------------------------" echo "Getting node-gyp to build C++ modules" -sudo npm install node-gyp -g --unsafe-perm +if [ ! -x "$(command -v node-gyp)" ]; then + # Check if Ubuntu + if [ -x "$(command -v apt)" ]; then + sudo apt install node-gyp -y + sudo apt-get install gcc g++ build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev -y + fi + # Check if Cent OS + if [ -x "$(command -v yum)" ]; then + sudo yum install node-gyp -y + sudo yum install gcc-c++ cairo-devel libjpeg-turbo-devel pango-devel giflib-devel -y + fi +fi +sudo npm install node-gyp -g --unsafe-perm --force echo "-----------------------------------" +npm uninstall @tensorflow/tfjs-node-gpu --unsafe-perm +npm uninstall @tensorflow/tfjs-node --unsafe-perm +echo "Getting C++ module : @tensorflow/tfjs-node@0.1.21" +echo "https://github.com/tensorflow/tfjs-node" +npm install @tensorflow/tfjs-core@1.7.3 --unsafe-perm --force +npm install @tensorflow/tfjs-converter@1.7.3 --unsafe-perm --force +npm install @tensorflow/tfjs-layers@1.7.3 --unsafe-perm --force echo "Getting C++ module : face-api.js" echo "https://github.com/justadudewhohacks/face-api.js" -sudo npm install --unsafe-perm -echo "Getting C++ module : @tensorflow/tfjs-node-gpu@0.1.21" -echo "https://github.com/tensorflow/tfjs-node" -sudo npm install @tensorflow/tfjs-node-gpu@0.1.21 --unsafe-perm +sudo npm install --unsafe-perm --force +if [ "$INSTALL_WITH_GPU" = "1" ]; then + echo "GPU version of tjfs : https://github.com/tensorflow/tfjs-node-gpu" +else + echo "CPU version of tjfs : https://github.com/tensorflow/tfjs-node" +fi +sudo npm install @tensorflow/tfjs-node$TFJS_SUFFIX@1.7.3 --unsafe-perm --force +if [ "$INSTALL_FOR_ARM" = "1" ]; then + cd node_modules/@tensorflow/tfjs-node$TFJS_SUFFIX + if [ "$INSTALL_FOR_ARM64" = "1" ]; then + echo "{ + \"tf-lib\": \"https://cdn.shinobi.video/binaries/libtensorflow-gpu-linux-arm64-1.15.0.tar.gz\" +}" > scripts/custom-binary.json + else + echo "{ + \"tf-lib\": \"https://cdn.shinobi.video/binaries/libtensorflow-cpu-linux-arm-1.15.0.tar.gz\" +}" > scripts/custom-binary.json + fi + npm install --unsafe-perm + cd ../../.. +fi sudo npm audit fix --force -cd $THE_DIR echo "-----------------------------------" echo "Start the plugin with pm2 like so :" echo "pm2 start shinobi-face.js" diff --git a/plugins/face/README.md b/plugins/face/README.md index 7585cdd0..c3d70076 100644 --- a/plugins/face/README.md +++ b/plugins/face/README.md @@ -24,6 +24,26 @@ pm2 start shinobi-face.js Doing this will reveal options in the monitor configuration. Shinobi does not need to be restarted when a plugin is initiated or stopped. +###Train Your Own Faces (Facial Recognition) + +> Currently this plugin loads faces upon plugin start + +1. Within the plugin's folder you will find a folder labelled `faces`. It will be empty. + ``` + ls faces + ``` +2. Create a folder with your name and put a JPEG image of your face inside it. The JPEG images must be photos of only you. The file structure may look like this. + ``` + faces/[HUMAN NAME]/[FILENAME] + faces/moe/1.jpg + faces/moe/2.jpg + faces/moe/3.jpg + ``` +3. Restart the plugin. + ``` + pm2 restart shinobi-face + ``` + ## Run the plugin as a Host > The main app (Shinobi) will be the client and the plugin will be the host. The purpose of allowing this method is so that you can use one plugin for multiple Shinobi instances. Allowing you to easily manage connections without starting multiple processes. @@ -71,21 +91,3 @@ Add the `plugins` array if you don't already have it. Add the following *object } ], ``` - -###Train Your Own Faces (Facial Recognition) - -1. Within the plugin's folder you will find a folder labelled `faces`. It will be empty. - ``` - ls faces - ``` -2. Create a folder with your name and put a JPEG image of your face inside it. The JPEG images must be photos of only you. The file structure may look like this. - ``` - faces/[HUMAN NAME]/[FILENAME] - faces/moe/1.jpg - faces/moe/2.jpg - faces/moe/3.jpg - ``` -3. Restart the plugin. - ``` - pm2 restart shinobi-face - ``` diff --git a/plugins/face/conf.sample.json b/plugins/face/conf.sample.json index dbe923d5..f19c3e5a 100644 --- a/plugins/face/conf.sample.json +++ b/plugins/face/conf.sample.json @@ -1,5 +1,6 @@ { "plug":"Face", + "tfjsBuild":"cpu", "host":"localhost", "port":8080, "key":"Face123123", diff --git a/plugins/face/faceManagerCustomAutoLoadLibrary/index.js b/plugins/face/faceManagerCustomAutoLoadLibrary/index.js new file mode 100644 index 00000000..bbe12e28 --- /dev/null +++ b/plugins/face/faceManagerCustomAutoLoadLibrary/index.js @@ -0,0 +1,242 @@ +var fs = require('fs') +var fileUpload = require('express-fileupload') +module.exports = function(s,config,lang,app,io){ + if(!config.facesFolder)config.facesFolder = s.mainDirectory + '/plugins/face/faces/' + config.facesFolder = s.checkCorrectPathEnding(config.facesFolder) + if(!fs.existsSync(config.facesFolder)){ + fs.mkdirSync(config.facesFolder) + } + const sendDataToConnectedSuperUsers = (data) => { + return s.tx(data,'$') + } + const getFaceFolderNames = (callback) => { + fs.readdir(config.facesFolder,(err,folders) => { + var faces = [] + folders.forEach((folder)=>{ + var stats = fs.statSync(config.facesFolder + folder) + if(stats.isDirectory()){ + faces.push(folder) + } + }) + callback(faces) + }) + } + const getFaceImages = (callback) => { + fs.readdir(config.facesFolder,(err,folders)=>{ + var faces = {} + folders.forEach((name)=>{ + var stats = fs.statSync(config.facesFolder + name) + if(stats.isDirectory()){ + faces[name] = [] + var images + try{ + images = fs.readdirSync(config.facesFolder + name) + }catch(err){ + images = [] + } + images.forEach((image)=>{ + faces[name].push(image) + }) + } + }) + callback(faces) + }) + } + const getFaceImagesByName = (name,callback) => { + var stats = fs.statSync(config.facesFolder + name) + if(stats.isDirectory()){ + var images + try{ + images = fs.readdirSync(config.facesFolder + name) + }catch(err){ + images = [] + } + callback(images) + }else{ + callback([]) + } + } + const deletePath = (deletionPath,callback) => { + if(fs.existsSync(deletionPath)){ + fs.unlink(deletionPath,() => { + s.file('delete',deletionPath) + if(callback)callback() + }) + }else{ + if(callback)callback(true) + } + } + app.get(config.webPaths.superApiPrefix+':auth/faceManager/names', function (req,res){ + s.superAuth(req.params,function(resp){ + getFaceFolderNames((faces)=>{ + res.setHeader('Content-Type', 'application/json') + res.end(s.prettyPrint({ + ok: true, + faces: faces + })) + }) + },res,req) + }) + app.get(config.webPaths.superApiPrefix+':auth/faceManager/images', function (req,res){ + s.superAuth(req.params,function(resp){ + getFaceImages((faces)=>{ + res.setHeader('Content-Type', 'application/json') + res.end(s.prettyPrint({ + ok: true, + faces: faces + })) + }) + },res,req) + }) + app.get(config.webPaths.superApiPrefix+':auth/faceManager/image/:name/:image', function (req,res){ + s.superAuth(req.params,function(resp){ + const imagePath = config.facesFolder + req.params.name + '/' + req.params.image + if(fs.existsSync(imagePath)){ + res.setHeader('Content-Type', 'image/jpeg') + fs.createReadStream(imagePath).pipe(res) + }else{ + res.setHeader('Content-Type', 'application/json') + res.end(s.prettyPrint({ + ok: false, + msg: lang['File Not Found'] + })) + } + },res,req) + }) + app.get(config.webPaths.superApiPrefix+':auth/faceManager/image/:name/:image/delete', function (req,res){ + s.superAuth(req.params,function(resp){ + res.setHeader('Content-Type', 'application/json') + const imagePath = config.facesFolder + req.params.name + '/' + req.params.image + deletePath(imagePath,() => { + sendDataToConnectedSuperUsers({ + f:'faceManagerImageDeleted', + faceName: req.params.name, + fileName: req.params.image, + }) + getFaceFolderNames((faces) => { + s.sendToAllDetectors({ + f: 'recompileFaceDescriptors', + faces: faces + }) + }) + }) + res.end(s.prettyPrint({ + ok: true, + })) + },res,req) + }) + app.get(config.webPaths.superApiPrefix+':auth/faceManager/delete/:name', function (req,res){ + s.superAuth(req.params,function(resp){ + res.setHeader('Content-Type', 'application/json') + const facePath = config.facesFolder + req.params.name + deletePath(facePath,() => { + getFaceFolderNames((faces) => { + s.sendToAllDetectors({ + f: 'recompileFaceDescriptors', + faces: faces + }) + }) + }) + sendDataToConnectedSuperUsers({ + f:'faceManagerFolderDeleted', + faceName: req.params.name, + }) + res.end(s.prettyPrint({ + ok: true, + })) + },res,req) + }) + app.get(config.webPaths.superApiPrefix+':auth/faceManager/image/:name/:image/move/:newName/:newImage', function (req,res){ + s.superAuth(req.params,function(resp){ + res.setHeader('Content-Type', 'application/json') + const oldImagePath = config.facesFolder + req.params.name + '/' + req.params.image + const newImagePath = config.facesFolder + req.params.newName + '/' + req.params.newImage + const fileExists = fs.existsSync(oldImagePath) + if(fileExists){ + fs.readFile(oldImagePath,(err,data) => { + fs.writeFile(newImagePath,data,() => { + fs.unlink(oldImagePath,() => { + s.file('delete',oldImagePath) + if(req.query.websocketResponse){ + sendDataToConnectedSuperUsers({ + f:'faceManagerImageDeleted', + faceName: req.params.name, + fileName: req.params.image, + }) + var fileLink = config.webPaths.superApiPrefix + req.params.auth + `/faceManager/image/${req.params.newName}/${req.params.newImage}` + sendDataToConnectedSuperUsers({ + f:'faceManagerImageUploaded', + faceName: req.params.newName, + fileName: req.params.newImage, + url: fileLink + }) + } + getFaceFolderNames((faces) => { + s.sendToAllDetectors({ + f: 'recompileFaceDescriptors', + faces: faces + }) + }) + }) + }) + }) + } + res.end(s.prettyPrint({ + ok: fileExists, + })) + },res,req) + }) + app.post(config.webPaths.superApiPrefix+':auth/faceManager/image/:name', fileUpload(), function (req,res){ + s.superAuth(req.params,function(resp){ + res.setHeader('Content-Type', 'application/json') + var fileKeys = Object.keys(req.files || {}) + if(fileKeys.length == 0){ + return res.status(400).send('No files were uploaded.') + } + var filesUploaded = [] + var checkFile = (file) => { + if(file.name.indexOf('.jpg') > -1 || file.name.indexOf('.jpeg') > -1){ + filesUploaded.push(file.name) + if(!fs.existsSync(config.facesFolder + req.params.name)){ + fs.mkdirSync(config.facesFolder + req.params.name) + } + file.mv(config.facesFolder + req.params.name + '/' + file.name, function(err) { + var fileLink = config.webPaths.superApiPrefix + req.params.auth + `/faceManager/image/${req.params.name}/${file.name}` + sendDataToConnectedSuperUsers({ + f:'faceManagerImageUploaded', + faceName: req.params.name, + fileName: file.name, + url: fileLink + }) + }) + } + } + fileKeys.forEach(function(key){ + var file = req.files[key] + try{ + if(file instanceof Array){ + file.forEach(function(fileOfFile){ + checkFile(fileOfFile) + }) + }else{ + checkFile(file) + } + }catch(err){ + console.log(file) + console.log(err) + } + }) + var response = { + ok: true, + filesUploaded: filesUploaded + } + res.send(s.prettyPrint(response)) + getFaceFolderNames((faces) => { + s.sendToAllDetectors({ + f: 'recompileFaceDescriptors', + faces: faces + }) + }) + },res,req) + }) +} diff --git a/plugins/face/faceManagerCustomAutoLoadLibrary/languages/en_CA.json b/plugins/face/faceManagerCustomAutoLoadLibrary/languages/en_CA.json new file mode 100644 index 00000000..41282ae3 --- /dev/null +++ b/plugins/face/faceManagerCustomAutoLoadLibrary/languages/en_CA.json @@ -0,0 +1,11 @@ +{ + "Upload Images": "Upload Images", + "Images Sent": "Images Sent", + "Click to Upload Images": "Click to Upload Images", + "Face Name": "Face Name", + "faceManager": "Face Manager", + "deleteFace": "Delete Face", + "deleteFaceText": "Are you sure you want to delete ALL the images for this face? they will not be recoverable.", + "deleteImage": "Delete Image", + "deleteImageText": "Are you sure you want to delete this image? it will not be recoverable." +} diff --git a/plugins/face/faceManagerCustomAutoLoadLibrary/web/libs/css/super.faceManager.css b/plugins/face/faceManagerCustomAutoLoadLibrary/web/libs/css/super.faceManager.css new file mode 100644 index 00000000..61cca93c --- /dev/null +++ b/plugins/face/faceManagerCustomAutoLoadLibrary/web/libs/css/super.faceManager.css @@ -0,0 +1,89 @@ +#faceManagerImages .face-image { + transition: 0s; +} +#faceManagerImages .face-title:first-child { + margin-top: 0!important; +} +#faceManagerImages > .row { + background: #fafafa; + border-radius: 5px; + padding: 5px; + margin: 0; +} +#faceManagerImages > .row.ui-droppable-hover { + background: #eee; +} +#faceManagerImages > .row:empty { + height: 200px; +} +#faceManagerImages .face-image-bg { + position: relative; + background-size: cover; + background-position: center; + border-radius: 5px; + width: 100%; + height: 100%; + border: 1px solid #eee; +} +#faceManagerImages .face-image .controls, +#faceManagerImages .face-image .controls-bottom { + position: absolute; + width: 100%; + top: 0; + left: 0; + padding: 5px; + line-height: 1; +} +#faceManagerImages .face-image .controls-bottom { + top: initial; + bottom: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +#faceManagerImages .face-image:hover .controls .badge, +#faceManagerImages .face-image:hover .controls-bottom .badge{ + opacity: 0.1 +} +/* ===================== FILE INPUT ===================== */ +.file-area { + width: 100%; + position: relative; +} + .file-area input[type=file] { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + right: 0; + bottom: 0; + opacity: 0; + cursor: pointer; +} + .file-area .file-dummy { + width: 100%; + padding: 30px; + background: rgba(255, 255, 255, 0.2); + border: 2px dashed rgba(200, 200, 200, 0.2); + text-align: center; + transition: background 0.3s ease-in-out; + border-radius: 5px; + background-color: #f2f9ff; +} + .file-area .file-dummy .success { + display: none; +} + .file-area input[type=file]:focus + .file-dummy { + outline: 0; + } + .file-area input[type=file]:valid + .file-dummy { + border-color: rgba(0, 255, 0, 0.4); + background-color: rgba(0, 255, 0, 0.2); +} + .file-area input[type=file]:valid + .file-dummy .success { + display: inline-block; +} + .file-area input[type=file]:valid + .file-dummy .default { + display: none; +} diff --git a/plugins/face/faceManagerCustomAutoLoadLibrary/web/libs/js/super.faceManager.js b/plugins/face/faceManagerCustomAutoLoadLibrary/web/libs/js/super.faceManager.js new file mode 100644 index 00000000..a25c064f --- /dev/null +++ b/plugins/face/faceManagerCustomAutoLoadLibrary/web/libs/js/super.faceManager.js @@ -0,0 +1,202 @@ +$(document).ready(function(){ + var faceManagerModal = $('#faceManager') + var faceManagerImages = $('#faceManagerImages') + var faceManagerForm = $('#faceManagerUploadForm') + var faceNameField = $('#faceNameField') + var getFaceImages = function(callback){ + $.get(superApiPrefix + $user.sessionKey + '/faceManager/images',function(response){ + callback(response.faces || []) + }) + } + var deleteFaceImage = function(name,image,callback){ + $.get(superApiPrefix + $user.sessionKey + '/faceManager/image/' + name + '/' + image + '/delete',function(response){ + callback(response) + }) + } + var deleteFaceFolder = function(name,callback){ + $.get(superApiPrefix + $user.sessionKey + '/faceManager/delete/' + name,function(response){ + callback(response) + }) + } + var moveFaceImage = function(name,image,newFaceName,callback){ + $.get(superApiPrefix + $user.sessionKey + '/faceManager/image/' + name + '/' + image + '/move/' + newFaceName + '/' + image + '?websocketResponse=1' ,function(response){ + callback(response) + }) + } + var getFaceImageHtml = function(name,image){ + return `
+
+
+
+ +
+
+ ${name} +
+
+
+ ${image} +
+
+
` + } + var createFaceHeader = function(name){ + return `
${name}
` + } + var drawFaceImages = function(){ + getFaceImages(function(faces){ + var html = '' + $.each(faces,function(name,images){ + // if(images.length === 0)return + html += `${createFaceHeader(name)}
` + $.each(images,function(n,image){ + html += getFaceImageHtml(name,image) + }) + html += `
` + }) + faceManagerImages.html(html) + $.each(faces,function(name,images){ + activateDroppableContainer(name) + }) + activateDraggableImages() + prettySizeFaceImages() + }) + } + var prettySizeFaceImages = function(){ + var faceImagesRendered = faceManagerImages.find('.face-image') + var faceHeight = faceImagesRendered.first().width() + faceImagesRendered.css('height',faceHeight) + } + var activateDroppableContainer = function(name){ + var row = faceManagerImages.find(`.row[face="${name}"]`) + try{ + row.droppable("destroy") + }catch(err){ + + } + row.droppable({ + tolerance: "intersect", + accept: ".face-image", + activeClass: "ui-state-default", + hoverClass: "ui-state-hover", + drop: function(event, ui) { + var el = $(this) + var newFace = el.attr('face') + var faceImageElement = $(ui.draggable) + var oldFace = faceImageElement.attr('face') + var fileName = faceImageElement.attr('image') + if(oldFace !== newFace){ + moveFaceImage(oldFace,fileName,newFace) + }else{ + faceImageElement.css({ + top: 0, + left: 0, + }) + } + } + }) + } + var activateDraggableImages = function(name){ + var imageEls = faceManagerImages.find(`.face-image`) + try{ + imageEls.draggable("destroy") + }catch(err){ + + } + imageEls.draggable({ + appendTo: "body", + cursor: "move", + // helper: 'clone', + revert: "invalid" + }); + } + var createFaceImageBlock = function(row,faceName,fileName){ + var existingBlock = row.find(`[face="${faceName}"][image="${fileName}"]`) + if(existingBlock.length > 0){ + existingBlock.draggable('destroy') + existingBlock.remove() + } + row.prepend(getFaceImageHtml(faceName,fileName)) + } + faceManagerModal.on('shown.bs.modal',function(){ + drawFaceImages() + }) + $(window).resize(function(){ + prettySizeFaceImages() + }) + faceManagerImages.on('click','.delete',function(e){ + e.preventDefault() + var el = $(this).parents('.face-image') + var faceName = el.attr('face') + var faceImage = el.attr('image') + $.confirm.create({ + title: lang.deleteImage, + body: lang.deleteImageText + `
`, + clickOptions: { + class: 'btn-danger', + title: lang.Delete, + }, + clickCallback: function(){ + deleteFaceImage(faceName,faceImage,function(response){ + console.log(response) + }) + } + }) + return false; + }) + faceManagerImages.on('click','.deleteFolder',function(e){ + e.preventDefault() + var faceName = $(this).attr('face') + $.confirm.create({ + title: lang.deleteFace, + body: lang.deleteFaceText, + clickOptions: { + class: 'btn-danger', + title: lang.Delete, + }, + clickCallback: function(){ + deleteFaceFolder(faceName,function(response){ + console.log(response) + }) + } + }) + return false; + }) + $('#fileinput').change(function(){ + var name = faceNameField.val() + $.ajax({ + url: superApiPrefix + $user.sessionKey + '/faceManager/image/' + name, + type: 'POST', + data: new FormData(faceManagerForm[0]), + cache: false, + contentType: false, + processData: false, + },function(data){ + console.log(data) + }) + }) + $('#tablist').append('') + $.ccio.ws.on('f',function(d){ + switch(d.f){ + case'faceManagerImageUploaded': + var row = faceManagerImages.find(`.row[face="${d.faceName}"]`) + if(row.length === 0){ + faceManagerImages.append(`${createFaceHeader(d.faceName)}
`) + row = faceManagerImages.find(`.row[face="${d.faceName}"]`) + activateDroppableContainer(d.faceName) + } + createFaceImageBlock(row,d.faceName,d.fileName) + activateDraggableImages() + prettySizeFaceImages() + break; + case'faceManagerImageDeleted': + $(`.face-image[face="${d.faceName}"][image="${d.fileName}"]`).remove() + break; + case'faceManagerFolderDeleted': + $(`[face="${d.faceName}"]`).remove() + break; + } + }) +}) diff --git a/plugins/face/faceManagerCustomAutoLoadLibrary/web/pages/blocks/super.faceManager.ejs b/plugins/face/faceManagerCustomAutoLoadLibrary/web/pages/blocks/super.faceManager.ejs new file mode 100644 index 00000000..3e7f6a8f --- /dev/null +++ b/plugins/face/faceManagerCustomAutoLoadLibrary/web/pages/blocks/super.faceManager.ejs @@ -0,0 +1,33 @@ + diff --git a/plugins/face/package.json b/plugins/face/package.json index e1958118..448cfd6b 100644 --- a/plugins/face/package.json +++ b/plugins/face/package.json @@ -8,9 +8,9 @@ "express": "^4.16.2", "moment": "^2.19.2", "socket.io": "^2.0.4", - "face-api.js": "^0.16.1", + "face-api.js": "^0.22.2", "canvas": "^2.1.0", - "@tensorflow/tfjs-node-gpu": "^0.1.21" + "dotenv": "^8.2.0" }, "devDependencies": {}, "scripts": { diff --git a/plugins/face/shinobi-face.js b/plugins/face/shinobi-face.js index 2d5e9147..5f6731ee 100644 --- a/plugins/face/shinobi-face.js +++ b/plugins/face/shinobi-face.js @@ -10,6 +10,7 @@ // Base Init >> var fs = require('fs'); var config = require('./conf.json') +var dotenv = require('dotenv').config() var s try{ s = require('../pluginBase.js')(__dirname,config) @@ -25,10 +26,24 @@ try{ // Base Init />> // Face - Face Recognition Init >> var weightLocation = __dirname + '/weights' -const tf = require('@tensorflow/tfjs') -canvas = require('canvas') +const canvas = require('canvas') +var tfjsSuffix = '' +switch(config.tfjsBuild){ + case'gpu': + tfjsSuffix = '-gpu' + break; + case'cpu': + break; + default: + try{ + require(`@tensorflow/tfjs-node`) + }catch(err){ + console.log(err) + } + break; +} +var tf = require(`@tensorflow/tfjs-node${tfjsSuffix}`) faceapi = require('face-api.js') -require('@tensorflow/tfjs-node-gpu') const { createCanvas, Image, ImageData, Canvas } = canvas faceapi.env.monkeyPatch({ Canvas, Image, ImageData }) @@ -36,49 +51,79 @@ s.monitorLock = {} // Face - Face Recognition Init />> // SsdMobilenetv1Options const minConfidence = 0.5 - -// TinyFaceDetectorOptions -const inputSize = 384 -const scoreThreshold = 0.5 - -// MtcnnOptions -const minFaceSize = 50 -const scaleFactor = 0.8 - -function getFaceDetectorOptions(net) { - return net === faceapi.nets.ssdMobilenetv1 - ? new faceapi.SsdMobilenetv1Options({ minConfidence }) - : (net === faceapi.nets.tinyFaceDetector - ? new faceapi.TinyFaceDetectorOptions({ inputSize, scoreThreshold }) - : new faceapi.MtcnnOptions({ minFaceSize, scaleFactor }) - ) -} var addAwaitStatements = async function(){ await faceapi.nets.ssdMobilenetv1.loadFromDisk(weightLocation) - // faceapi.nets.tinyFaceDetector.loadFromDisk(weightLocation) await faceapi.nets.faceLandmark68Net.loadFromDisk(weightLocation) await faceapi.nets.faceRecognitionNet.loadFromDisk(weightLocation) const faceDetectionNet = faceapi.nets.ssdMobilenetv1 - // const faceDetectionNet = faceapi.nets.tinyFaceDetector - // const faceDetectionNet = faceapi.nets.mtcnn - var faceDetectionOptions = getFaceDetectorOptions(faceDetectionNet) + var faceDetectionOptions = new faceapi.SsdMobilenetv1Options({ minConfidence }); if(!fs.existsSync('./faces')){ fs.mkdirSync('./faces'); } var faces = fs.readdirSync('./faces') - const labeledDescriptors = [ - // new faceapi.LabeledFaceDescriptors( - // 'obama', - // [descriptorObama1, descriptorObama2] - // ), - // new faceapi.LabeledFaceDescriptors( - // 'trump', - // [descriptorTrump] - // ) - ] + var labeledDescriptors = [] var faceMatcher var facesLoaded = 0 + const createAllFaceDescriptors = (faces) => { + s.detectObject = function(){} + faceMatcher = null + facesLoaded = 0 + labeledDescriptors = [] + const checkComplete = () => { + ++facesLoaded + if(facesLoaded === faces.length){ + faceMatcher = new faceapi.FaceMatcher(labeledDescriptors) + startDetecting() + } + } + faces.forEach(function(personName){ + var descriptors = [] + var faceFolder = './faces/' + personName + '/' + var imageList = fs.readdirSync(faceFolder) + var foundImages = [] + var faceResults = [] + imageList.forEach(function(imageFile,number){ + if(imageFile.indexOf('.jpg') > -1 || imageFile.indexOf('.jpeg') > -1){ + foundImages.push(imageFile) + } + }) + if(foundImages.length === 0){ + checkComplete(facesLoaded,faces.length) + }else{ + console.log('Loading : ' + personName) + foundImages.forEach(function(imageFile,number){ + var image = new Image; + image.onload = function() { + faceapi + .detectSingleFace(image) + .withFaceLandmarks() + .withFaceDescriptor() + .then((singleResult) => { + if (!singleResult) { + return console.log('no faces',imageFile) + } + descriptors.push(singleResult.descriptor) + faceResults.push(singleResult) + if(number === foundImages.length - 1){ + console.log('Loaded : ' + personName) + labeledDescriptors.push(new faceapi.LabeledFaceDescriptors( + personName, + descriptors + )) + checkComplete() + } + }) + .catch((error) => { + console.log(error) + }) + } + image.src = fs.readFileSync(faceFolder + imageFile) + }) + } + }) + } var startDetecting = function(){ + console.log('Ready to Detect Faces') s.detectObject = function(buffer,d,tx,frameLocation){ var detectStuff = function(frameBuffer,callback){ try{ @@ -93,15 +138,20 @@ var addAwaitStatements = async function(){ if(faceMatcher){ data.forEach(fd => { var bestMatch = faceMatcher.findBestMatch(fd.descriptor) - fd._detection.tag = bestMatch.toString() + fd.detection.tag = bestMatch.toString() }) } var endTime = new Date() var matrices = [] - var imgHeight = data[0]._detection._imageDims._height - var imgWidth = data[0]._detection._imageDims._width + try{ + var imgHeight = data[0].detection._imageDims._height + var imgWidth = data[0].detection._imageDims._width + }catch(err){ + var imgHeight = data[0]._detection._imageDims._height + var imgWidth = data[0]._detection._imageDims._width + } data.forEach(function(box){ - var v = box._detection + var v = box.detection || box._detection var tag,confidence if(v.tag){ var split = v.tag.split('(') @@ -117,12 +167,13 @@ var addAwaitStatements = async function(){ confidence = v._score } matrices.push({ - x:v._box.x, - y:v._box.y, - width:v._box.width, - height:v._box.height, - tag:tag, - confidence:v._score, + id: tag, + x: v._box.x, + y: v._box.y, + width: v._box.width, + height: v._box.height, + tag: tag, + confidence: v._score, }) }) if(matrices.length > 0){ @@ -167,60 +218,33 @@ var addAwaitStatements = async function(){ } } } - var checkComplete = function(){ - ++facesLoaded - if(facesLoaded === faces.length){ - faceMatcher = new faceapi.FaceMatcher(labeledDescriptors) - startDetecting() - } - } if(faces.length === 0){ startDetecting() }else{ - faces.forEach(function(personName){ - var descriptors = [] - var faceFolder = './faces/' + personName + '/' - var imageList = fs.readdirSync(faceFolder) - var foundImages = [] - var faceResults = [] - imageList.forEach(function(imageFile,number){ - if(imageFile.indexOf('.jpg') > -1 || imageFile.indexOf('.jpeg') > -1){ - foundImages.push(imageFile) - } - }) - if(foundImages.length === 0){ - checkComplete(facesLoaded,faces.length) - }else{ - foundImages.forEach(function(imageFile,number){ - var image = new Image; - image.onload = function() { - faceapi - .detectSingleFace(image) - .withFaceLandmarks() - .withFaceDescriptor() - .then((singleResult) => { - if (!singleResult) { - return console.log('no faces',imageFile) - } - descriptors.push(singleResult.descriptor) - faceResults.push(singleResult) - if(number === foundImages.length - 1){ - console.log('Loaded : ' + personName) - labeledDescriptors.push(new faceapi.LabeledFaceDescriptors( - personName, - descriptors - )) - checkComplete() - } - }) - .catch((error) => { - console.log(error) - }) - } - image.src = fs.readFileSync(faceFolder + imageFile) - }) - } + createAllFaceDescriptors(faces) + } + // add websocket handlers + const io = s.getWebsocket() + var faceDescriptorRefreshTimeout + const onSocketEvent = (d) => { + switch(d.f){ + case'recompileFaceDescriptors': + if(faceDescriptorRefreshTimeout)console.log('Cancelling previous recompilation request...') + console.log('Recompiling Face Descriptors...') + clearTimeout(faceDescriptorRefreshTimeout) + faceDescriptorRefreshTimeout = setTimeout(()=>{ + delete(faceDescriptorRefreshTimeout) + createAllFaceDescriptors(d.faces) + },10000) + break; + } + } + if(config.mode === 'host'){ + io.on('connection', function (cn) { + cn.on('f',onSocketEvent) }) + }else{ + io.on('f',onSocketEvent) } } addAwaitStatements() diff --git a/plugins/motion/.gitignore b/plugins/motion/.gitignore deleted file mode 100644 index 85825a52..00000000 --- a/plugins/motion/.gitignore +++ /dev/null @@ -1 +0,0 @@ -conf.json diff --git a/plugins/motion/INSTALL.sh b/plugins/motion/INSTALL.sh deleted file mode 100644 index 902d6ab2..00000000 --- a/plugins/motion/INSTALL.sh +++ /dev/null @@ -1,5 +0,0 @@ -apt-get install libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev build-essential g++ -npm install canvas -cd plugins/motion -cp conf.sample.json conf.json -pm2 start shinobi-motion.js \ No newline at end of file diff --git a/plugins/motion/README.md b/plugins/motion/README.md deleted file mode 100644 index ca3c6f42..00000000 --- a/plugins/motion/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# Shinobi Motion Detector - -Install required libraries. - -**Ubuntu and Debian only** - -``` -sudo apt-get install libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev build-essential g++ -``` - -**CentOS only** - -``` -su -c 'yum install cairo cairo-devel cairomm-devel libjpeg-turbo-devel pango pango-devel pangomm pangomm-devel giflib-devel' -yum search arial -yum install liberation-sans-fonts.noarch -``` - -**Install the Node.js Canvas engine** - -``` -sudo npm install canvas -``` - -Go to the Shinobi directory. **Below is an example.** - -``` -cd /home/Shinobi -``` - -Copy the config file. - -``` -cp plugins/motion/conf.sample.json plugins/motion/conf.json -``` - -Edit it the new file. Host should be `localhost` and port should match the `listening port for camera.js`. - -``` -nano plugins/motion/conf.json -``` - -Start the plugin. - -``` -node plugins/motion/shinobi-motion.js -``` - -Or to daemonize with PM2. - -``` -pm2 start plugins/motion/shinobi-motion.js -``` - -Doing this will reveal options in the monitor configuration. Shinobi does not need to be restarted when a plugin is initiated or stopped. - diff --git a/plugins/motion/conf.sample.json b/plugins/motion/conf.sample.json deleted file mode 100644 index 171a562f..00000000 --- a/plugins/motion/conf.sample.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "plug":"Motion", - "host":"localhost", - "port":8080, - "key":"change_this_to_something_very_random____make_sure_to_match__/plugins/motion/conf.json", - "notice":"Looks like you have the Motion plugin running. Don't forget to enable Send Frames to start pushing frames to be read." -} \ No newline at end of file diff --git a/plugins/motion/libs/clusterPoints.js b/plugins/motion/libs/clusterPoints.js deleted file mode 100644 index 9d05fe8d..00000000 --- a/plugins/motion/libs/clusterPoints.js +++ /dev/null @@ -1,115 +0,0 @@ -'use strict' - -module.exports = { - - data: getterSetter([], function(arrayOfArrays) { - var n = arrayOfArrays[0].length; - return (arrayOfArrays.map(function(array) { - return array.length == n; - }).reduce(function(boolA, boolB) { return (boolA & boolB) }, true)); - }), - - clusters: function() { - var pointsAndCentroids = kmeans(this.data(), {k: this.k(), iterations: this.iterations() }); - var points = pointsAndCentroids.points; - var centroids = pointsAndCentroids.centroids; - - return centroids.map(function(centroid) { - return { - centroid: centroid.location(), - points: points.filter(function(point) { return point.label() == centroid.label() }).map(function(point) { return point.location() }), - }; - }); - }, - - k: getterSetter(undefined, function(value) { return ((value % 1 == 0) & (value > 0)) }), - - iterations: getterSetter(Math.pow(10, 3), function(value) { return ((value % 1 == 0) & (value > 0)) }), - -}; - -function kmeans(data, config) { - // default k - var k = config.k || Math.round(Math.sqrt(data.length / 2)); - var iterations = config.iterations; - - // initialize point objects with data - var points = data.map(function(vector) { return new Point(vector) }); - - // intialize centroids randomly - var centroids = []; - for (var i = 0; i < k; i++) { - centroids.push(new Centroid(points[i % points.length].location(), i)); - }; - - // update labels and centroid locations until convergence - for (var iter = 0; iter < iterations; iter++) { - points.forEach(function(point) { point.updateLabel(centroids) }); - centroids.forEach(function(centroid) { centroid.updateLocation(points) }); - }; - - // return points and centroids - return { - points: points, - centroids: centroids - }; - -}; - -// objects -function Point(location) { - var self = this; - this.location = getterSetter(location); - this.label = getterSetter(); - this.updateLabel = function(centroids) { - var distancesSquared = centroids.map(function(centroid) { - return sumOfSquareDiffs(self.location(), centroid.location()); - }); - self.label(mindex(distancesSquared)); - }; -}; - -function Centroid(initialLocation, label) { - var self = this; - this.location = getterSetter(initialLocation); - this.label = getterSetter(label); - this.updateLocation = function(points) { - var pointsWithThisCentroid = points.filter(function(point) { return point.label() == self.label() }); - if (pointsWithThisCentroid.length > 0) self.location(averageLocation(pointsWithThisCentroid)); - }; -}; - -// convenience functions -function getterSetter(initialValue, validator) { - var thingToGetSet = initialValue; - var isValid = validator || function(val) { return true }; - return function(newValue) { - if (typeof newValue === 'undefined') return thingToGetSet; - if (isValid(newValue)) thingToGetSet = newValue; - }; -}; - -function sumOfSquareDiffs(oneVector, anotherVector) { - var squareDiffs = oneVector.map(function(component, i) { - return Math.pow(component - anotherVector[i], 2); - }); - return squareDiffs.reduce(function(a, b) { return a + b }, 0); -}; - -function mindex(array) { - var min = array.reduce(function(a, b) { - return Math.min(a, b); - }); - return array.indexOf(min); -}; - -function sumVectors(a, b) { - return a.map(function(val, i) { return val + b[i] }); -}; - -function averageLocation(points) { - var zeroVector = points[0].location().map(function() { return 0 }); - var locations = points.map(function(point) { return point.location() }); - var vectorSum = locations.reduce(function(a, b) { return sumVectors(a, b) }, zeroVector); - return vectorSum.map(function(val) { return val / points.length }); -}; diff --git a/plugins/motion/shinobi-motion-pixel.js b/plugins/motion/shinobi-motion-pixel.js deleted file mode 100644 index 6dfba785..00000000 --- a/plugins/motion/shinobi-motion-pixel.js +++ /dev/null @@ -1,245 +0,0 @@ -// -// Shinobi - Motion Plugin -// Copyright (C) 2016-2025 Moe Alam, moeiscool -// -// # Donate -// -// If you like what I am doing here and want me to continue please consider donating :) -// PayPal : paypal@m03.ca -// -process.on('uncaughtException', function (err) { - console.error('uncaughtException',err); -}); -var fs = require('fs'); -var moment = require('moment'); -var Canvas = require('canvas'); -var Cluster = require('./libs/clusterPoints.js'); -var config=require('./conf.json'); -if(process.argv[2]&&process.argv[3]){ - config.host=process.argv[2] - config.port=process.argv[3] - config.key=process.argv[4] -} -if(config.systemLog===undefined){config.systemLog=true} -s={ - group:{}, -} -s.systemLog=function(q,w,e){ - if(!w){w=''} - if(!e){e=''} - if(config.systemLog===true){ - return console.log(moment().format(),q,w,e) - } -} -s.checkRegion=function(d,cord){ - d.width = d.image.width; - d.height = d.image.height; - if(!s.group[d.ke][d.id].canvas[cord.name]){ - if(!cord.sensitivity||isNaN(cord.sensitivity)){ - cord.sensitivity=d.mon.detector_sensitivity; - } - s.group[d.ke][d.id].canvas[cord.name] = new Canvas(d.width,d.height); - s.group[d.ke][d.id].canvasContext[cord.name] = s.group[d.ke][d.id].canvas[cord.name].getContext('2d'); - s.group[d.ke][d.id].canvasContext[cord.name].fillStyle = '#005337'; - s.group[d.ke][d.id].canvasContext[cord.name].fillRect( 0, 0,d.width,d.height); - if(cord.points&&cord.points.length>0){ - s.group[d.ke][d.id].canvasContext[cord.name].beginPath(); - for (var b = 0; b < cord.points.length; b++){ - cord.points[b][0]=parseFloat(cord.points[b][0]); - cord.points[b][1]=parseFloat(cord.points[b][1]); - if(b===0){ - s.group[d.ke][d.id].canvasContext[cord.name].moveTo(cord.points[b][0],cord.points[b][1]); - }else{ - s.group[d.ke][d.id].canvasContext[cord.name].lineTo(cord.points[b][0],cord.points[b][1]); - } - } - s.group[d.ke][d.id].canvasContext[cord.name].clip(); - } - } - s.group[d.ke][d.id].canvasContext[cord.name].drawImage(d.image, 0, 0, d.width, d.height); - var blenderCanvas = s.group[d.ke][d.id].canvas[cord.name]; - var blenderCanvasContext = s.group[d.ke][d.id].canvasContext[cord.name]; - s.group[d.ke][d.id].frameSelected[s.group[d.ke][d.id].frameNumber] = blenderCanvasContext.getImageData(0, 0, blenderCanvas.width, blenderCanvas.height); - s.group[d.ke][d.id].frameNumber = 0 == s.group[d.ke][d.id].frameNumber ? 1 : 0; - s.group[d.ke][d.id].lastRegionImageData = blenderCanvasContext.getImageData(0, 0, blenderCanvas.width, blenderCanvas.height); - if(!s.group[d.ke][d.id].lastRegionImageData){return} - var foundPixels = []; - var average = 0; - var currentImageLength = s.group[d.ke][d.id].lastRegionImageData.data.length * 0.25; - for (b = 0; b < currentImageLength;){ - var pos = b * 4 - s.group[d.ke][d.id].lastRegionImageData.data[pos] = .5 * (255 - s.group[d.ke][d.id].lastRegionImageData.data[pos]) + .5 * s.group[d.ke][d.id].frameSelected[s.group[d.ke][d.id].frameNumber].data[pos]; - s.group[d.ke][d.id].lastRegionImageData.data[pos + 1] = .5 * (255 - s.group[d.ke][d.id].lastRegionImageData.data[pos + 1]) + .5 * s.group[d.ke][d.id].frameSelected[s.group[d.ke][d.id].frameNumber].data[pos + 1]; - s.group[d.ke][d.id].lastRegionImageData.data[pos + 2] = .5 * (255 - s.group[d.ke][d.id].lastRegionImageData.data[pos + 2]) + .5 * s.group[d.ke][d.id].frameSelected[s.group[d.ke][d.id].frameNumber].data[pos + 2]; - s.group[d.ke][d.id].lastRegionImageData.data[pos + 3] = 255; - var score = (s.group[d.ke][d.id].lastRegionImageData.data[pos] + s.group[d.ke][d.id].lastRegionImageData.data[pos + 1] + s.group[d.ke][d.id].lastRegionImageData.data[pos + 2]) / 3; - if(score>170){ - var x = (pos / 4) % d.width; - var y = Math.floor((pos / 4) / d.width); - foundPixels.push([x,y]) - } - - average += (s.group[d.ke][d.id].lastRegionImageData.data[b * 4] + s.group[d.ke][d.id].lastRegionImageData.data[b * 4 + 1] + s.group[d.ke][d.id].lastRegionImageData.data[b * 4 + 2]); - - b += 4; - } -// console.log(foundPixels) - var matrices - if(d.mon.detector_region_of_interest==='1'&&foundPixels.length>0){ - var groupedPoints = Object.assign({},Cluster); - groupedPoints.iterations(25); - groupedPoints.data(foundPixels); - var groupedPoints = groupedPoints.clusters() - var matrices=[] - var mostHeight = 0; - var mostWidth = 0; - var mostWithMotion = null; - groupedPoints.forEach(function(v,n){ - var matrix = { - topLeft:[d.width,d.height], - topRight:[0,d.height], - bottomRight:[0,0], - bottomLeft:[d.width,0], - } - v.points.forEach(function(b){ - var x = b[0] - var y = b[1] - if(xmatrix.topRight[0])matrix.topRight[0]=x; - if(ymatrix.bottomRight[0])matrix.bottomRight[0]=x; - if(y>matrix.bottomRight[1])matrix.bottomRight[1]=y; - //Bottom Left point - if(xmatrix.bottomLeft[1])matrix.bottomLeft[1]=y; - }) - matrix.x = matrix.topLeft[0]; - matrix.y = matrix.topLeft[1]; - matrix.width = matrix.topRight[0] - matrix.topLeft[0] - matrix.height = matrix.bottomLeft[1] - matrix.topLeft[1] - - if(matrix.width>mostWidth&&matrix.height>mostHeight){ - mostWidth = matrix.width; - mostHeight = matrix.height; - mostWithMotion = matrix; - } - - matrices.push(matrix) - }) - } - average = (average / (currentImageLength)); - if (average > parseFloat(cord.sensitivity)){ - s.cx({f:'trigger',id:d.id,ke:d.ke,details:{plug:config.plug,name:cord.name,reason:'motion',confidence:average,matrices:matrices}}) - } - s.group[d.ke][d.id].canvasContext[cord.name].clearRect(0, 0, d.width, d.height); -} -s.checkAreas=function(d){ - if(!s.group[d.ke][d.id].cords){ - if(!d.mon.cords){d.mon.cords={}} - s.group[d.ke][d.id].cords=Object.values(d.mon.cords); - } - if(d.mon.detector_frame==='1'){ - d.mon.cords.frame={name:'frame',s:d.mon.detector_sensitivity,points:[[0,0],[0,d.image.height],[d.image.width,d.image.height],[d.image.width,0]]}; - s.group[d.ke][d.id].cords.push(d.mon.cords.frame); - } - for (var b = 0; b < s.group[d.ke][d.id].cords.length; b++){ - if(!s.group[d.ke][d.id].cords[b]){return} - s.checkRegion(d,s.group[d.ke][d.id].cords[b]) - } - delete(d.image) -} - -io = require('socket.io-client')('ws://'+config.host+':'+config.port);//connect to master -s.cx=function(x){x.pluginKey=config.key;x.plug=config.plug;return io.emit('ocv',x)} -io.on('connect',function(d){ - s.cx({f:'init',plug:config.plug,notice:config.notice}); -}) -io.on('disconnect',function(d){ - io.connect(); -}) -io.on('f',function(d){ - switch(d.f){ - case'init_monitor': - if(s.group[d.ke]&&s.group[d.ke][d.id]){ - s.group[d.ke][d.id].canvas={} - s.group[d.ke][d.id].canvasContext={} - s.group[d.ke][d.id].lastRegionImageData=undefined - s.group[d.ke][d.id].frameNumber=0 - s.group[d.ke][d.id].frameSelected=[] - delete(s.group[d.ke][d.id].cords) - delete(s.group[d.ke][d.id].buffer) - } - break; - case'frame': - try{ - if(!s.group[d.ke]){ - s.group[d.ke]={} - } - if(!s.group[d.ke][d.id]){ - s.group[d.ke][d.id]={ - canvas:{}, - canvasContext:{}, - lastRegionImageData:undefined, - frameNumber:0, - frameSelected:[], - } - } - if(!s.group[d.ke][d.id].buffer){ - s.group[d.ke][d.id].buffer=[d.frame]; - }else{ - s.group[d.ke][d.id].buffer.push(d.frame) - } - if(d.frame[d.frame.length-2] === 0xFF && d.frame[d.frame.length-1] === 0xD9){ - if(s.group[d.ke][d.id].motion_lock){ - return - }else{ - if(!d.mon.detector_lock_timeout||d.mon.detector_lock_timeout===''||d.mon.detector_lock_timeout==0){ - d.mon.detector_lock_timeout=2000 - }else{ - d.mon.detector_lock_timeout=parseFloat(d.mon.detector_lock_timeout) - } - s.group[d.ke][d.id].motion_lock=setTimeout(function(){ - clearTimeout(s.group[d.ke][d.id].motion_lock); - delete(s.group[d.ke][d.id].motion_lock); - },d.mon.detector_lock_timeout) - } - s.group[d.ke][d.id].buffer=Buffer.concat(s.group[d.ke][d.id].buffer); - if((typeof d.mon.cords ==='string')&&d.mon.cords.trim()===''){ - d.mon.cords=[] - }else{ - try{ - d.mon.cords=JSON.parse(d.mon.cords) - }catch(err){ - } - } - if(d.mon.detector_frame_save==="1"){ - d.base64=s.group[d.ke][d.id].buffer.toString('base64') - } - s.group[d.ke][d.id].cords=Object.values(d.mon.cords); - d.mon.cords=d.mon.cords; - d.image = new Canvas.Image; - if(d.mon.detector_scale_x===''||d.mon.detector_scale_y===''){ - s.systemLog('Must set detector image size') - return - }else{ - d.image.width=d.mon.detector_scale_x; - d.image.height=d.mon.detector_scale_y; - } - d.image.onload = function() { - s.checkAreas(d); - } - d.image.src = s.group[d.ke][d.id].buffer; - s.group[d.ke][d.id].buffer=null; - } - }catch(err){ - if(err){ - s.systemLog(err) - delete(s.group[d.ke][d.id].buffer) - } - } - break; - } -}) \ No newline at end of file diff --git a/plugins/motion/shinobi-motion.js b/plugins/motion/shinobi-motion.js deleted file mode 100644 index 3b003cf1..00000000 --- a/plugins/motion/shinobi-motion.js +++ /dev/null @@ -1,233 +0,0 @@ -// -// Shinobi - Motion Plugin -// Copyright (C) 2016-2025 Moe Alam, moeiscool -// -// # Donate -// -// If you like what I am doing here and want me to continue please consider donating :) -// PayPal : paypal@m03.ca -// -process.on('uncaughtException', function (err) { - console.error('uncaughtException',err); -}); -var fs = require('fs'); -var moment = require('moment'); -var Canvas = require('canvas'); -var config=require('./conf.json'); -if(process.argv[2]&&process.argv[3]){ - config.host=process.argv[2] - config.port=process.argv[3] - config.key=process.argv[4] -} -if(config.systemLog===undefined){config.systemLog=true} -s={ - group:{}, -} -s.systemLog=function(q,w,e){ - if(!w){w=''} - if(!e){e=''} - if(config.systemLog===true){ - return console.log(moment().format(),q,w,e) - } -} -s.blenderRegion=function(d,cord){ - d.width = d.image.width; - d.height = d.image.height; - if(!s.group[d.ke][d.id].canvas[cord.name]){ - if(!cord.sensitivity||isNaN(cord.sensitivity)){ - cord.sensitivity=d.mon.detector_sensitivity; - } - s.group[d.ke][d.id].canvas[cord.name] = new Canvas(d.width,d.height); - s.group[d.ke][d.id].canvasContext[cord.name] = s.group[d.ke][d.id].canvas[cord.name].getContext('2d'); - s.group[d.ke][d.id].canvasContext[cord.name].fillStyle = '#005337'; - s.group[d.ke][d.id].canvasContext[cord.name].fillRect( 0, 0,d.width,d.height); - if(cord.points&&cord.points.length>0){ - s.group[d.ke][d.id].canvasContext[cord.name].beginPath(); - for (var b = 0; b < cord.points.length; b++){ - cord.points[b][0]=parseFloat(cord.points[b][0]); - cord.points[b][1]=parseFloat(cord.points[b][1]); - if(b===0){ - s.group[d.ke][d.id].canvasContext[cord.name].moveTo(cord.points[b][0],cord.points[b][1]); - }else{ - s.group[d.ke][d.id].canvasContext[cord.name].lineTo(cord.points[b][0],cord.points[b][1]); - } - } - s.group[d.ke][d.id].canvasContext[cord.name].clip(); - } - } - if(!s.group[d.ke][d.id].canvasContext[cord.name]){ - return - } - s.group[d.ke][d.id].canvasContext[cord.name].drawImage(d.image, 0, 0, d.width, d.height); - if(!s.group[d.ke][d.id].blendRegion[cord.name]){ - s.group[d.ke][d.id].blendRegion[cord.name] = new Canvas(d.width, d.height); - s.group[d.ke][d.id].blendRegionContext[cord.name] = s.group[d.ke][d.id].blendRegion[cord.name].getContext('2d'); - } - var sourceData = s.group[d.ke][d.id].canvasContext[cord.name].getImageData(0, 0, d.width, d.height); - // create an image if the previous image doesn�t exist - if (!s.group[d.ke][d.id].lastRegionImageData[cord.name]) s.group[d.ke][d.id].lastRegionImageData[cord.name] = s.group[d.ke][d.id].canvasContext[cord.name].getImageData(0, 0, d.width, d.height); - // create a ImageData instance to receive the blended result - var blendedData = s.group[d.ke][d.id].canvasContext[cord.name].createImageData(d.width, d.height); - // blend the 2 images - s.differenceAccuracy(blendedData.data,sourceData.data,s.group[d.ke][d.id].lastRegionImageData[cord.name].data); - // draw the result in a canvas - s.group[d.ke][d.id].blendRegionContext[cord.name].putImageData(blendedData, 0, 0); - // store the current webcam image - s.group[d.ke][d.id].lastRegionImageData[cord.name] = sourceData; - blendedData = s.group[d.ke][d.id].blendRegionContext[cord.name].getImageData(0, 0, d.width, d.height); - var i = 0; - var average = 0; - while (i < (blendedData.data.length * 0.25)) { - average += (blendedData.data[i * 4] + blendedData.data[i * 4 + 1] + blendedData.data[i * 4 + 2]); - ++i; - } - average = (average / (blendedData.data.length * 0.25))*10; - if (average > parseFloat(cord.sensitivity)){ - s.cx({f:'trigger',id:d.id,ke:d.ke,details:{plug:config.plug,name:cord.name,reason:'motion',confidence:average}}) - - } - s.group[d.ke][d.id].canvasContext[cord.name].clearRect(0, 0, d.width, d.height); - s.group[d.ke][d.id].blendRegionContext[cord.name].clearRect(0, 0, d.width, d.height); -} -function fastAbs(value) { - return (value ^ (value >> 31)) - (value >> 31); -} - -function threshold(value) { - return (value > 0x15) ? 0xFF : 0; -} - -function difference(target, data1, data2) { - // blend mode difference - if (data1.length != data2.length) return null; - var i = 0; - while (i < (data1.length * 0.25)) { - target[4 * i] = data1[4 * i] == 0 ? 0 : fastAbs(data1[4 * i] - data2[4 * i]); - target[4 * i + 1] = data1[4 * i + 1] == 0 ? 0 : fastAbs(data1[4 * i + 1] - data2[4 * i + 1]); - target[4 * i + 2] = data1[4 * i + 2] == 0 ? 0 : fastAbs(data1[4 * i + 2] - data2[4 * i + 2]); - target[4 * i + 3] = 0xFF; - ++i; - } -} -s.differenceAccuracy=function(target, data1, data2) { - if (data1.length != data2.length) return null; - var i = 0; - while (i < (data1.length * 0.25)) { - var average1 = (data1[4 * i] + data1[4 * i + 1] + data1[4 * i + 2]) / 3; - var average2 = (data2[4 * i] + data2[4 * i + 1] + data2[4 * i + 2]) / 3; - var diff = threshold(fastAbs(average1 - average2)); - target[4 * i] = diff; - target[4 * i + 1] = diff; - target[4 * i + 2] = diff; - target[4 * i + 3] = 0xFF; - ++i; - } -} - -s.checkAreas=function(d){ - if(!s.group[d.ke][d.id].cords){ - if(!d.mon.cords){d.mon.cords={}} - s.group[d.ke][d.id].cords=Object.values(d.mon.cords); - } - if(d.mon.detector_frame==='1'){ - d.mon.cords.frame={name:'frame',s:d.mon.detector_sensitivity,points:[[0,0],[0,d.image.height],[d.image.width,d.image.height],[d.image.width,0]]}; - s.group[d.ke][d.id].cords.push(d.mon.cords.frame); - } - for (var b = 0; b < s.group[d.ke][d.id].cords.length; b++){ - if(!s.group[d.ke][d.id].cords[b]){return} - s.blenderRegion(d,s.group[d.ke][d.id].cords[b]) - } - delete(d.image) -} - -io = require('socket.io-client')('ws://'+config.host+':'+config.port);//connect to master -s.cx=function(x){x.pluginKey=config.key;x.plug=config.plug;return io.emit('ocv',x)} -io.on('connect',function(d){ - s.cx({f:'init',plug:config.plug,notice:config.notice}); -}) -io.on('disconnect',function(d){ - io.connect(); -}) -io.on('f',function(d){ - switch(d.f){ - case'init_monitor': - if(s.group[d.ke]&&s.group[d.ke][d.id]){ - s.group[d.ke][d.id].canvas={} - s.group[d.ke][d.id].canvasContext={} - s.group[d.ke][d.id].blendRegion={} - s.group[d.ke][d.id].blendRegionContext={} - s.group[d.ke][d.id].lastRegionImageData={} - delete(s.group[d.ke][d.id].cords) - delete(s.group[d.ke][d.id].buffer) - } - break; - case'frame': - try{ - if(!s.group[d.ke]){ - s.group[d.ke]={} - } - if(!s.group[d.ke][d.id]){ - s.group[d.ke][d.id]={ - canvas:{}, - canvasContext:{}, - lastRegionImageData:{}, - blendRegion:{}, - blendRegionContext:{}, - } - } - if(!s.group[d.ke][d.id].buffer){ - s.group[d.ke][d.id].buffer=[d.frame]; - }else{ - s.group[d.ke][d.id].buffer.push(d.frame) - } - if(d.frame[d.frame.length-2] === 0xFF && d.frame[d.frame.length-1] === 0xD9){ - if(s.group[d.ke][d.id].motion_lock){ - return - }else{ - if(!d.mon.detector_lock_timeout||d.mon.detector_lock_timeout===''||d.mon.detector_lock_timeout==0){ - d.mon.detector_lock_timeout=2000 - }else{ - d.mon.detector_lock_timeout=parseFloat(d.mon.detector_lock_timeout) - } - s.group[d.ke][d.id].motion_lock=setTimeout(function(){ - clearTimeout(s.group[d.ke][d.id].motion_lock); - delete(s.group[d.ke][d.id].motion_lock); - },d.mon.detector_lock_timeout) - } - s.group[d.ke][d.id].buffer=Buffer.concat(s.group[d.ke][d.id].buffer); - if((typeof d.mon.cords ==='string')&&d.mon.cords.trim()===''){ - d.mon.cords=[] - }else{ - try{ - d.mon.cords=JSON.parse(d.mon.cords) - }catch(err){ - } - } - if(d.mon.detector_frame_save==="1"){ - d.base64=s.group[d.ke][d.id].buffer.toString('base64') - } - s.group[d.ke][d.id].cords=Object.values(d.mon.cords); - d.mon.cords=d.mon.cords; - d.image = new Canvas.Image; - if(d.mon.detector_scale_x===''||d.mon.detector_scale_y===''){ - s.systemLog('Must set detector image size') - return - }else{ - d.image.width=d.mon.detector_scale_x; - d.image.height=d.mon.detector_scale_y; - } - d.image.onload = function() { - s.checkAreas(d); - } - d.image.src = s.group[d.ke][d.id].buffer; - s.group[d.ke][d.id].buffer=null; - } - }catch(err){ - if(err){ - s.systemLog(err) - delete(s.group[d.ke][d.id].buffer) - } - } - break; - } -}) \ No newline at end of file diff --git a/plugins/openalpr/INSTALL.sh b/plugins/openalpr/INSTALL.sh index b2dd28d4..720bd8e8 100644 --- a/plugins/openalpr/INSTALL.sh +++ b/plugins/openalpr/INSTALL.sh @@ -32,6 +32,10 @@ else echo "conf.json already exists..." fi echo "-----------------------------------" +echo "Adding Random Plugin Key to Main Configuration" + +node $DIR/../../tools/modifyConfigurationForPlugin.js openalpr key=$(head -c 64 < /dev/urandom | sha256sum | awk '{print substr($1,1,60)}') +echo "-----------------------------------" echo "Installing Modules.." apt install node-pre-gyp -y npm install nopt npmlog rimraf semver -g diff --git a/plugins/openalpr/shinobi-openalpr.js b/plugins/openalpr/shinobi-openalpr.js index 169a912b..fcb34cc2 100644 --- a/plugins/openalpr/shinobi-openalpr.js +++ b/plugins/openalpr/shinobi-openalpr.js @@ -64,7 +64,7 @@ var convertResultsToMatrices = function(results){ return mats } // OpenALPR Init />> -s.detectObject = function(buffer,d,tx,frameLocation){ +s.detectObject = function(buffer,d,tx,frameLocation,callback){ try{ var region = d.mon.detector_lisence_plate_country || 'us' openalpr[region].IdentifyLicense(buffer, {}, function (error, output){ @@ -86,6 +86,7 @@ s.detectObject = function(buffer,d,tx,frameLocation){ } }) } + callback() }) }catch(err){ console.log(err) diff --git a/plugins/pluginBase.js b/plugins/pluginBase.js index ea502713..5d33ad8f 100644 --- a/plugins/pluginBase.js +++ b/plugins/pluginBase.js @@ -7,20 +7,25 @@ // If you like what I am doing here and want me to continue please consider donating :) // PayPal : paypal@m03.ca // -var fs=require('fs'); +var fs = require('fs'); var exec = require('child_process').exec; var spawn = require('child_process').spawn; var moment = require('moment'); var express = require('express'); var http = require('http'), app = express(); -module.exports = function(__dirname,config){ - var plugLog = function(d1){ - console.log(new Date(),config.plug,d1) +var overAllProcessingCount = 0 +module.exports = function(__dirname, config){ + if(!config){ + return console.log(`Configuration file is missing.`) } - process.on('uncaughtException', function (err) { - console.error('uncaughtException',err); - }); + var plugLog = (d1) => { + console.log(new Date(), config.plug, d1) + } + + process.on('uncaughtException', (err) => { + console.error('uncaughtException', err) + }) try{ if(!config.skipMainConfigCheck){ @@ -31,7 +36,7 @@ module.exports = function(__dirname,config){ foundKeyAdded = true } if(mainConfig.plugins){ - mainConfig.plugins.forEach(function(plug){ + mainConfig.plugins.forEach((plug) => { if(plug.id === config.plug){ foundKeyAdded = true } @@ -46,55 +51,60 @@ module.exports = function(__dirname,config){ } - if(!config.port){config.port=8080} - if(!config.hostPort){config.hostPort=8082} - if(config.systemLog===undefined){config.systemLog=true} + if(!config.dirname){config.dirname = '.'} + if(!config.port){config.port = 8080} + if(!config.hostPort){config.hostPort = 8082} + if(config.systemLog === undefined){config.systemLog = true} if(config.connectionType === undefined)config.connectionType = 'websocket' s = { - group:{}, - dir:{}, - isWin:(process.platform==='win32'), - s:function(json){return JSON.stringify(json,null,3)} + group: {}, + dir: {}, + isWin: (process.platform === 'win32'), + s: (json) => { + return JSON.stringify(json,null,3) + } } //default stream folder check if(!config.streamDir){ - if(s.isWin===false){ - config.streamDir='/dev/shm' + if(s.isWin === false){ + config.streamDir = '/dev/shm' }else{ - config.streamDir=config.windowsTempDir + config.streamDir = config.windowsTempDir } if(!fs.existsSync(config.streamDir)){ - config.streamDir=__dirname+'/streams/' + config.streamDir = config.dirname+'/streams/' }else{ - config.streamDir+='/streams/' + config.streamDir += '/streams/' } } - s.dir.streams=config.streamDir; + s.dir.streams = config.streamDir //streams dir if(!fs.existsSync(s.dir.streams)){ - fs.mkdirSync(s.dir.streams); + fs.mkdirSync(s.dir.streams) } - s.checkCorrectPathEnding=function(x){ - var length=x.length - if(x.charAt(length-1)!=='/'){ - x=x+'/' + s.checkCorrectPathEnding = (x) => { + var length = x.length + if(x.charAt(length-1) !== '/'){ + x = x+'/' } - return x.replace('__DIR__',__dirname) + return x.replace('__DIR__',config.dirname) } - s.gid = function(x){ - if(!x){x=10};var t = "";var p = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - for( var i=0; i < x; i++ ) + s.gid = (x) => { + if(!x){x = 10}; + var t = ""; + var p = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for( var i = 0; i < x; i++ ) t += p.charAt(Math.floor(Math.random() * p.length)); return t; }; - s.systemLog = function(q,w,e){ + s.systemLog = (q,w,e) => { if(!w){w=''} if(!e){e=''} if(config.systemLog===true){ return console.log(moment().format(),q,w,e) } } - s.debugLog = function(){ + s.debugLog = () => { if(config.debugLog === true){ console.log(new Date(),arguments) if(config.debugLogVerbose === true){ @@ -102,14 +112,96 @@ module.exports = function(__dirname,config){ } } } - s.detectObject=function(buffer,d,tx,frameLocation){ + s.detectObject = (buffer,d,tx,frameLocation) => { console.log('detectObject handler not set') } + const processImage = async (buffer,d,tx,frameLocation) => { + const theSocket = s.getWebsocket() + ++overAllProcessingCount + theSocket.emit('processCount',overAllProcessingCount) + s.detectObject(buffer,d,tx,frameLocation,() => { + --overAllProcessingCount + theSocket.emit('processCount',overAllProcessingCount) + }) + } + const getCpuUsage = (callback) => { + var k = {} + switch(s.platform){ + case'win32': + k.cmd = "@for /f \"skip=1\" %p in ('wmic cpu get loadpercentage') do @echo %p%" + break; + case'darwin': + k.cmd = "ps -A -o %cpu | awk '{s+=$1} END {print s}'"; + break; + case'linux': + k.cmd = 'top -b -n 2 | awk \'toupper($0) ~ /^.?CPU/ {gsub("id,","100",$8); gsub("%","",$8); print 100-$8}\' | tail -n 1'; + break; + case'freebsd': + k.cmd = 'vmstat 1 2 | awk \'END{print 100-$19}\'' + break; + case'openbsd': + k.cmd = 'vmstat 1 2 | awk \'END{print 100-$18}\'' + break; + } + if(config.customCpuCommand){ + exec(config.customCpuCommand,{encoding:'utf8',detached: true},function(err,d){ + if(s.isWin===true) { + d = d.replace(/(\r\n|\n|\r)/gm, "").replace(/%/g, "") + } + callback(d) + s.onGetCpuUsageExtensions.forEach(function(extender){ + extender(d) + }) + }) + } else if(k.cmd){ + exec(k.cmd,{encoding:'utf8',detached: true},function(err,d){ + if(s.isWin===true){ + d=d.replace(/(\r\n|\n|\r)/gm,"").replace(/%/g,"") + } + callback(d) + s.onGetCpuUsageExtensions.forEach(function(extender){ + extender(d) + }) + }) + } else { + callback(0) + } + } + const parseNvidiaSmi = function(callback){ + var response = { + ok: true, + } + exec('nvidia-smi -x -q',function(err,data){ + var response = xmlParser.toJson(data) + var newArray = [] + try{ + JSON.parse(response).nvidia_smi_log.gpu.forEach((gpu)=>{ + newArray.push({ + id: gpu.minor_number, + name: gpu.product_name, + brand: gpu.product_brand, + fan_speed: gpu.fan_speed, + temperature: gpu.temperature, + power: gpu.power_readings, + utilization: gpu.utilization, + maxClocks: gpu.max_clocks, + }) + }) + }catch(err){ + + } + if(callback)callback(newArray) + }) + } + s.onCameraInitExtensions = [] + s.onCameraInit = (extender) => { + s.onCameraInitExtensions.push(extender) + } s.onPluginEvent = [] - s.onPluginEventExtender = function(extender){ + s.onPluginEventExtender = (extender) => { s.onPluginEvent.push(extender) } - s.MainEventController = function(d,cn,tx){ + s.MainEventController = async (d,cn,tx) => { switch(d.f){ case'init_plugin_as_host': if(!cn){ @@ -127,20 +219,23 @@ module.exports = function(__dirname,config){ break; case'init_monitor': retryConnection = 0 - if(s.group[d.ke]&&s.group[d.ke][d.id]){ + if(s.group[d.ke] && s.group[d.ke][d.id]){ s.group[d.ke][d.id].numberOfTriggers = 0 delete(s.group[d.ke][d.id].cords) delete(s.group[d.ke][d.id].buffer) + s.onCameraInitExtensions.forEach((extender) => { + extender(d,cn,tx) + }) } break; case'frameFromRam': if(!s.group[d.ke]){ - s.group[d.ke]={} + s.group[d.ke] = {} } if(!s.group[d.ke][d.id]){ - s.group[d.ke][d.id]={} + s.group[d.ke][d.id] = {} } - s.detectObject(buffer,d,tx,d.frameLocation) + processImage(buffer,d,tx,d.frameLocation) break; case'frame': try{ @@ -148,17 +243,20 @@ module.exports = function(__dirname,config){ s.group[d.ke]={} } if(!s.group[d.ke][d.id]){ - s.group[d.ke][d.id]={} + s.group[d.ke][d.id] = {} + s.onCameraInitExtensions.forEach((extender) => { + extender(d,cn,tx) + }) } if(!s.group[d.ke][d.id].buffer){ - s.group[d.ke][d.id].buffer=[d.frame]; + s.group[d.ke][d.id].buffer = [d.frame]; }else{ s.group[d.ke][d.id].buffer.push(d.frame) } if(d.frame[d.frame.length-2] === 0xFF && d.frame[d.frame.length-1] === 0xD9){ var buffer = Buffer.concat(s.group[d.ke][d.id].buffer); - s.detectObject(buffer,d,tx) - s.group[d.ke][d.id].buffer=null; + processImage(buffer,d,tx) + s.group[d.ke][d.id].buffer = null } }catch(err){ if(err){ @@ -168,11 +266,11 @@ module.exports = function(__dirname,config){ } break; } - s.onPluginEvent.forEach(function(extender){ + s.onPluginEvent.forEach((extender) => { extender(d,cn,tx) }) } - server = http.createServer(app).on('error', function(err){ + server = http.createServer(app).on('error', (err) => { if(err.code === 'EADDRINUSE'){ //try next port if(webServerTryCount === 5){ @@ -181,16 +279,16 @@ module.exports = function(__dirname,config){ ++webServerTryCount var port = parseInt(config.hostPort) config.hostPort = parseInt(config.hostPort) + 1 - plugLog('Failed to Start Web Server on '+port+'. Trying next Port '+config.hostPort) + plugLog(`Failed to Start Web Server on ${port}. Trying next Port ${config.hostPort}`) startWebServer() }else{ console.log(err) } }) var webServerTryCount = 0 - var startWebServer = function(){ + var startWebServer = () => { var port = parseInt(config.hostPort) - server.listen(config.hostPort,function(err){ + server.listen(config.hostPort, (err) => { if(port === config.hostPort){ plugLog('Plugin started on Port ' + port) } @@ -199,7 +297,7 @@ module.exports = function(__dirname,config){ startWebServer() //web pages and plugin api var webPageMssage = ''+config.plug+' for Shinobi is running' - app.get('/', function (req, res) { + app.get('/', (req, res) => { res.end() }); //Conector to Shinobi @@ -214,119 +312,170 @@ module.exports = function(__dirname,config){ perMessageDeflate: false }) io.attach(server); - s.connectedClients={}; - io.on('connection', function (cn) { - s.connectedClients[cn.id]={id:cn.id} - s.connectedClients[cn.id].tx = function(data){ - data.pluginKey=config.key;data.plug=config.plug; - return io.to(cn.id).emit('ocv',data); + s.connectedClients = {}; + io.on('connection', (cn) => { + plugLog('Plugin Connected to a Shinobi..') + s.connectedClients[cn.id] = { + id: cn.id } - cn.on('f',function(d){ + s.connectedClients[cn.id].tx = (data) => { + data.pluginKey = config.key + data.plug = config.plug + return io.to(cn.id).emit('ocv',data) + } + cn.on('f',(d) => { s.MainEventController(d,cn,s.connectedClients[cn.id].tx) - }); - cn.on('disconnect',function(d){ + }) + cn.on('disconnect',(d) => { plugLog('Plugin Disconnected.',cn.id) delete(s.connectedClients[cn.id]) }) }); }else{ - var retryConnection = 0 - maxRetryConnection = config.maxRetryConnection || 5 - plugLog('Plugin starting as Client, Host Address : '+'ws://'+config.host+':'+config.port) //start plugin as client + var retryConnection = 0 + var clearRetryConnectionTimeout + maxRetryConnection = parseInt(config.maxRetryConnection) || 5 + plugLog('Plugin starting as Client, Host Address : '+'ws://'+config.host+':'+config.port) if(!config.host){config.host='localhost'} - var io = require('socket.io-client')('ws://'+config.host+':'+config.port,{ - transports: ['websocket'] - }); - //connect to master - s.cx = function(x){ - var sendData = Object.assign(x,{ - pluginKey : config.key, - plug : config.plug - }) - return io.emit('ocv',sendData) + const createConnection = () => { + var allowDisconnect = false; + var io = require('socket.io-client')('ws://'+config.host+':'+config.port,{ + transports: ['websocket'] + }); + const onDisconnect = (err) => { + clearTimeout(clearRetryConnectionTimeout) + if(io.connected){ + io.disconnect() + return + } + if(err && err.type){ + plugLog('Plugin Error. Attempting Reconnect..') + plugLog(err.type) + } + if(retryConnection > maxRetryConnection && maxRetryConnection !== 0){ + webPageMssage = 'Max Failed Retries Reached' + return plugLog('Max Failed Retries Reached!',maxRetryConnection) + } + ++retryConnection + plugLog('Plugin Disconnected..') + if(!allowDisconnect){ + setTimeout(() => { + if(!io.connected){ + plugLog('Attempting Reconnect..') + io.connect() + } + },3000) + }; + } + //connect to master + s.cx = (x) => { + var sendData = Object.assign(x,{ + pluginKey : config.key, + plug : config.plug + }) + return io.emit('ocv',sendData) + } + io.on('connect_error', onDisconnect) + io.on('connect', (d) => { + plugLog('Plugin Connected to Shinobi..') + s.cx({f:'init',plug:config.plug,notice:config.notice,type:config.type,connectionType:config.connectionType}); + clearRetryConnectionTimeout = setTimeout(() => { + retryConnection = 0 + },10000) + }) + io.on('disconnect',onDisconnect) + io.on('error',onDisconnect) + io.on('f', (d) => { + s.MainEventController(d,null,s.cx) + }) + return io } - io.on('connect_error', function(err){ - plugLog('ws://'+config.host+':'+config.port) - plugLog('Connection Failed') - plugLog(err) - }) - io.on('connect',function(d){ - s.cx({f:'init',plug:config.plug,notice:config.notice,type:config.type,connectionType:config.connectionType}); - }) - io.on('disconnect',function(d){ - if(retryConnection > maxRetryConnection && maxRetryConnection !== 0){ - webPageMssage = 'Max Failed Retries Reached' - return plugLog('Max Failed Retries Reached!',maxRetryConnection) - } - ++retryConnection - plugLog('Plugin Disconnected. Attempting Reconnect..') - io.connect(); - }) - io.on('f',function(d){ - s.MainEventController(d,null,s.cx) - }) + var io = createConnection() } - - s.createPythonScriptDaemon = function(){ - if(!config.pythonScript){config.pythonScript=__dirname+'/pumpkin.py'} - if(!config.pythonPort){config.pythonPort=7990} + s.getWebsocket = () => { + return io + } + if(config.clusterMode){ + plugLog('Plugin enabling Cluster Mode...') + if(config.clusterBasedOnGpu){ + setTimeout(() => { + parseNvidiaSmi((gpus)=>{ + io.emit('gpuUsage',gpus) + }) + },1000 * 10) + }else{ + setTimeout(() => { + getCpuUsage((percent) => { + io.emit('cpuUsage',percent) + }) + },1000 * 10) + } + } + s.createPythonScriptDaemon = () => { + if(!config.pythonScript){config.pythonScript = config.dirname + '/pumpkin.py'} + if(!config.pythonPort){config.pythonPort = 7990} //Start Python Controller s.callbacks = {} - s.createCameraBridgeToPython = function(uniqueId){ - var pythonIo = require('socket.io-client')('ws://localhost:'+config.pythonPort,{transports : ['websocket']}); - var sendToPython = function(data,callback){ + s.createCameraBridgeToPython = (uniqueId) => { + var pythonIo = require('socket.io-client')('ws://localhost:' + config.pythonPort,{ + transports: ['websocket'] + }) + var sendToPython = (data,callback) => { s.callbacks[data.id] = callback pythonIo.emit('f',data) } - var refreshTracker = function(data){ + var refreshTracker = (data) => { pythonIo.emit('refreshTracker',{trackerId : data}) } - pythonIo.on('connect',function(d){ + pythonIo.on('connect', (d) => { s.debugLog(uniqueId+' is Connected from Python') }) - pythonIo.on('disconnect',function(d){ + pythonIo.on('disconnect', (d) => { s.debugLog(uniqueId+' is Disconnected from Python') - setTimeout(function(){ + setTimeout(() => { pythonIo.connect(); s.debugLog(uniqueId+' is Attempting to Reconect to Python') },3000) }) - pythonIo.on('f',function(d){ + pythonIo.on('f', (d) => { if(s.callbacks[d.id]){ s.callbacks[d.id](d.data) delete(s.callbacks[d.id]) } }) - return {refreshTracker : refreshTracker, sendToPython : sendToPython} + return { + refreshTracker: refreshTracker, + sendToPython: sendToPython + } } //Start Python Daemon process.env.PYTHONUNBUFFERED = 1; - var createPythonProcess = function(){ + var createPythonProcess = () => { s.isPythonRunning = false - s.pythonScript = spawn('sh',[__dirname+'/bootPy.sh',config.pythonScript,__dirname]); - var onStdErr = function(data){ + s.pythonScript = spawn('sh',[config.dirname + '/bootPy.sh',config.pythonScript,config.dirname]); + var onStdErr = (data) => { s.debugLog(data.toString()) } - var onStdOut = function(data){ + var onStdOut = (data) => { s.debugLog(data.toString()) } - setTimeout(function(){ + setTimeout(() => { s.isPythonRunning = true },5000) s.pythonScript.stderr.on('data',onStdErr); s.pythonScript.stdout.on('data',onStdOut); - s.pythonScript.on('close', function () { + s.pythonScript.on('close', () => { s.debugLog('Python CLOSED') }); } createPythonProcess() } - s.getImageDimensions = function(d){ + s.getImageDimensions = (d) => { var height var width if( diff --git a/plugins/python-contour/.gitignore b/plugins/python-contour/.gitignore deleted file mode 100644 index 543f8308..00000000 --- a/plugins/python-contour/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -conf.json -faces -data \ No newline at end of file diff --git a/plugins/python-contour/INSTALL.sh b/plugins/python-contour/INSTALL.sh deleted file mode 100644 index da271df6..00000000 --- a/plugins/python-contour/INSTALL.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash -echo "-----------------------------------------------" -echo "-- Installing Python Dlib Plugin for Shinobi --" -echo "-----------------------------------------------" -echo "-----------------------------------" -if [ ! -e "./conf.json" ]; then - echo "Creating conf.json" - sudo cp conf.sample.json conf.json -else - echo "conf.json already exists..." -fi -echo "-----------------------------------" -sudo apt update -y -echo "Installing python3" -sudo apt install python3 python3-dev python3-pip -y -echo "-----------------------------------" -sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y -sudo apt update -sudo apt-get install gcc-6 g++-6 -y && sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-6 60 --slave /usr/bin/g++ g++ /usr/bin/g++-6 -echo "-----------------------------------" -if ! [ -x "$(command -v nvidia-smi)" ]; then - echo "You need to install NVIDIA Drivers to use this." - echo "inside the Shinobi directory run the following :" - echo "sh INSTALL/cuda9-part1.sh" - exit 1 -else - echo "NVIDIA Drivers found..." - echo "$(nvidia-smi |grep 'Driver Version')" -fi -echo "-----------------------------------" -if [ ! -d "/usr/local/cuda" ]; then - echo "You need to install CUDA Toolkit to use this." - echo "inside the Shinobi directory run the following :" - echo "sh INSTALL/cuda9-part2-after-reboot.sh" - exit 1 -else - echo "CUDA Toolkit found..." -fi -echo "-----------------------------------" -if ! [ -x "$(command -v opencv_version)" ]; then - echo "You need to install OpenCV with CUDA first." - echo "inside the Shinobi directory run the following :" - echo "sh INSTALL/opencv-cuda.sh" - exit 1 -else - echo "OpenCV found... : $(opencv_version)" -fi -echo "-----------------------------------" -echo "Getting new pip..." -pip3 install --upgrade pip -pip install --user --upgrade pip -export PATH=/usr/local/cuda/bin:$PATH -echo "Smoking pips..." -pip3 install flask_socketio -pip3 install flask -pip3 install numpy -pip3 install gevent gevent-websocket -echo "Start the plugin with pm2 like so :" -echo "pm2 start shinobi-python-dlib.js" diff --git a/plugins/python-contour/README.md b/plugins/python-contour/README.md deleted file mode 100644 index 26c20f5f..00000000 --- a/plugins/python-contour/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# Python Contour Detection with OpenCV - -> This plugin requires the use of port `7990` by default. You can specify a different port by adding `pythonPort` to your plugin's conf.json. - -**Ubuntu and Debian only** - -Go to the Shinobi directory. **/home/Shinobi** is the default directory. - -``` -cd /home/Shinobi/plugins/python-contour -``` - -Copy the config file. - -``` -sh INSTALL.sh -``` - -Start the plugin. - -``` -pm2 start shinobi-python-contour.js -``` - -Doing this will reveal options in the monitor configuration. Shinobi does not need to be restarted when a plugin is initiated or stopped. - -## Run the plugin as a Host -> The main app (Shinobi) will be the client and the plugin will be the host. The purpose of allowing this method is so that you can use one plugin for multiple Shinobi instances. Allowing you to easily manage connections without starting multiple processes. - -Edit your plugins configuration file. Set the `hostPort` **to be different** than the `listening port for camera.js`. - -``` -nano conf.json -``` - -Here is a sample of a Host configuration for the plugin. - - `plug` is the name of the plugin corresponding in the main configuration file. - - `https` choose if you want to use SSL or not. Default is `false`. - - `hostPort` can be any available port number. **Don't make this the same port number as Shinobi.** Default is `8082`. - - `type` tells the main application (Shinobi) what kind of plugin it is. In this case it is a detector. - -``` -{ - "plug":"PythonContour", - "hostPort":8082, - "key":"SomeOpenALPRkeySoPeopleDontMessWithYourShinobi", - "mode":"host", - "type":"detector" -} -``` - -Now modify the **main configuration file** located in the main directory of Shinobi. *Where you currently should be.* - -``` -nano conf.json -``` - -Add the `plugins` array if you don't already have it. Add the following *object inside the array*. - -``` - "plugins":[ - { - "id" : "PythonContour", - "https" : false, - "host" : "localhost", - "port" : 8082, - "key" : "SomeOpenALPRkeySoPeopleDontMessWithYourShinobi", - "mode" : "host", - "type" : "detector" - } - ], -``` diff --git a/plugins/python-contour/bootPy.sh b/plugins/python-contour/bootPy.sh deleted file mode 100644 index 68be5794..00000000 --- a/plugins/python-contour/bootPy.sh +++ /dev/null @@ -1 +0,0 @@ -python3 -u $@ \ No newline at end of file diff --git a/plugins/python-contour/conf.sample.json b/plugins/python-contour/conf.sample.json deleted file mode 100644 index 2687164e..00000000 --- a/plugins/python-contour/conf.sample.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "plug":"PythonContour", - "host":"localhost", - "port":8080, - "pythonPort":7990, - "hostPort":8082, - "key":"YOUR_CONTOUR_PLUGIN_KEY", - "mode":"client", - "type":"detector" -} diff --git a/plugins/python-contour/package.json b/plugins/python-contour/package.json deleted file mode 100644 index c4c96d93..00000000 --- a/plugins/python-contour/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "shinobi-python-contour", - "version": "1.0.0", - "description": "Contour plugin for Shinobi that uses Python functions for detection.", - "main": "shinobi-python-contour.js", - "dependencies": { - "socket.io-client": "^1.7.4", - "express": "^4.16.2", - "moment": "^2.19.2", - "socket.io": "^2.0.4" - }, - "devDependencies": {}, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "Moe Alam", - "license": "ISC" -} diff --git a/plugins/python-contour/pumpkin.py b/plugins/python-contour/pumpkin.py deleted file mode 100644 index 728c0376..00000000 --- a/plugins/python-contour/pumpkin.py +++ /dev/null @@ -1,116 +0,0 @@ -from flask import Flask, request, jsonify, render_template -from flask_socketio import SocketIO, emit -import cv2 -import os -import json -import numpy as np -import sys - -dirname = sys.argv[1] -try: - with open("{}/conf.json".format(dirname)) as json_file: - config = json.load(json_file) - httpPort = config['pythonPort'] - try: - httpPort - except NameError: - httpPort = 7990 -except Exception as e: - print("conf.json not found.") - httpPort = 7990 - -# Load Flask -app = Flask("Contour Detection for Shinobi (Pumpkin Pie)") -socketio = SocketIO(app) -# Silence Flask -# import logging -# log = logging.getLogger('werkzeug') -# log.setLevel(logging.ERROR) - -#load car detector -oldFrames = {} - -fgbg = cv2.createBackgroundSubtractorMOG2() - -# detection function -def spark(filepath,trackerId): - try: - filepath - except NameError: - return "File path not found." - frame = cv2.imread(filepath) - returnData = [] - # resize the frame, convert it to grayscale, and blur it - # frame = imutils.resize(frame, width=500) - gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - gray = cv2.GaussianBlur(gray, (21, 21), 0) - - # if the first frame is None, initialize it - global oldFrames - try: - oldFrames[trackerId] - except KeyError: - oldFrames[trackerId] = None - - if oldFrames[trackerId] is None: - oldFrames[trackerId] = gray - - # compute the absolute difference between the current frame and - # first frame - frameDelta = cv2.absdiff(oldFrames[trackerId], gray) - thresh = cv2.threshold(frameDelta, 55, 255, cv2.THRESH_BINARY)[1] - - # dilate the thresholded image to fill in holes, then find contours - # on thresholded image - thresh = cv2.dilate(thresh, None, iterations=2) - image = thresh.copy() - image,cnts,hierarchy = cv2.findContours(image, cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE) - - # loop over the contours - for c in cnts: - # if the contour is too small, ignore it - #if cv2.contourArea(c) > args["max_area"] or cv2.contourArea < args["min_area"]: - # continue - d = max(cnts, key = cv2.contourArea) - # compute the bounding box for the contour, draw it on the frame, - # and update the text - (x, y, w, h) = cv2.boundingRect(d) - matrix = {} - matrix["tag"] = "Contour" - matrix["x"] = int(x) - matrix["y"] = int(y) - matrix["w"] = int(w) - matrix["h"] = int(h) - returnData.append(matrix) - return returnData - -# bake the image data by a file path -# POST body contains the "img" variable. The value should be to a local image path. -# Example : /dev/shm/streams/[GROUP_KEY]/[MONITOR_ID]/s.jpg -@app.route('/', methods=['GET']) -def index(): - return "Pumpkin.py is running. This web interface should NEVER be accessible remotely. The Node.js plugin that runs this script should only be allowed accessible remotely." - -# bake the image data by a file path -# POST body contains the "img" variable. The value should be to a local image path. -# Example : /dev/shm/streams/[GROUP_KEY]/[MONITOR_ID]/s.jpg -@app.route('/post', methods=['POST']) -def post(): - filepath = request.form['img'] - return jsonify(spark(filepath)) - -# bake the image data by a file path -# GET string contains the "img" variable. The value should be to a local image path. -# Example : /dev/shm/streams/[GROUP_KEY]/[MONITOR_ID]/s.jpg -@app.route('/get', methods=['GET']) -def get(): - filepath = request.args.get('img') - return jsonify(spark(filepath)) - -@socketio.on('f') -def receiveMessage(message): - emit('f',{'id':message.get("id"),'data':spark(message.get("path"),message.get("trackerId"))}) - -# quick-and-dirty start -if __name__ == '__main__': - socketio.run(app, port=httpPort) diff --git a/plugins/python-contour/shinobi-python-contour.js b/plugins/python-contour/shinobi-python-contour.js deleted file mode 100644 index 711eb479..00000000 --- a/plugins/python-contour/shinobi-python-contour.js +++ /dev/null @@ -1,298 +0,0 @@ -// -// Shinobi - Python DLIB Plugin -// Copyright (C) 2016-2025 Moe Alam, moeiscool -// -// # Donate -// -// If you like what I am doing here and want me to continue please consider donating :) -// PayPal : paypal@m03.ca -// -process.on('uncaughtException', function (err) { - console.error('uncaughtException',err); -}); -//main vars -var fs=require('fs'); -var exec = require('child_process').exec; -var spawn = require('child_process').spawn; -var moment = require('moment'); -var http = require('http'); -var express = require('express'); -var socketIoClient = require('socket.io-client'); -var config = require('./conf.json'); -var http = require('http'), - app = express(), - server = http.createServer(app); - -exec("kill $(ps aux | grep '[p]ython3 pumpkin.py' | awk '{print $2}')") - -s={ - group:{}, - dir:{}, - isWin:(process.platform==='win32'), - s:function(json){return JSON.stringify(json,null,3)} -} -s.checkCorrectPathEnding=function(x){ - var length=x.length - if(x.charAt(length-1)!=='/'){ - x=x+'/' - } - return x.replace('__DIR__',__dirname) -} -s.debugLog = function(){ - if(config.debugLog === true){ - console.log(new Date(),arguments) - if(config.debugLogVerbose === true){ - console.log(new Error()) - } - } -} -if(!config.port){config.port=8080} -if(!config.pythonScript){config.pythonScript=__dirname+'/pumpkin.py'} -if(!config.pythonPort){config.pythonPort=7990} -if(!config.hostPort){config.hostPort=8082} -if(config.systemLog===undefined){config.systemLog=true} -//default stream folder check -if(!config.streamDir){ - if(s.isWin===false){ - config.streamDir='/dev/shm' - }else{ - config.streamDir=config.windowsTempDir - } - if(!fs.existsSync(config.streamDir)){ - config.streamDir=__dirname+'/streams/' - }else{ - config.streamDir+='/streams/' - } -} -s.dir.streams=config.streamDir; -//streams dir -if(!fs.existsSync(s.dir.streams)){ - fs.mkdirSync(s.dir.streams); -} -s.gid=function(x){ - if(!x){x=10};var t = "";var p = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - for( var i=0; i < x; i++ ) - t += p.charAt(Math.floor(Math.random() * p.length)); - return t; -}; -s.getRequest = function(url,callback){ - return http.get(url, function(res){ - var body = ''; - res.on('data', function(chunk){ - body += chunk; - }); - res.on('end',function(){ - try{body = JSON.parse(body)}catch(err){} - callback(body) - }); - }).on('error', function(e){ -// s.systemLog("Get Snapshot Error", e); - }); -} -s.multiplerHeight = 1 -s.multiplerWidth = 1 -s.detectObject=function(buffer,d,tx){ - d.tmpFile=s.gid(5)+'.jpg' - if(!fs.existsSync(s.dir.streams)){ - fs.mkdirSync(s.dir.streams); - } - d.dir=s.dir.streams+d.ke+'/' - if(!fs.existsSync(d.dir)){ - fs.mkdirSync(d.dir); - } - d.dir=s.dir.streams+d.ke+'/'+d.id+'/' - if(!fs.existsSync(d.dir)){ - fs.mkdirSync(d.dir); - } - fs.writeFile(d.dir+d.tmpFile,buffer,function(err){ - if(err) return s.systemLog(err); - if(s.isPythonRunning === false){ - return console.log('Python Script is not Running.') - } - var callbackId = s.gid(10) - s.group[d.ke][d.id].sendToPython({path:d.dir+d.tmpFile,id:callbackId,trackerId:d.ke+d.id},function(data){ - if(data.length > 0){ - var mats=[] - data.forEach(function(v){ - mats.push({ - x:v.x, - y:v.y, - width: v.w, - height: v.h, - confidence:v.confidence, - tag:v.tag - }) - }) - tx({ - f:'trigger', - id:d.id, - ke:d.ke, - details:{ - plug:config.plug, - name:'dlib', - reason:'object', - matrices:mats, - imgHeight:parseFloat(d.mon.detector_scale_y), - imgWidth:parseFloat(d.mon.detector_scale_x) - } - }) - } - delete(s.callbacks[callbackId]) - exec('rm -rf '+d.dir+d.tmpFile,{encoding:'utf8'}) - }) - }) -} -s.systemLog=function(q,w,e){ - if(w===undefined){return} - if(!w){w=''} - if(!e){e=''} - if(config.systemLog===true){ - return console.log(moment().format(),q,w,e) - } -} -s.MainEventController=function(d,cn,tx){ - switch(d.f){ - case'init_plugin_as_host': - if(!cn){ - console.log('No CN',d) - return - } - if(d.key!==config.key){ - console.log(new Date(),'Plugin Key Mismatch',cn.request.connection.remoteAddress,d) - cn.emit('init',{ok:false}) - cn.disconnect() - }else{ - console.log(new Date(),'Plugin Connected to Client',cn.request.connection.remoteAddress) - cn.emit('init',{ok:true,plug:config.plug,notice:config.notice,type:config.type}) - } - break; - case'init_monitor': - if(s.group[d.ke]&&s.group[d.ke][d.id]){ - delete(s.group[d.ke][d.id].buffer) - s.group[d.ke][d.id].refreshTracker(d.ke+d.id) - } - break; - case'frame': - try{ - if(!s.group[d.ke]){ - s.group[d.ke]={} - } - if(!s.group[d.ke][d.id]){ - var engine = s.createCameraBridgeToPython(d.ke+d.id) - s.group[d.ke][d.id]={ - sendToPython : engine.sendToPython, - refreshTracker : engine.refreshTracker - } - } - if(!s.group[d.ke][d.id].buffer){ - s.group[d.ke][d.id].buffer=[d.frame]; - }else{ - s.group[d.ke][d.id].buffer.push(d.frame) - } - if(d.frame[d.frame.length-2] === 0xFF && d.frame[d.frame.length-1] === 0xD9){ - s.detectObject(Buffer.concat(s.group[d.ke][d.id].buffer),d,tx) - s.group[d.ke][d.id].buffer=null; - } - }catch(err){ - if(err){ - s.systemLog(err) - delete(s.group[d.ke][d.id].buffer) - } - } - break; - } -} -server.listen(config.hostPort); -//web pages and plugin api -app.get('/', function (req, res) { - res.end(''+config.plug+' for Shinobi is running') -}); -//Conector to Shinobi -if(config.mode==='host'){ - //start plugin as host - var io = require('socket.io')(server); - io.attach(server); - s.connectedClients={}; - io.on('connection', function (cn) { - s.connectedClients[cn.id]={id:cn.id} - s.connectedClients[cn.id].tx = function(data){ - data.pluginKey=config.key;data.plug=config.plug; - return io.to(cn.id).emit('ocv',data); - } - cn.on('f',function(d){ - s.MainEventController(d,cn,s.connectedClients[cn.id].tx) - }); - cn.on('disconnect',function(d){ - delete(s.connectedClients[cn.id]) - }) - }); -}else{ - //start plugin as client - if(!config.host){config.host='localhost'} - var io = socketIoClient('ws://'+config.host+':'+config.port);//connect to master - s.cx=function(x){x.pluginKey=config.key;x.plug=config.plug;return io.emit('ocv',x)} - io.on('connect',function(d){ - s.cx({f:'init',plug:config.plug,notice:config.notice,type:config.type}); - }) - io.on('disconnect',function(d){ - io.connect(); - }) - io.on('f',function(d){ - s.MainEventController(d,null,s.cx) - }) -} - -//Start Python Controller -s.callbacks = {} -s.createCameraBridgeToPython = function(uniqueId){ - var pythonIo = socketIoClient('ws://localhost:'+config.pythonPort,{transports : ['websocket']}); - var sendToPython = function(data,callback){ - s.callbacks[data.id] = callback - pythonIo.emit('f',data) - } - var refreshTracker = function(data){ - pythonIo.emit('refreshTracker',{trackerId : data}) - } - pythonIo.on('connect',function(d){ - s.debugLog(uniqueId+' is Connected from Python') - }) - pythonIo.on('disconnect',function(d){ - s.debugLog(uniqueId+' is Disconnected from Python') - setTimeout(function(){ - pythonIo.connect(); - s.debugLog(uniqueId+' is Attempting to Reconect to Python') - },3000) - }) - pythonIo.on('f',function(d){ - if(s.callbacks[d.id]){ - s.callbacks[d.id](d.data) - delete(s.callbacks[d.id]) - } - }) - return {refreshTracker : refreshTracker, sendToPython : sendToPython} -} - - -//Start Python Daemon -process.env.PYTHONUNBUFFERED = 1; -s.createPythonProcess = function(){ - s.isPythonRunning = false - s.pythonScript = spawn('sh',[__dirname+'/bootPy.sh',config.pythonScript,__dirname]); - var onStdErr = function(data){ - s.debugLog(data.toString()) - } - var onStdOut = function(data){ - s.debugLog(data.toString()) - } - setTimeout(function(){ - s.isPythonRunning = true - },5000) - s.pythonScript.stderr.on('data',onStdErr); - - s.pythonScript.stdout.on('data',onStdOut); - - s.pythonScript.on('close', function () { - s.debugLog('Python CLOSED') - }); -} -s.createPythonProcess() diff --git a/plugins/tensorflow/.env b/plugins/tensorflow/.env new file mode 100644 index 00000000..09483c0a --- /dev/null +++ b/plugins/tensorflow/.env @@ -0,0 +1,2 @@ +TF_FORCE_GPU_ALLOW_GROWTH=true +#CUDA_VISIBLE_DEVICES=0,2 diff --git a/plugins/tensorflow/.gitignore b/plugins/tensorflow/.gitignore index 58816542..f6c52d28 100644 --- a/plugins/tensorflow/.gitignore +++ b/plugins/tensorflow/.gitignore @@ -1,2 +1,4 @@ conf.json -cascades \ No newline at end of file +dist +models +.env diff --git a/plugins/tensorflow/INSTALL.sh b/plugins/tensorflow/INSTALL.sh index 09f92577..4b5e6209 100644 --- a/plugins/tensorflow/INSTALL.sh +++ b/plugins/tensorflow/INSTALL.sh @@ -1,20 +1,57 @@ #!/bin/bash -mkdir data -mkdir data/inception -chmod -R 777 data -wget https://cdn.shinobi.video/weights/inception5h.zip -O inception5h.zip -unzip inception5h.zip -d data/inception -if [ $(dpkg-query -W -f='${Status}' opencv_version 2>/dev/null | grep -c "ok installed") -eq 0 ]; then - echo "Shinobi - Do ypu want to let the `opencv4nodejs` npm package install OpenCV? " - echo "Only do this if you do not have OpenCV already or will not use a GPU (Hardware Acceleration)." +DIR=`dirname $0` +echo "Removing existing Tensorflow Node.js modules..." +npm uninstall @tensorflow/tfjs-node-gpu --unsafe-perm +npm uninstall @tensorflow/tfjs-node --unsafe-perm +npm install yarn -g --unsafe-perm --force +GPU_INSTALL="0" +echo "Shinobi - Are you installing on ARM64? This applies to computers like Jetson Nano and Raspberry Pi Model 3 B+" +echo "(y)es or (N)o" +read armCpu +if [ "$armCpu" = "y" ] || [ "$armCpu" = "Y" ]; then + echo "Shinobi - Is it a Jetson Nano?" + echo "You must be on JetPack 4.3 for this plugin to install." + echo "JetPack 4.3 Image can be found here : https://developer.nvidia.com/jetpack-43-archive" + echo "(y)es or (N)o" + read isItJetsonNano + echo "Shinobi - You may see Unsupported Errors, please wait while patches are applied." + if [ "$isItJetsonNano" = "y" ] || [ "$isItJetsonNano" = "Y" ]; then + GPU_INSTALL="1" + npm install @tensorflow/tfjs-node-gpu@1.7.3 --unsafe-perm + cd node_modules/@tensorflow/tfjs-node-gpu + echo '{"tf-lib": "https://cdn.shinobi.video/installers/libtensorflow-gpu-linux-arm64-1.15.0.tar.gz"}' > "scripts/custom-binary.json" + else + npm install @tensorflow/tfjs-node@1.7.3 --unsafe-perm + cd node_modules/@tensorflow/tfjs-node + echo '{"tf-lib": "https://cdn.shinobi.video/installers/libtensorflow-cpu-linux-arm-1.15.0.tar.gz"}' > "scripts/custom-binary.json" + fi + npm install --unsafe-perm + npm audit fix --force + cd ../../.. +else + echo "Shinobi - Do you want to install TensorFlow.js with GPU support? " + echo "You can run this installer again to change it." echo "(y)es or (N)o" read nodejsinstall if [ "$nodejsinstall" = "y" ] || [ "$nodejsinstall" = "Y" ]; then - export OPENCV4NODEJS_DISABLE_AUTOBUILD=0 + GPU_INSTALL="1" + npm install @tensorflow/tfjs-node-gpu@1.7.3 --unsafe-perm else - export OPENCV4NODEJS_DISABLE_AUTOBUILD=1 + npm install @tensorflow/tfjs-node@1.7.3 --unsafe-perm fi -else - export OPENCV4NODEJS_DISABLE_AUTOBUILD=1 fi -npm install opencv4nodejs moment express canvas@1.6 --unsafe-perm \ No newline at end of file +npm install --unsafe-perm +if [ ! -e "./conf.json" ]; then + echo "Creating conf.json" + sudo cp conf.sample.json conf.json +else + echo "conf.json already exists..." +fi +echo "Adding Random Plugin Key to Main Configuration" +tfjsBuildVal="cpu" +if [ "$GPU_INSTALL" = "1" ]; then + tfjsBuildVal="gpu" +fi +node $DIR/../../tools/modifyConfigurationForPlugin.js tensorflow key=$(head -c 64 < /dev/urandom | sha256sum | awk '{print substr($1,1,60)}') tfjsBuild=$tfjsBuildVal +echo "TF_FORCE_GPU_ALLOW_GROWTH=true" > "$DIR/.env" +echo "#CUDA_VISIBLE_DEVICES=0,2" >> "$DIR/.env" diff --git a/plugins/tensorflow/ObjectDetectors.js b/plugins/tensorflow/ObjectDetectors.js new file mode 100644 index 00000000..0270f213 --- /dev/null +++ b/plugins/tensorflow/ObjectDetectors.js @@ -0,0 +1,78 @@ +module.exports = function(config){ + var tfjsSuffix = '' + switch(config.tfjsBuild){ + case'gpu': + tfjsSuffix = '-gpu' + var tf = require('@tensorflow/tfjs-node-gpu') + break; + case'cpu': + var tf = require('@tensorflow/tfjs-node') + break; + default: + try{ + tfjsSuffix = '-gpu' + var tf = require('@tensorflow/tfjs-node-gpu') + }catch(err){ + console.log(err) + } + break; + } + + const cocossd = require('@tensorflow-models/coco-ssd'); + // const mobilenet = require('@tensorflow-models/mobilenet'); + + + async function loadCocoSsdModal() { + const modal = await cocossd.load({ + base: config.cocoBase || 'lite_mobilenet_v2', //lite_mobilenet_v2 + modelUrl: config.cocoUrl, + }) + return modal; + } + + // async function loadMobileNetModal() { + // const modal = await mobilenet.load({ + // version: 1, + // alpha: 0.25 | .50 | .75 | 1.0, + // }) + // return modal; + // } + + function getTensor3dObject(numOfChannels,imageArray) { + + const tensor3d = tf.node.decodeJpeg( imageArray, numOfChannels ); + + return tensor3d; + } + // const mobileNetModel = this.loadMobileNetModal(); + var loadCocoSsdModel = { + detect: function(){ + return {data:[]} + } + } + async function init() { + loadCocoSsdModel = await loadCocoSsdModal(); + } + init() + return class ObjectDetectors { + + constructor(image, type) { + this.startTime = new Date(); + this.inputImage = image; + this.type = type; + } + + async process() { + const tensor3D = getTensor3dObject(3,(this.inputImage)); + let predictions = await loadCocoSsdModel.detect(tensor3D); + + tensor3D.dispose(); + + return { + data: predictions, + type: this.type, + time: new Date() - this.startTime + } + } + } +} diff --git a/plugins/dlib/README.md b/plugins/tensorflow/README.md similarity index 87% rename from plugins/dlib/README.md rename to plugins/tensorflow/README.md index 8ac49c71..0bf520f7 100644 --- a/plugins/dlib/README.md +++ b/plugins/tensorflow/README.md @@ -1,11 +1,11 @@ -#Dlib Plugin for Shinobi +# TensorFlow.js **Ubuntu and CentOS only** Go to the Shinobi directory. **/home/Shinobi** is the default directory. ``` -cd /home/Shinobi/plugins/dlib +cd /home/Shinobi/plugins/tensorflow ``` Copy the config file. @@ -17,7 +17,7 @@ sh INSTALL.sh Start the plugin. ``` -pm2 start shinobi-dlib.js +pm2 start shinobi-tensorflow.js ``` Doing this will reveal options in the monitor configuration. Shinobi does not need to be restarted when a plugin is initiated or stopped. @@ -39,12 +39,11 @@ Here is a sample of a Host configuration for the plugin. ``` { - "plug":"Dlib", + "plug":"Tensorflow", "hostPort":8082, - "key":"Dlib123123", + "key":"Tensorflow123123", "mode":"host", - "type":"detector", - "conectionType":"websocket" + "type":"detector" } ``` @@ -59,11 +58,11 @@ Add the `plugins` array if you don't already have it. Add the following *object ``` "plugins":[ { - "id" : "Dlib", + "id" : "Tensorflow", "https" : false, "host" : "localhost", "port" : 8082, - "key" : "Dlib123123", + "key" : "Tensorflow123123", "mode" : "host", "type" : "detector" } diff --git a/plugins/tensorflow/conf.sample.json b/plugins/tensorflow/conf.sample.json index 7c27d4ee..272dea74 100644 --- a/plugins/tensorflow/conf.sample.json +++ b/plugins/tensorflow/conf.sample.json @@ -1,9 +1,10 @@ { "plug":"Tensorflow", "host":"localhost", + "tfjsBuild":"gpuORcpu", "port":8080, "hostPort":8082, "key":"change_this_to_something_very_random____make_sure_to_match__/plugins/opencv/conf.json", "mode":"client", "type":"detector" -} \ No newline at end of file +} diff --git a/plugins/tensorflow/openalpr.conf b/plugins/tensorflow/openalpr.conf deleted file mode 100644 index 070752b1..00000000 --- a/plugins/tensorflow/openalpr.conf +++ /dev/null @@ -1,94 +0,0 @@ - -; Specify the path to the runtime data directory -runtime_dir = ${CMAKE_INSTALL_PREFIX}/share/openalpr/runtime_data - - -ocr_img_size_percent = 1.33333333 -state_id_img_size_percent = 2.0 - -; Calibrating your camera improves detection accuracy in cases where vehicle plates are captured at a steep angle -; Use the openalpr-utils-calibrate utility to calibrate your fixed camera to adjust for an angle -; Once done, update the prewarp config with the values obtained from the tool -prewarp = - -; detection will ignore plates that are too large. This is a good efficiency technique to use if the -; plates are going to be a fixed distance away from the camera (e.g., you will never see plates that fill -; up the entire image -max_plate_width_percent = 100 -max_plate_height_percent = 100 - -; detection_iteration_increase is the percentage that the LBP frame increases each iteration. -; It must be greater than 1.0. A value of 1.01 means increase by 1%, 1.10 increases it by 10% each time. -; So a 1% increase would be ~10x slower than 10% to process, but it has a higher chance of landing -; directly on the plate and getting a strong detection -detection_iteration_increase = 1.1 - -; The minimum detection strength determines how sure the detection algorithm must be before signaling that -; a plate region exists. Technically this corresponds to LBP nearest neighbors (e.g., how many detections -; are clustered around the same area). For example, 2 = very lenient, 9 = very strict. -detection_strictness = 3 - -; The detection doesn't necessarily need an extremely high resolution image in order to detect plates -; Using a smaller input image should still find the plates and will do it faster -; Tweaking the max_detection_input values will resize the input image if it is larger than these sizes -; max_detection_input_width/height are specified in pixels -max_detection_input_width = 1280 -max_detection_input_height = 720 - -; detector is the technique used to find license plate regions in an image. Value can be set to -; lbpcpu - default LBP-based detector uses the system CPU -; lbpgpu - LBP-based detector that uses Nvidia GPU to increase recognition speed. -; lbpopencl - LBP-based detector that uses OpenCL GPU to increase recognition speed. Requires OpenCV 3.0 -; morphcpu - Experimental detector that detects white rectangles in an image. Does not require training. -detector = lbpgpu - -; If set to true, all results must match a postprocess text pattern if a pattern is available. -; If not, the result is disqualified. -must_match_pattern = 0 - -; Bypasses plate detection. If this is set to 1, the library assumes that each region provided is a likely plate area. -skip_detection = 0 - -; Specifies the full path to an image file that constrains the detection area. Only the plate regions allowed through the mask -; will be analyzed. The mask image must match the resolution of your image to be analyzed. The mask is black and white. -; Black areas will be ignored, white areas will be searched. An empty value means no mask (scan the entire image) -detection_mask_image = - -; OpenALPR can scan the same image multiple times with different randomization. Setting this to a value larger than -; 1 may increase accuracy, but will increase processing time linearly (e.g., analysis_count = 3 is 3x slower) -analysis_count = 1 - -; OpenALPR detects high-contrast plate crops and uses an alternative edge detection technique. Setting this to 0.0 -; would classify ALL images as high-contrast, setting it to 1.0 would classify no images as high-contrast. -contrast_detection_threshold = 0.3 - -max_plate_angle_degrees = 15 - -ocr_min_font_point = 6 - -; Minimum OCR confidence percent to consider. -postprocess_min_confidence = 65 - -; Any OCR character lower than this will also add an equally likely -; chance that the character is incorrect and will be skipped. Value is a confidence percent -postprocess_confidence_skip_level = 80 - - -debug_general = 0 -debug_timing = 0 -debug_detector = 0 -debug_prewarp = 0 -debug_state_id = 0 -debug_plate_lines = 0 -debug_plate_corners = 0 -debug_char_segment = 0 -debug_char_analysis = 0 -debug_color_filter = 0 -debug_ocr = 0 -debug_postprocess = 0 -debug_show_images = 0 -debug_pause_on_frame = 0 - - - - diff --git a/plugins/tensorflow/package.json b/plugins/tensorflow/package.json new file mode 100644 index 00000000..3260a7eb --- /dev/null +++ b/plugins/tensorflow/package.json @@ -0,0 +1,40 @@ +{ + "name": "shinobi-tensorflow", + "author": "Shinob Systems, Moinul Alam", + "version": "1.0.0", + "description": "Object Detection plugin based on @tensorflow/tfjs-node", + "main": "shinobi-tensorflow.js", + "dependencies": { + "@tensorflow-models/coco-ssd": "^2.0.3", + "@tensorflow/tfjs-converter": "^1.7.3", + "@tensorflow/tfjs-core": "^1.7.3", + "@tensorflow/tfjs-layers": "^1.7.3", + "@tensorflow/tfjs-node": "^1.7.3", + "@tensorflow/tfjs-node-gpu": "^1.7.3", + "dotenv": "^8.2.0", + "express": "^4.16.2", + "moment": "^2.19.2", + "socket.io": "^2.0.4", + "socket.io-client": "^1.7.4" + }, + "devDependencies": {}, + "bin": "shinobi-tensorflow.js", + "scripts": { + "package": "pkg package.json -t linux,macos,win --out-path dist", + "package-x64": "pkg package.json -t linux-x64,macos-x64,win-x64 --out-path dist/x64", + "package-x86": "pkg package.json -t linux-x86,macos-x86,win-x86 --out-path dist/x86", + "package-armv6": "pkg package.json -t linux-armv6,macos-armv6,win-armv6 --out-path dist/armv6", + "package-armv7": "pkg package.json -t linux-armv7,macos-armv7,win-armv7 --out-path dist/armv7", + "package-all": "npm run package && npm run package-x64 && npm run package-x86 && npm run package-armv6 && npm run package-armv7" + }, + "pkg": { + "targets": [ + "node12" + ], + "scripts": [ + "../pluginBase.js" + ], + "assets": [ + ] + } +} diff --git a/plugins/tensorflow/shinobi-tensorflow.js b/plugins/tensorflow/shinobi-tensorflow.js index 204b5782..975e3162 100644 --- a/plugins/tensorflow/shinobi-tensorflow.js +++ b/plugins/tensorflow/shinobi-tensorflow.js @@ -1,5 +1,5 @@ // -// Shinobi - OpenCV Plugin +// Shinobi - Tensorflow Plugin // Copyright (C) 2016-2025 Moe Alam, moeiscool // // # Donate @@ -7,496 +7,59 @@ // If you like what I am doing here and want me to continue please consider donating :) // PayPal : paypal@m03.ca // -process.on('uncaughtException', function (err) { - console.error('uncaughtException',err); -}); -var fs=require('fs'); -var cv=require('opencv4nodejs'); -var exec = require('child_process').exec; -var moment = require('moment'); -var Canvas = require('canvas'); -var express = require('express'); -const path = require('path'); -var http = require('http'), - app = express(), - server = http.createServer(app); -var config=require('./conf.json'); -if(!config.port){config.port=8080} -if(!config.hostPort){config.hostPort=8082} -if(config.systemLog===undefined){config.systemLog=true} -if(config.cascadesDir===undefined){config.cascadesDir=__dirname+'/cascades/'} -if(config.alprConfig===undefined){config.alprConfig=__dirname+'/openalpr.conf'} -s={ - group:{}, - dir:{ - cascades : config.cascadesDir - }, - isWin:(process.platform==='win32'), - foundCascades : { - +// Base Init >> +var fs = require('fs'); +var config = require('./conf.json') +var dotenv = require('dotenv').config() +var s +try{ + s = require('../pluginBase.js')(__dirname,config) +}catch(err){ + console.log(err) + try{ + s = require('./pluginBase.js')(__dirname,config) + }catch(err){ + console.log(err) + return console.log(config.plug,'Plugin start has failed. pluginBase.js was not found.') } } -//default stream folder check -if(!config.streamDir){ - if(s.isWin===false){ - config.streamDir='/dev/shm' - }else{ - config.streamDir=config.windowsTempDir - } - if(!fs.existsSync(config.streamDir)){ - config.streamDir=__dirname+'/streams/' - }else{ - config.streamDir+='/streams/' - } -} -s.dir.streams=config.streamDir; -//streams dir -if(!fs.existsSync(s.dir.streams)){ - fs.mkdirSync(s.dir.streams); -} -//streams dir -if(!fs.existsSync(s.dir.cascades)){ - fs.mkdirSync(s.dir.cascades); -} -s.gid=function(x){ - if(!x){x=10};var t = "";var p = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - for( var i=0; i < x; i++ ) - t += p.charAt(Math.floor(Math.random() * p.length)); - return t; -}; -s.findCascades=function(callback){ - var tmp={}; - tmp.foundCascades=[]; - fs.readdir(s.dir.cascades,function(err,files){ - files.forEach(function(cascade,n){ - if(cascade.indexOf('.xml')>-1){ - tmp.foundCascades.push(cascade.replace('.xml','')) - } - }) - s.cascadesInDir=tmp.foundCascades; - callback(tmp.foundCascades) - }) -} -s.findCascades(function(){ - //get cascades -}) -s.detectLicensePlate=function(buffer,d,tx){ - if(!d.mon.detector_lisence_plate_country||d.mon.detector_lisence_plate_country===''){ - d.mon.detector_lisence_plate_country='us' - } - d.tmpFile=s.gid(5)+'.jpg' - if(!fs.existsSync(s.dir.streams)){ - fs.mkdirSync(s.dir.streams); - } - d.dir=s.dir.streams+d.ke+'/' - if(!fs.existsSync(d.dir)){ - fs.mkdirSync(d.dir); - } - d.dir=s.dir.streams+d.ke+'/'+d.id+'/' - if(!fs.existsSync(d.dir)){ - fs.mkdirSync(d.dir); - } - fs.writeFile(d.dir+d.tmpFile,buffer,function(err){ - if(err) return s.systemLog(err); - exec('alpr -j --config '+config.alprConfig+' -c '+d.mon.detector_lisence_plate_country+' '+d.dir+d.tmpFile,{encoding:'utf8'},(err, scan, stderr) => { - if(err){ - s.systemLog(err); - }else{ - try{ - scan=JSON.parse(scan.replace('--(!)Loaded CUDA classifier','').trim()) - }catch(err){ - if(!scan||!scan.results){ - return s.systemLog(scan,err); - } - } - if(scan.results.length>0){ - scan.plates=[] - scan.mats=[] - scan.results.forEach(function(v){ - v.candidates.forEach(function(g,n){ - if(v.candidates[n].matches_template) - delete(v.candidates[n].matches_template) - }) - scan.plates.push({coordinates:v.coordinates,candidates:v.candidates,confidence:v.confidence,plate:v.plate}) - var width = Math.sqrt( Math.pow(v.coordinates[1].x - v.coordinates[0].x, 2) + Math.pow(v.coordinates[1].y - v.coordinates[0].y, 2)); - var height = Math.sqrt( Math.pow(v.coordinates[2].x - v.coordinates[1].x, 2) + Math.pow(v.coordinates[2].y - v.coordinates[1].y, 2)) - scan.mats.push({ - x:v.coordinates[0].x, - y:v.coordinates[0].y, - width:width, - height:height, - tag:v.plate - }) - }) - tx({f:'trigger',id:d.id,ke:d.ke,details:{split:true,plug:config.plug,name:'licensePlate',reason:'object',matrices:scan.mats,imgHeight:d.mon.detector_scale_y,imgWidth:d.mon.detector_scale_x,frame:d.base64}}) - } - } - exec('rm -rf '+d.dir+d.tmpFile,{encoding:'utf8'}) - }) - }) -} -s.detectObject=function(buffer,d,tx){ - //detect license plate? - if(d.mon.detector_lisence_plate==="1"){ - s.detectLicensePlate(buffer,d,tx) - } - cv.imdecodeAsync(buffer,(err,im) => { - if(err){ - console.log(err) - return - } +// Base Init />> - if (!cv.xmodules.dnn) { - throw new Error('exiting: opencv4nodejs compiled without dnn module'); - } +const ObjectDetectors = require('./ObjectDetectors.js')(config); - // replace with path where you unzipped inception model - const inceptionModelPath = __dirname+'/data/inception'; - - - const modelFile = path.resolve(inceptionModelPath, 'tensorflow_inception_graph.pb'); - const classNamesFile = path.resolve(inceptionModelPath, 'imagenet_comp_graph_label_strings.txt'); - if (!fs.existsSync(modelFile) || !fs.existsSync(classNamesFile)) { - console.log('could not find inception model'); - console.log('download the model from: https://cdn.shinobi.video/weights/inception5h.zip'); - throw new Error('exiting'); - } - - // read classNames and store them in an array - const classNames = fs.readFileSync(classNamesFile).toString().split('\n'); - - // initialize tensorflow inception model from modelFile - const net = cv.readNetFromTensorflow(modelFile); - - // inception model works with 224 x 224 images, so we resize - // our input images and pad the image with white pixels to - // make the images have the same width and height - const maxImgDim = 224; - const white = new cv.Vec(255, 255, 255); - const imgResized = im.resizeToMax(maxImgDim).padToSquare(white); - - // network accepts blobs as input - const inputBlob = cv.blobFromImage(imgResized); - net.setInput(inputBlob); - - // forward pass input through entire network, will return - // classification result as 1xN Mat with confidences of each class - const outputBlob = net.forward(); - - // find all labels with a minimum confidence - const minConfidence = 0.05; - const locations = - outputBlob - .threshold(minConfidence, 1, cv.THRESH_BINARY) - .convertTo(cv.CV_8U) - .findNonZero(); -// locations.forEach(function(v){ -// console.log(v) -// }) - const result = - locations.map(pt => ({ - confidence: parseInt(outputBlob.at(0, pt.x) * 100) / 100, - className: classNames[pt.x] - })) - // sort result by confidence - .sort((r0, r1) => r1.confidence - r0.confidence) - .map(res => `${res.className} (${res.confidence})`); - console.log(result) - if(result.length > 0) { - s.cx({ +s.detectObject = function(buffer,d,tx,frameLocation,callback){ + new ObjectDetectors(buffer).process().then((resp)=>{ + var results = resp.data + if(results[0]){ + var mats = [] + results.forEach(function(v){ + mats.push({ + x: v.bbox[0], + y: v.bbox[1], + width: v.bbox[2], + height: v.bbox[3], + tag: v.class, + confidence: v.score, + }) + }) + var isObjectDetectionSeparate = d.mon.detector_pam === '1' && d.mon.detector_use_detect_object === '1' + var width = parseFloat(isObjectDetectionSeparate && d.mon.detector_scale_y_object ? d.mon.detector_scale_y_object : d.mon.detector_scale_y) + var height = parseFloat(isObjectDetectionSeparate && d.mon.detector_scale_x_object ? d.mon.detector_scale_x_object : d.mon.detector_scale_x) + tx({ f:'trigger', id:d.id, ke:d.ke, - name:'tensorflow', details:{ - plug:'tensorflow', - name:'tensorflow', + plug:config.plug, + name:'Tensorflow', reason:'object', - matrices : result - // confidence:d.average - }, - imgHeight:d.mon.detector_scale_y, - imgWidth:d.mon.detector_scale_x + matrices:mats, + imgHeight:width, + imgWidth:height, + time: resp.time + } }) - } - }) -} -s.systemLog=function(q,w,e){ - if(!w){w=''} - if(!e){e=''} - if(config.systemLog===true){ - return console.log(moment().format(),q,w,e) - } -} - -s.blenderRegion=function(d,cord,tx){ - d.width = d.image.width; - d.height = d.image.height; - if(!s.group[d.ke][d.id].canvas[cord.name]){ - if(!cord.sensitivity||isNaN(cord.sensitivity)){ - cord.sensitivity=d.mon.detector_sensitivity; - } - s.group[d.ke][d.id].canvas[cord.name] = new Canvas(d.width,d.height); - s.group[d.ke][d.id].canvasContext[cord.name] = s.group[d.ke][d.id].canvas[cord.name].getContext('2d'); - s.group[d.ke][d.id].canvasContext[cord.name].fillStyle = '#000'; - s.group[d.ke][d.id].canvasContext[cord.name].fillRect( 0, 0,d.width,d.height); - if(cord.points&&cord.points.length>0){ - s.group[d.ke][d.id].canvasContext[cord.name].beginPath(); - for (var b = 0; b < cord.points.length; b++){ - cord.points[b][0]=parseFloat(cord.points[b][0]); - cord.points[b][1]=parseFloat(cord.points[b][1]); - if(b===0){ - s.group[d.ke][d.id].canvasContext[cord.name].moveTo(cord.points[b][0],cord.points[b][1]); - }else{ - s.group[d.ke][d.id].canvasContext[cord.name].lineTo(cord.points[b][0],cord.points[b][1]); - } - } - s.group[d.ke][d.id].canvasContext[cord.name].clip(); - } - } - if(!s.group[d.ke][d.id].canvasContext[cord.name]){ - return - } - s.group[d.ke][d.id].canvasContext[cord.name].drawImage(d.image, 0, 0, d.width, d.height); - if(!s.group[d.ke][d.id].blendRegion[cord.name]){ - s.group[d.ke][d.id].blendRegion[cord.name] = new Canvas(d.width, d.height); - s.group[d.ke][d.id].blendRegionContext[cord.name] = s.group[d.ke][d.id].blendRegion[cord.name].getContext('2d'); - } - var sourceData = s.group[d.ke][d.id].canvasContext[cord.name].getImageData(0, 0, d.width, d.height); - // create an image if the previous image doesn�t exist - if (!s.group[d.ke][d.id].lastRegionImageData[cord.name]) s.group[d.ke][d.id].lastRegionImageData[cord.name] = s.group[d.ke][d.id].canvasContext[cord.name].getImageData(0, 0, d.width, d.height); - // create a ImageData instance to receive the blended result - var blendedData = s.group[d.ke][d.id].canvasContext[cord.name].createImageData(d.width, d.height); - // blend the 2 images - s.differenceAccuracy(blendedData.data,sourceData.data,s.group[d.ke][d.id].lastRegionImageData[cord.name].data); - // draw the result in a canvas - s.group[d.ke][d.id].blendRegionContext[cord.name].putImageData(blendedData, 0, 0); - // store the current webcam image - s.group[d.ke][d.id].lastRegionImageData[cord.name] = sourceData; - blendedData = s.group[d.ke][d.id].blendRegionContext[cord.name].getImageData(0, 0, d.width, d.height); - var i = 0; - d.average = 0; - while (i < (blendedData.data.length * 0.25)) { - d.average += (blendedData.data[i * 4] + blendedData.data[i * 4 + 1] + blendedData.data[i * 4 + 2]); - ++i; - } - d.average = (d.average / (blendedData.data.length * 0.25))*10; - if (d.average > parseFloat(cord.sensitivity)){ - if(d.mon.detector_use_detect_object==="1"&&d.mon.detector_second!=='1'){ - var buffer=s.group[d.ke][d.id].canvas[cord.name].toBuffer(); - s.detectObject(buffer,d,tx) - }else{ - tx({f:'trigger',id:d.id,ke:d.ke,details:{split:true,plug:config.plug,name:cord.name,reason:'motion',confidence:d.average,frame:d.base64}}) - } - } - s.group[d.ke][d.id].canvasContext[cord.name].clearRect(0, 0, d.width, d.height); - s.group[d.ke][d.id].blendRegionContext[cord.name].clearRect(0, 0, d.width, d.height); -} -function blobToBuffer (blob, cb) { - if (typeof Blob === 'undefined' || !(blob instanceof Blob)) { - throw new Error('first argument must be a Blob') - } - if (typeof cb !== 'function') { - throw new Error('second argument must be a function') - } - - var reader = new FileReader() - - function onLoadEnd (e) { - reader.removeEventListener('loadend', onLoadEnd, false) - if (e.error) cb(e.error) - else cb(null, Buffer.from(reader.result)) - } - - reader.addEventListener('loadend', onLoadEnd, false) - reader.readAsArrayBuffer(blob) -} -function fastAbs(value) { - return (value ^ (value >> 31)) - (value >> 31); -} - -function threshold(value) { - return (value > 0x15) ? 0xFF : 0; -} -s.differenceAccuracy=function(target, data1, data2) { - if (data1.length != data2.length) return null; - var i = 0; - while (i < (data1.length * 0.25)) { - var average1 = (data1[4 * i] + data1[4 * i + 1] + data1[4 * i + 2]) / 3; - var average2 = (data2[4 * i] + data2[4 * i + 1] + data2[4 * i + 2]) / 3; - var diff = threshold(fastAbs(average1 - average2)); - target[4 * i] = diff; - target[4 * i + 1] = diff; - target[4 * i + 2] = diff; - target[4 * i + 3] = 0xFF; - ++i; - } -} -s.checkAreas=function(d,tx){ - if(!s.group[d.ke][d.id].cords){ - if(!d.mon.cords){d.mon.cords={}} - s.group[d.ke][d.id].cords=Object.values(d.mon.cords); - } - if(d.mon.detector_frame==='1'){ - d.mon.cords.frame={name:'FULL_FRAME',s:d.mon.detector_sensitivity,points:[[0,0],[0,d.image.height],[d.image.width,d.image.height],[d.image.width,0]]}; - s.group[d.ke][d.id].cords.push(d.mon.cords.frame); - } - for (var b = 0; b < s.group[d.ke][d.id].cords.length; b++){ - if(!s.group[d.ke][d.id].cords[b]){return} - s.blenderRegion(d,s.group[d.ke][d.id].cords[b],tx) - } - delete(d.image) -} - -s.MainEventController=function(d,cn,tx){ - switch(d.f){ - case'refreshPlugins': - s.findCascades(function(cascades){ - s.cx({f:'s.tx',data:{f:'detector_cascade_list',cascades:cascades},to:'GRP_'+d.ke}) - }) - break; - case'readPlugins': - s.cx({f:'s.tx',data:{f:'detector_cascade_list',cascades:s.cascadesInDir},to:'GRP_'+d.ke}) - break; - case'init_plugin_as_host': - if(!cn){ - console.log('No CN',d) - return - } - if(d.key!==config.key){ - console.log(new Date(),'Plugin Key Mismatch',cn.request.connection.remoteAddress,d) - cn.emit('init',{ok:false}) - cn.disconnect() - }else{ - console.log(new Date(),'Plugin Connected to Client',cn.request.connection.remoteAddress) - cn.emit('init',{ok:true,plug:config.plug,notice:config.notice,type:config.type}) - } - break; - case'init_monitor': - if(s.group[d.ke]&&s.group[d.ke][d.id]){ - s.group[d.ke][d.id].canvas={} - s.group[d.ke][d.id].canvasContext={} - s.group[d.ke][d.id].blendRegion={} - s.group[d.ke][d.id].blendRegionContext={} - s.group[d.ke][d.id].lastRegionImageData={} - s.group[d.ke][d.id].numberOfTriggers=0 - delete(s.group[d.ke][d.id].cords) - delete(s.group[d.ke][d.id].buffer) - } - break; - case'init_aws_push': -// console.log('init_aws') - s.group[d.ke][d.id].aws={links:[],complete:0,total:d.total,videos:[],tx:tx} - break; - case'frame': - try{ - if(!s.group[d.ke]){ - s.group[d.ke]={} - } - if(!s.group[d.ke][d.id]){ - s.group[d.ke][d.id]={ - canvas:{}, - canvasContext:{}, - lastRegionImageData:{}, - blendRegion:{}, - blendRegionContext:{}, - } - } - if(!s.group[d.ke][d.id].buffer){ - s.group[d.ke][d.id].buffer=[d.frame]; - }else{ - s.group[d.ke][d.id].buffer.push(d.frame) - } - if(d.frame[d.frame.length-2] === 0xFF && d.frame[d.frame.length-1] === 0xD9){ - s.group[d.ke][d.id].buffer=Buffer.concat(s.group[d.ke][d.id].buffer); - try{ - d.mon.detector_cascades=JSON.parse(d.mon.detector_cascades) - }catch(err){ - - } - if(d.mon.detector_frame_save==="1"){ - d.base64=s.group[d.ke][d.id].buffer.toString('base64') - } - if(d.mon.detector_second==='1'&&d.objectOnly===true){ - s.detectObject(s.group[d.ke][d.id].buffer,d,tx) - }else{ - if((d.mon.detector_pam !== '1' && d.mon.detector_use_motion === "1") || d.mon.detector_use_detect_object !== "1"){ - if((typeof d.mon.cords ==='string')&&d.mon.cords.trim()===''){ - d.mon.cords=[] - }else{ - try{ - d.mon.cords=JSON.parse(d.mon.cords) - }catch(err){ - // console.log('d.mon.cords',err,d) - } - } - s.group[d.ke][d.id].cords=Object.values(d.mon.cords); - d.mon.cords=d.mon.cords; - d.image = new Canvas.Image; - if(d.mon.detector_scale_x===''||d.mon.detector_scale_y===''){ - s.systemLog('Must set detector image size') - return - }else{ - d.image.width=d.mon.detector_scale_x; - d.image.height=d.mon.detector_scale_y; - } - d.width=d.image.width; - d.height=d.image.height; - d.image.onload = function() { - s.checkAreas(d,tx); - } - d.image.src = s.group[d.ke][d.id].buffer; - }else{ - s.detectObject(s.group[d.ke][d.id].buffer,d,tx) - } - } - s.group[d.ke][d.id].buffer=null; - } - }catch(err){ - if(err){ - s.systemLog(err) - delete(s.group[d.ke][d.id].buffer) - } - } - break; - } -} -server.listen(config.hostPort); -//web pages and plugin api -app.get('/', function (req, res) { - res.end(''+config.plug+' for Shinobi is running') -}); -//Conector to Shinobi -if(config.mode==='host'){ - //start plugin as host - var io = require('socket.io')(server); - io.attach(server); - s.connectedClients={}; - io.on('connection', function (cn) { - s.connectedClients[cn.id]={id:cn.id} - s.connectedClients[cn.id].tx = function(data){ - data.pluginKey=config.key;data.plug=config.plug; - return io.to(cn.id).emit('ocv',data); - } - cn.on('f',function(d){ - s.MainEventController(d,cn,s.connectedClients[cn.id].tx) - }); - cn.on('disconnect',function(d){ - delete(s.connectedClients[cn.id]) - }) - }); -}else{ - //start plugin as client - if(!config.host){config.host='localhost'} - var io = require('socket.io-client')('ws://'+config.host+':'+config.port);//connect to master - s.cx=function(x){x.pluginKey=config.key;x.plug=config.plug;return io.emit('ocv',x)} - io.on('connect',function(d){ - s.cx({f:'init',plug:config.plug,notice:config.notice,type:config.type}); - }) - io.on('disconnect',function(d){ - io.connect(); - }) - io.on('f',function(d){ - s.MainEventController(d,null,s.cx) + } + callback() }) } diff --git a/plugins/yolo/.gitignore b/plugins/yolo/.gitignore new file mode 100644 index 00000000..2726a9cc --- /dev/null +++ b/plugins/yolo/.gitignore @@ -0,0 +1,3 @@ +conf.json +dist +models diff --git a/plugins/yolo/INSTALL.sh b/plugins/yolo/INSTALL.sh index b3c8c031..6772b4cf 100644 --- a/plugins/yolo/INSTALL.sh +++ b/plugins/yolo/INSTALL.sh @@ -1,4 +1,5 @@ #!/bin/bash +DIR=`dirname $0` echo "----------------------------------------" echo "-- Installing Yolo Plugin for Shinobi --" echo "----------------------------------------" @@ -21,6 +22,8 @@ fi # else # echo "OpenCV found... : $(opencv_version)" # fi +echo "If you get gcc/g++ build errors you can try running this then the installer again." +echo "bash <(curl -s https://gitlab.com/Shinobi-Systems/supplements/-/raw/master/downgradeGccG++.sh)" echo "=============" echo "Shinobi - Do you want to Install Tiny Weights?" echo "This is better for Graphics Cards with less than 4GB RAM" @@ -63,6 +66,9 @@ else echo "conf.json already exists..." fi echo "-----------------------------------" +echo "Adding Random Plugin Key to Main Configuration" +node $DIR/../../tools/modifyConfigurationForPlugin.js yolo key=$(head -c 64 < /dev/urandom | sha256sum | awk '{print substr($1,1,60)}') +echo "-----------------------------------" if [ -f /etc/redhat-release ]; then yum update yum install imagemagick -y diff --git a/plugins/yolo/shinobi-yolo.js b/plugins/yolo/shinobi-yolo.js index e04f67d9..1c518127 100644 --- a/plugins/yolo/shinobi-yolo.js +++ b/plugins/yolo/shinobi-yolo.js @@ -26,45 +26,45 @@ try{ var yolo = require('node-yolo-shinobi');//this is @vapi/node-yolo@1.2.4 without the console output for detection speed // var yolo = require('@vapi/node-yolo'); var detector = new yolo(__dirname + "/models", "cfg/coco.data", "cfg/yolov3.cfg", "yolov3.weights"); -s.detectObject=function(buffer,d,tx,frameLocation){ - var detectStuff = function(frame,callback){ - detector.detect(frame) - .then(detections => { - matrices = [] - detections.forEach(function(v){ - matrices.push({ - x:v.box.x, - y:v.box.y, - width:v.box.w, - height:v.box.h, - tag:v.className, - confidence:v.probability, - }) - }) - if(matrices.length > 0){ - tx({ - f:'trigger', - id:d.id, - ke:d.ke, - details:{ - plug:config.plug, - name:'yolo', - reason:'object', - matrices:matrices, - imgHeight:parseFloat(d.mon.detector_scale_y), - imgWidth:parseFloat(d.mon.detector_scale_x) - } - }) - } - fs.unlink(frame,function(){ +s.detectObject = async function(buffer,d,tx,frameLocation,callback){ + var timeStart = new Date() + var detectStuff = async function(frame){ + try{ + const detections = await detector.detect(frame) + matrices = [] + detections.forEach(function(v){ + matrices.push({ + x:v.box.x, + y:v.box.y, + width:v.box.w, + height:v.box.h, + tag:v.className, + confidence:v.probability, + }) + }) + if(matrices.length > 0){ + tx({ + f:'trigger', + id:d.id, + ke:d.ke, + details:{ + plug:config.plug, + name:'yolo', + reason:'object', + matrices:matrices, + imgHeight:parseFloat(d.mon.detector_scale_y), + imgWidth:parseFloat(d.mon.detector_scale_x), + time: (new Date()) - timeStart + } + }) + } + fs.unlink(frame,function(){ - }) - }) - .catch(error => { - console.log(error) - - // here you can handle the errors. Ex: Out of memory - }) + }) + }catch(err){ + console.log(err) + } + callback() } if(frameLocation){ detectStuff(frameLocation) diff --git a/sql/framework.sql b/sql/framework.sql index 3f307c2c..f3f116c1 100644 --- a/sql/framework.sql +++ b/sql/framework.sql @@ -95,6 +95,7 @@ CREATE TABLE IF NOT EXISTS `Users` ( `auth` varchar(50) DEFAULT NULL, `mail` varchar(100) DEFAULT NULL, `pass` varchar(100) DEFAULT NULL, + `accountType` int(1) DEFAULT '0', `details` longtext, UNIQUE KEY `mail` (`mail`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -110,7 +111,8 @@ CREATE TABLE IF NOT EXISTS `Videos` ( `size` float DEFAULT NULL, `frames` int(11) DEFAULT NULL, `end` timestamp NULL DEFAULT NULL, - `status` int(1) DEFAULT '0' COMMENT '0:Building,1:Complete,2:Read,3:Archive', + `status` int(1) DEFAULT '0', + `archived` int(1) DEFAULT '0', `details` text ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -157,6 +159,19 @@ CREATE TABLE IF NOT EXISTS `Timelapse Frames` ( `time` timestamp NULL DEFAULT NULL, `size` int(11) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +-- Dumping structure for table ccio.Timelapse Frames +CREATE TABLE IF NOT EXISTS `Cloud Timelapse Frames` (`ke` varchar(50) NOT NULL,`mid` varchar(50) NOT NULL,`href` text NOT NULL,`details` longtext,`filename` varchar(50) NOT NULL,`time` timestamp NULL DEFAULT NULL,`size` int(11) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Dumping structure for table ccio.Events Counts +CREATE TABLE IF NOT EXISTS `Events Counts` ( + `ke` varchar(50) NOT NULL, + `mid` varchar(50) NOT NULL, + `details` longtext NOT NULL, + `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `end` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `count` int(10) NOT NULL DEFAULT 1, + `tag` varchar(30) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Data exporting was unselected. /*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */; diff --git a/sql/postgresql/default_data.sql b/sql/postgresql/default_data.sql deleted file mode 100644 index 3cf0d482..00000000 --- a/sql/postgresql/default_data.sql +++ /dev/null @@ -1,20 +0,0 @@ --- -------------------------------------------------------- --- Host: 66.51.132.100 --- Server version: 5.7.16-0ubuntu0.16.04.1 - (Ubuntu) --- Server OS: Linux --- HeidiSQL Version: 9.3.0.4984 --- -------------------------------------------------------- - -/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; -/*!40101 SET NAMES utf8mb4 */; -/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; -/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; --- Dumping data for table ccio.Users: ~0 rows (approximately) -/*!40000 ALTER TABLE `Users` DISABLE KEYS */; -INSERT INTO Users (ke, uid, auth, mail, pass, details) VALUES - ('2Df5hBE', 'XDf5hB3', 'ec49f05c1ddc7d818c61b3343c98cbc6', 'ccio@m03.ca', '5f4dcc3b5aa765d61d8327deb882cf99', '{"days":"10"}'); -INSERT INTO Monitors (mid, ke, name, shto, shfr, details, type, ext, protocol, host, path, port, fps, mode, width, height) VALUES ('bunny', '2Df5hBE', 'Bunny', '[]', '[]', '{"fatal_max":"","notes":"","dir":"","rtsp_transport":"tcp","muser":"","mpass":"","port_force":"0","sfps":"","aduration":"1000000","probesize":"1000000","accelerator":"0","hwaccel":null,"hwaccel_vcodec":"","hwaccel_device":"","stream_type":"hls","stream_mjpeg_clients":"","stream_vcodec":"copy","stream_acodec":"no","hls_time":"","preset_stream":"","hls_list_size":"","signal_check":"","signal_check_log":null,"stream_quality":"","stream_fps":"1","stream_scale_x":"","stream_scale_y":"","rotate_stream":null,"svf":"","stream_timestamp":"0","stream_timestamp_font":"","stream_timestamp_font_size":"","stream_timestamp_color":"","stream_timestamp_box_color":"","stream_timestamp_x":"","stream_timestamp_y":"","stream_watermark":"0","stream_watermark_location":"","stream_watermark_position":null,"snap":"1","snap_fps":"","snap_scale_x":"","snap_scale_y":"","snap_vf":"","vcodec":"copy","crf":"","preset_record":"","acodec":"libvorbis","dqf":null,"cutoff":"10","rotate_record":null,"vf":"","timestamp":"1","timestamp_font":"","timestamp_font_size":"","timestamp_color":"","timestamp_box_color":"","timestamp_x":"","timestamp_y":"","watermark":null,"watermark_location":"","watermark_position":null,"cust_input":"","cust_snap":"","cust_detect":"","cust_stream":"","cust_stream_server":"","cust_record":"","custom_output":"","detector":"0","detector_webhook":null,"detector_webhook_url":"","detector_command_enable":null,"detector_command":"","detector_command_timeout":"","detector_lock_timeout":"","detector_save":null,"detector_frame_save":null,"detector_mail":null,"detector_mail_timeout":"","detector_record_method":null,"detector_trigger":null,"detector_trigger_record_fps":"","detector_timeout":"","watchdog_reset":null,"detector_delete_motionless_videos":null,"detector_send_frames":null,"detector_fps":"","detector_scale_x":"","detector_scale_y":"","detector_use_motion":null,"detector_use_detect_object":null,"detector_frame":null,"detector_sensitivity":"","cords":"","detector_lisence_plate":null,"detector_lisence_plate_country":null,"detector_notrigger":null,"detector_notrigger_mail":null,"detector_notrigger_timeout":"","control":"0","control_base_url":"","control_stop":null,"control_url_stop_timeout":"","control_url_center":"","control_url_left":"","control_url_left_stop":"","control_url_right":"","control_url_right_stop":"","control_url_up":"","control_url_up_stop":"","control_url_down":"","control_url_down_stop":"","control_url_enable_nv":"","control_url_disable_nv":"","control_url_zoom_out":"","control_url_zoom_out_stop":"","control_url_zoom_in":"","control_url_zoom_in_stop":"","groups":"","loglevel":"warning","sqllog":"0","detector_cascades":""}', 'mjpeg', 'mp4', 'http', 'came3.nkansai.ne.jp', '/nphMotionJpeg?Resolution=640x480&Quality=Motion', 81, 15, 'start', 640, 480); -/*!40000 ALTER TABLE `Users` ENABLE KEYS */; -/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */; -/*!40014 SET FOREIGN_KEY_CHECKS=IF(@OLD_FOREIGN_KEY_CHECKS IS NULL, 1, @OLD_FOREIGN_KEY_CHECKS) */; -/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; diff --git a/sql/postgresql/framework.pgsql b/sql/postgresql/framework.pgsql new file mode 100644 index 00000000..a37d46d3 --- /dev/null +++ b/sql/postgresql/framework.pgsql @@ -0,0 +1,218 @@ +/* + * PostgresSQL rewrite of framework.sql - dave@dream-tech.com + * Placed into open source, no license required here unless you want one, licenses and lawyers + * are the primary bane of good software development. :) + * + * Trigger code lifted from stack overflow here: + * https://stackoverflow.com/questions/9556474/how-do-i-automatically-update-a-timestamp-in-postgresql + * + * Summary of changes: + * a) Removed mysql cruft and comments, no need for 'use' + * b) Removed create database statement (I can put one back but usually I create dbs using postgres command line tools: + * e.g. 'createdb foo') + * c) Removed all cases of int(\d+) and replaced with just int, postgres does not support those + * d) Removed ENGINE=InnoDB + * e) Removed default charset statements, Postgresql automatically supports 4-byte UTF8 at database createion + * f) Removed backtick quotes and added double quotes + * g) All timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP replaced with triggers + * and the ON UPDATE portion removed as postgres doesn't support this sadly + * h) tinytext/longtest is changed to just text, generally postgres does a good job of managing arbitrary text columns + * i) Enums created the Postgres way by creating a type + * + * Here's my DB create flow: + * 1) become the account that controls pgsql (pgsql superuser) + * 2) from that shell prompt, say: + * createuser -p shinobi + * Enter a secure password after this, twice. + * 3) from same shell prompt say: + * createdb --owner shinobi --encoding='utf-8' shinobi + * 4) now from same shell prompt you can do + * psql shinobi obj && typeof obj === 'object'; + + return objects.reduce((prev, obj) => { + Object.keys(obj).forEach(key => { + const pVal = prev[key]; + const oVal = obj[key]; + + if (Array.isArray(pVal) && Array.isArray(oVal)) { + prev[key] = pVal.concat(...oVal); + } + else if (isObject(pVal) && isObject(oVal)) { + prev[key] = mergeDeep(pVal, oVal); + } + else { + prev[key] = oVal; + } + }); + + return prev; + }, {}); +} + processArgv.forEach(function(val) { var theSplit = val.split('='); - var index = theSplit[0]; + var index = (theSplit[0] || '').trim(); var value = theSplit[1]; - if(index.indexOf('addToConfig') > -1){ + if(index.indexOf('addToConfig') > -1 || index == 'addToConfig'){ try{ value = JSON.parse(value) - config = Object.assign(config,value) + config = mergeDeep(config,value) }catch(err){ console.log('Not a valid Data set. "addToConfig" value must be a JSON string. You may need to wrap it in singles quotes.') } @@ -33,7 +56,20 @@ processArgv.forEach(function(val) { console.log(index + ': ' + value); }); -jsonfile.writeFile(configLocation,config,{spaces: 2},function(){ +try{ + if(config.thisIsDocker){ + const dockerConfigFile = '/config/conf.json' + fs.stat(dockerConfigFile,(err) => { + if(!err){ + fs.writeFile(dockerConfigFile,JSON.stringify(config,null,3),function(){}) + } + }) + } +}catch(err){ + console.log(err) +} + +fs.writeFile(configLocation,JSON.stringify(config,null,3),function(){ console.log('Changes Complete. Here is what it is now.') console.log(JSON.stringify(config,null,2)) }) diff --git a/tools/modifyConfigurationForPlugin.js b/tools/modifyConfigurationForPlugin.js new file mode 100644 index 00000000..b0276aff --- /dev/null +++ b/tools/modifyConfigurationForPlugin.js @@ -0,0 +1,129 @@ +var fs = require('fs'); +var execSync = require('child_process').execSync; +const getConfLocation = () => { + let chosenLocation + try{ + chosenLocation = __dirname + `/../plugins/${targetedPlugin}/` + fs.statSync(chosenLocation) + }catch(err){ + chosenLocation = __dirname + `/` + } + return chosenLocation +} +const mergeDeep = function(...objects) { + const isObject = obj => obj && typeof obj === 'object'; + + return objects.reduce((prev, obj) => { + Object.keys(obj).forEach(key => { + const pVal = prev[key]; + const oVal = obj[key]; + + if (Array.isArray(pVal) && Array.isArray(oVal)) { + prev[key] = pVal.concat(...oVal); + } + else if (isObject(pVal) && isObject(oVal)) { + prev[key] = mergeDeep(pVal, oVal); + } + else { + prev[key] = oVal; + } + }); + + return prev; + }, {}); +} +var anError = function(message,dontShowExample){ + console.log(message) + if(!dontShowExample){ + console.log('Example of usage :') + console.log('node tools/modifyConfigurationForPlugin.js tensorflow key=1234asdfg port=8080') + } +} +var testValueForObject = function(jsonString){ + var newValue = jsonString + '' + try{ + newValue = JSON.parse(jsonString) + }catch(err){ + + } + if(typeof newValue === 'object'){ + return true + } + return false +} +process.on('uncaughtException', function (err) { + console.error('Uncaught Exception occured!'); + console.error(err.stack); +}); +var targetedPlugin = process.argv[2] +if(!targetedPlugin || targetedPlugin === '' || targetedPlugin.indexOf('=') > -1){ + return anError('Specify a plugin folder name as the first argument.') +} +var pluginLocation = getConfLocation() +fs.stat(pluginLocation,function(err){ + if(!err){ + var configLocation = `${pluginLocation}conf.json` + try{ + var config = JSON.parse(fs.readFileSync(configLocation)) + }catch(err){ + try{ + var config = fs.readFileSync(`${pluginLocation}conf.sample.json`,'utf8') + fs.writeFileSync(`${pluginLocation}conf.json`,JSON.stringify(config,null,3),'utf8') + }catch(err){ + var config = {} + } + } + var processArgv = process.argv.splice(3,process.argv.length) + var arguments = {}; + if(processArgv.length === 0){ + return anError('No changes made. Add arguments to add or modify.') + } + processArgv.forEach(function(val) { + var theSplit = val.split('='); + var index = (theSplit[0] || '').trim(); + var value = theSplit[1]; + if(index.indexOf('addToConfig') > -1 || index == 'addToConfig'){ + try{ + value = JSON.parse(value) + config = mergeDeep(config,value) + }catch(err){ + anError('Not a valid Data set. "addToConfig" value must be a JSON string. You may need to wrap it in singles quotes.') + } + }else{ + if(value==='DELETE'){ + delete(config[index]) + }else{ + if(testValueForObject(value)){ + config[index] = JSON.parse(value); + }else{ + if(index === 'key'){ + const modifyMainFileLocation = `${__dirname}/modifyConfiguration.js` + fs.stat(modifyMainFileLocation,(err) => { + if(!err){ + console.log(`Updating main conf.json with new key.`) + execSync(`node ${modifyMainFileLocation} addToConfig='{"pluginKeys":{"${config.plug}":"${value + ''}"}}'`,function(err){ + console.log(err) + }) + }else{ + console.log(`Didn't find main conf.json. You may need to update it manually.`) + console.log(`Docker users using the official Ninja-Docker install method don't need to complete any other configuration.`) + } + }) + config[index] = value + '' + }else{ + config[index] = value + } + } + } + } + console.log(index + ': ' + value); + }); + + fs.writeFile(configLocation,JSON.stringify(config,null,3),function(){ + console.log('Changes Complete. Here is what it is now.') + console.log(JSON.stringify(config,null,2)) + }) + }else{ + anError(`Plugin "${targetedPlugin}" not found.`) + } +}) diff --git a/web/libs/css/dash2.basic.css b/web/libs/css/dash2.basic.css index 15bc10bc..4eadd5da 100644 --- a/web/libs/css/dash2.basic.css +++ b/web/libs/css/dash2.basic.css @@ -46,6 +46,13 @@ background-clip: padding-box; } /**/ +.json-to-block div > div{ + margin-left:10px; +} +.json-to-block.striped div > div{ + padding-left:10px; + border-left: 2px solid #fff; +} .flex{display:flex} .flex>div{flex:1} .flex-block{display:inline-flex;width:100%;flex-flow: row wrap;} @@ -149,7 +156,32 @@ img{max-width:100%} .follow-list ul{padding:0;margin:0;font-family:"Roboto","Helvetica","Arial",sans-serif;} -.follow-list ul a:not(.btn){color:#fff} +.follow-list ul a:not(.btn){ + color:#fff; + font-weight: 300; +} +/* .follow-list .affix, +.follow-list .affix-top + { + width: 100%; +} */ +.follow-list .dot {margin-right: 15px;} +.dot { + width:10px; + height:10px; + display:inline-block; + border-radius: 50%; +} + +.dot-red {background:#d9534f} +.dot-purple {background:#3f51b5} +.dot-blue {background:#375182} +.dot-navy {background:#0858ab} +.dot-green {background:#449d44} +.dot-forestgreen {background:#1e4046} +.dot-orange {background:#c49a68} +.dot-grey {background:#777} + .os_bars{width:600px;display:inline-block;padding:5px 0 0 10px} @media screen and (max-width: 600px){ .os_bars{width:200px;} @@ -433,6 +465,15 @@ ul.msg_list li .message { letter-spacing: 3pt; font-size: 8pt; } +.text-white { + color: #fff!important; +} +.text-purple { + color: #576fff!important; +} +.cursor-pointer { + cursor: pointer!important; +} /* Start of custom table sorter */ .table .table-header-sorter { cursor: pointer; @@ -445,3 +486,66 @@ ul.msg_list li .message { margin-left: 10px; } /* End of custom table sorter */ + +.row-flex { + display: flex; +} + +.row-flex-full-height { + display: flex; + height: 100%; +} + +.row-flex [class*="col-"]{ + height: 100%; + float: none; + overflow: auto; +} + +.row-flex .col-md-1{ + flex: 1; +} + +.row-flex .col-md-2{ + flex: 2; +} + +.row-flex .col-md-3{ + flex: 3; +} + +.row-flex .col-md-4{ + flex: 4; +} + +.row-flex .col-md-5{ + flex: 5; +} + +.row-flex .col-md-6{ + flex: 6; +} + +.row-flex .col-md-7{ + flex: 7; +} + +.row-flex .col-md-8{ + flex: 8; +} + +.row-flex .col-md-9{ + flex: 9; +} + +.row-flex .col-md-10{ + flex: 10; +} + +.row-flex .col-md-11{ + flex: 11; +} + +.row-flex .col-md-12{ + flex: 12; +} diff --git a/web/libs/css/dash2.darktheme.css b/web/libs/css/dash2.darktheme.css index 85917506..513560ea 100644 --- a/web/libs/css/dash2.darktheme.css +++ b/web/libs/css/dash2.darktheme.css @@ -41,5 +41,11 @@ } .dark .slider-selection { - background:#555; + background: #375182; +} +.dark .slider-handle { + background: #375182; +} +.dark .slider-track { + background: #333; } diff --git a/web/libs/css/dash2.forms.css b/web/libs/css/dash2.forms.css index ba885a18..5f01fc49 100644 --- a/web/libs/css/dash2.forms.css +++ b/web/libs/css/dash2.forms.css @@ -6,7 +6,7 @@ form.modal-body{margin:0} .form-group label>div:nth-child(2){width:70%;padding:5px;border:1px solid #dedede;border-radius:5px} .dark .form-group label>div,.dark .form-group label>div>span{border-color:#454545;color:#fff} .important.form-group label>div:nth-child(2),.important.form-group label>div>span{border-color:red} -.form-group label span small{margin-left: 2px;display:block;font-weight: 600;} +.form-group label span small{margin-left: 2px;display:block;font-weight: 400;} .form-group-group .round-left{border-radius: 50px 0 0 50px;margin-left:10px} .form-group-group blockquote:before,.form-group-group blockquote:after{display:none!important} .form-group-group blockquote{letter-spacing:normal;font-style:normal} @@ -23,17 +23,29 @@ form.modal-body{margin:0} .dark .form-group-group .mdl-list__item{color:#fff;border-bottom:1px solid #444;} .dark .form-group-group .mdl-list__item:hover{background:#555;} .form-group-group:visible:last-child,.form-group-group > .form-group:last-child{margin-bottom:0} -.form-group-group > h4{margin:0 -10px 15px -10px;padding:15px;background:#ddd;} +.form-group-group > h4{ + margin:0 -10px 15px -10px; + padding:15px; + background:#ddd; + font-family: sans-serif; + text-transform: uppercase; + letter-spacing: 3pt; + font-size: 8pt; +} +.form-group-group > h4 .btn{ + text-transform: none; + letter-spacing: initial; +} .form-group-group > h4:empty{padding:2px} .form-group-group > h4 small{color:#fff;} .form-group-group.red{border-color:#d9534f} .form-group-group.red > h4{background:#d9534f;color:#fff} .form-group-group.purple{border-color:#3f51b5} .form-group-group.purple > h4{background:#3f51b5;color:#fff} -.form-group-group.blue{border-color:#337ab7} -.form-group-group.blue > h4{background:#337ab7;color:#fff} -.form-group-group.navy{border-color:#31708f} -.form-group-group.navy > h4{background:#31708f;color:#fff} +.form-group-group.blue{border-color:#375182} +.form-group-group.blue > h4{background:#375182;color:#fff} +.form-group-group.navy{border-color:#0858ab} +.form-group-group.navy > h4{background:#0858ab;color:#fff} .form-group-group.green{border-color:#449d44} .form-group-group.green > h4{background:#449d44;color:#fff} .form-group-group.forestgreen{border-color:#1e4046} @@ -44,3 +56,6 @@ form.modal-body{margin:0} .form-group-group.grey > h4{background:#777;color:#fff} .dark .form-group-group{background:#222} .form-group-group:last-child {margin-bottom: 0} + +.btn-group-justified {display: flex} +.btn-group-justified .btn {flex: 1} diff --git a/web/libs/css/dash2.monitoredit.css b/web/libs/css/dash2.monitoredit.css new file mode 100644 index 00000000..c713661a --- /dev/null +++ b/web/libs/css/dash2.monitoredit.css @@ -0,0 +1,17 @@ +#add_monitor .form-group-group > h4{ + cursor: pointer; +} +#add_monitor .form-group-group > h4 small{ + cursor: pointer; + font-size: 8pt; +} +#add_monitor .hide-box-wrapper.form-group-group > h4{ + margin: 0; +} +#add_monitor .hide-box-wrapper.form-group-group{ + padding: 0; +} +#add_monitor .hide-box-wrapper .box-wrapper{ + height: 0; + overflow: hidden; +} diff --git a/web/libs/css/dash2.monitors.css b/web/libs/css/dash2.monitors.css index 79535873..14ba3456 100644 --- a/web/libs/css/dash2.monitors.css +++ b/web/libs/css/dash2.monitors.css @@ -101,3 +101,23 @@ img.circle-img,div.circle-img{border-radius:50%;height:50px;width:50px} .stream-objects .stream-detected-object{position:absolute;top:0;left:0;border:3px solid red;background:transparent;border-radius:5px} .stream-objects .stream-detected-point{position:absolute;top:0;left:0;border:3px solid yellow;background:transparent;border-radius:5px} .stream-objects .point{position:absolute;top:0;left:0;border:3px solid red;border-radius:50%} + +.monitor_item .gps-map { + position: absolute; + width: 190px; + height: 190px; + border-radius: 50%; + border: 1px solid #333; + z-index: 9; + bottom: 10px; + right: 10px; +} +.monitor_item .gps-map-details { + position: absolute; + padding: 5px 7px; + border-radius: 25px; + background:rgba(0,0,0,0.5); + z-index: 11; + top: 10px; + right: 10px; +} diff --git a/web/libs/css/dash2.powerVideo2.css b/web/libs/css/dash2.powerVideo2.css index c90e2fdb..59973929 100644 --- a/web/libs/css/dash2.powerVideo2.css +++ b/web/libs/css/dash2.powerVideo2.css @@ -41,6 +41,7 @@ position: absolute; padding: 20px 10px 20px 10px; height: 100%; + width: 100%; top: 0; left: 0; margin: auto; diff --git a/web/libs/css/dash2.powerVideoOld.css b/web/libs/css/dash2.powerVideoOld.css new file mode 100755 index 00000000..289d3969 --- /dev/null +++ b/web/libs/css/dash2.powerVideoOld.css @@ -0,0 +1,18 @@ +#powerVideo iframe{border:0;width:100%;height:350px;margin-bottom:10px;overflow:hidden} +#powerVideo video{max-height:300px;max-width:100%;} +#powerVideo .holder{height:300px;} +#powerVideo h3{margin-top:0} +#powerVideo .progressBar{position:relative;} +#powerVideo .bufferBar{position:absolute;left:0;top:0;opacity:0.4} +#powerVideo .timeBar{position:relative;z-index: 222;background:transparent} +#powerVideo h3{font-family:monospace} + + +#vis_pwrvideo{height:250px} + + +#vis_monitors{overflow:auto;max-height:400px} +#vis_monitors .btn-group-vertical{width:100%} + + +#motion_list{height:155px;overflow:auto;border-radius:5px;border:1px solid #444;position:relative;background: #222;margin:0} diff --git a/web/libs/css/dash2.ptzcontrols.css b/web/libs/css/dash2.ptzcontrols.css index 0ecee292..e2c06c96 100644 --- a/web/libs/css/dash2.ptzcontrols.css +++ b/web/libs/css/dash2.ptzcontrols.css @@ -13,9 +13,6 @@ position: relative; height: 120px; width: 120px; - background: #b7b7b7; - border-radius: 50%; - box-shadow: inset 0 0 1px rgba(120, 120, 120, 0.6), inset 0 2px 2px rgba(0, 0, 0, 0.1), 0 2px 2px rgba(240, 240, 240, 0.4); } .PTZ_controls .control { position: absolute; @@ -23,9 +20,10 @@ .PTZ_controls .pad .control { height: 30px; width: 30px; - background: #636363; - box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.6), 0 0 0 3px rgba(60, 60, 60, 0.2), 0 0 0 4px rgba(60, 60, 60, 0.2); + background: #101f54; + box-shadow: 0 0 15px rgba(0,0,0,0.4); border-radius: 2px; + cursor: pointer; } .PTZ_controls .zoom_in{ top: 0; @@ -44,25 +42,29 @@ right: 0; } .PTZ_controls .pad .top { - top: 15px; left: 50%; margin: 0 0 0 -15px; + top: 10px; + border-radius: 10px 10px 50px 50px; } .PTZ_controls .pad .left { top: 45px; - left: 15px; + left: 10px; + border-radius: 10px 50px 50px 10px; } .PTZ_controls .pad .right { top: 45px; - right: 15px; + right: 10px; + border-radius: 50px 10px 10px 50px; } .PTZ_controls .pad .control.right:before { transform: rotate(90deg) translate(-3px, -5px); } .PTZ_controls .pad .bottom { - bottom: 15px; left: 50%; margin: 0 0 0 -15px; + bottom: 10px; + border-radius: 50px 50px 10px 10px; } /* Overlap the other controls to hide box-shadow */ .PTZ_controls .pad .middle { @@ -72,18 +74,6 @@ top: 43px; left: 50%; margin: 0 0 0 -17px; - box-shadow: none; - border-radius: 3px; -} -.PTZ_controls .pad .middle:after { - position: absolute; - top: 50%; - left: 50%; - margin: -35% 0 0 -35%; - content: ''; - background: #636363; - height: 70%; - width: 70%; - border-radius: 100%; - box-shadow: inset 0 0 2px rgba(120, 120, 120, 0.6), inset 0 2px 8px rgba(0, 0, 0, 0.1), 0 2px 2px rgba(240, 240, 240, 0.2); + border-radius: 50%; + background: #1f396c; } diff --git a/web/libs/css/dash2.sidemenu.css b/web/libs/css/dash2.sidemenu.css index 148f9e93..b80387c2 100644 --- a/web/libs/css/dash2.sidemenu.css +++ b/web/libs/css/dash2.sidemenu.css @@ -5,15 +5,15 @@ padding: 0; } -.side-menu .monitor_block{padding:0;position:relative} -.side-menu .monitor_block img{width:100%;height:75px;cursor:pointer;border: 0.5px inset #263238;} +.side-menu .monitor_block{padding:2px;position:relative} +.side-menu .monitor_block img{width:100%;height:75px;cursor:pointer;border: 0.5px solid #003d71;border-radius:4px;} @media screen and (max-width:1025px){ .side-menu .monitor_block img{height:175px;} } .side-menu .monitor_block:hover .icons{opacity:1} .side-menu .monitor_block:hover .title{opacity:1} -.side-menu .monitor_block .icons,.side-menu .monitor_block .title{opacity:0;width:100%;bottom:0;left:0;background:rgba(0,0,0,0.6);position:absolute;padding:2.5px;z-index:11;cursor:move} -.side-menu .monitor_block .title{bottom:auto;top:0;color:#fff} +.side-menu .monitor_block .icons,.side-menu .monitor_block .title{opacity:0;width:calc(100% - 4px);bottom:2px;left:2px;background:rgba(0,0,0,0.6);position:absolute;padding:2.5px;z-index:11;cursor:move;border-radius:4px} +.side-menu .monitor_block .title{bottom:auto;top:2px;color:#fff} .nav-xs.side-menu .monitor_block{width:100%} .side-menu .monitor_block .list-data{display:none} .output_data:empty{display:none} diff --git a/web/libs/css/dash2.timelapse.jpeg.css b/web/libs/css/dash2.timelapse.jpeg.css index ebb06e93..56f1ea99 100644 --- a/web/libs/css/dash2.timelapse.jpeg.css +++ b/web/libs/css/dash2.timelapse.jpeg.css @@ -58,6 +58,7 @@ } #timelapsejpeg .playBackView img{ height:100%; + border-radius: 4px; } #timelapsejpeg .liveStreamView{ position:absolute; diff --git a/web/libs/css/super.configEditor.css b/web/libs/css/super.configEditor.css new file mode 100644 index 00000000..040574b0 --- /dev/null +++ b/web/libs/css/super.configEditor.css @@ -0,0 +1,66 @@ +.better-json-editor h3 { + font-size: initial; + margin: 10px 10px 20px 0; +} +.better-json-editor h3 > span{ + color: #bd4147; + background-color: #f8f9fa; + padding: 7px 10px; + border-radius: .25rem; + font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + margin-bottom: 5px; +} +.better-json-editor .well .well { + margin-left:20px; +} +.better-json-editor label, .better-json-editor .control-label { + color: #bd4147; + background-color: #f8f9fa; + padding: 7px 10px; + border-radius: .25rem; + font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + margin-bottom:5px; +} +.better-json-editor .floating-json { + padding: 10px; + border-radius: 5px; + border: 1px solid #E3E3E3; +} +.better-json-editor .form-group:nth-last-child(2) { + margin-bottom:0; +} +.better-json-editor table { + width: 100%!important; + margin-bottom: 10px; + overflow: hidden; + border-radius: 5px; + background: #fbfbfb; +} +.better-json-editor .floating-json textarea.form-control { + padding: 20px; + border-radius: 5px; + border: 1px solid #E3E3E3; + margin-bottom: 10px; + font-family: monospace; +} +.better-json-editor [class*="json-editor-btn"].badge { + margin: 0; + margin-right: 5px; +} +.better-json-editor .badge { + cursor: pointer; + outline: none; +} +.better-json-editor .row { + margin: 0; +} +.better-json-editor .row p:visible:last-child{ + margin: 0; +} +.better-json-editor .row > div { + border: 1px solid #eee; + border-radius: 5px; + padding-top: 15px; + padding-bottom: 15px; + margin-bottom: 10px; +} diff --git a/web/libs/css/super.customAutoLoad.css b/web/libs/css/super.customAutoLoad.css new file mode 100644 index 00000000..f6c3fde5 --- /dev/null +++ b/web/libs/css/super.customAutoLoad.css @@ -0,0 +1,12 @@ +#customAutoLoadList [package-name] .card-body{ + min-height:auto +} +#customAutoLoadList [package-name] .install-output-stdout, +#customAutoLoadList [package-name] .install-output-stderr +{ + max-height: 300px; + background: ##f7f7f7; + border-radius: 15px; + padding: 5px; + margin:0; +} diff --git a/web/libs/js/Chart.js b/web/libs/js/Chart.js new file mode 100755 index 00000000..06857c07 --- /dev/null +++ b/web/libs/js/Chart.js @@ -0,0 +1,12757 @@ +/*! + * Chart.js + * http://chartjs.org/ + * Version: 2.6.0 + * + * Copyright 2017 Nick Downie + * Released under the MIT license + * https://github.com/chartjs/Chart.js/blob/master/LICENSE.md + */ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Chart = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o lum2) { + return (lum1 + 0.05) / (lum2 + 0.05); + } + return (lum2 + 0.05) / (lum1 + 0.05); + }, + + level: function (color2) { + var contrastRatio = this.contrast(color2); + if (contrastRatio >= 7.1) { + return 'AAA'; + } + + return (contrastRatio >= 4.5) ? 'AA' : ''; + }, + + dark: function () { + // YIQ equation from http://24ways.org/2010/calculating-color-contrast + var rgb = this.values.rgb; + var yiq = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000; + return yiq < 128; + }, + + light: function () { + return !this.dark(); + }, + + negate: function () { + var rgb = []; + for (var i = 0; i < 3; i++) { + rgb[i] = 255 - this.values.rgb[i]; + } + this.setValues('rgb', rgb); + return this; + }, + + lighten: function (ratio) { + var hsl = this.values.hsl; + hsl[2] += hsl[2] * ratio; + this.setValues('hsl', hsl); + return this; + }, + + darken: function (ratio) { + var hsl = this.values.hsl; + hsl[2] -= hsl[2] * ratio; + this.setValues('hsl', hsl); + return this; + }, + + saturate: function (ratio) { + var hsl = this.values.hsl; + hsl[1] += hsl[1] * ratio; + this.setValues('hsl', hsl); + return this; + }, + + desaturate: function (ratio) { + var hsl = this.values.hsl; + hsl[1] -= hsl[1] * ratio; + this.setValues('hsl', hsl); + return this; + }, + + whiten: function (ratio) { + var hwb = this.values.hwb; + hwb[1] += hwb[1] * ratio; + this.setValues('hwb', hwb); + return this; + }, + + blacken: function (ratio) { + var hwb = this.values.hwb; + hwb[2] += hwb[2] * ratio; + this.setValues('hwb', hwb); + return this; + }, + + greyscale: function () { + var rgb = this.values.rgb; + // http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale + var val = rgb[0] * 0.3 + rgb[1] * 0.59 + rgb[2] * 0.11; + this.setValues('rgb', [val, val, val]); + return this; + }, + + clearer: function (ratio) { + var alpha = this.values.alpha; + this.setValues('alpha', alpha - (alpha * ratio)); + return this; + }, + + opaquer: function (ratio) { + var alpha = this.values.alpha; + this.setValues('alpha', alpha + (alpha * ratio)); + return this; + }, + + rotate: function (degrees) { + var hsl = this.values.hsl; + var hue = (hsl[0] + degrees) % 360; + hsl[0] = hue < 0 ? 360 + hue : hue; + this.setValues('hsl', hsl); + return this; + }, + + /** + * Ported from sass implementation in C + * https://github.com/sass/libsass/blob/0e6b4a2850092356aa3ece07c6b249f0221caced/functions.cpp#L209 + */ + mix: function (mixinColor, weight) { + var color1 = this; + var color2 = mixinColor; + var p = weight === undefined ? 0.5 : weight; + + var w = 2 * p - 1; + var a = color1.alpha() - color2.alpha(); + + var w1 = (((w * a === -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0; + var w2 = 1 - w1; + + return this + .rgb( + w1 * color1.red() + w2 * color2.red(), + w1 * color1.green() + w2 * color2.green(), + w1 * color1.blue() + w2 * color2.blue() + ) + .alpha(color1.alpha() * p + color2.alpha() * (1 - p)); + }, + + toJSON: function () { + return this.rgb(); + }, + + clone: function () { + // NOTE(SB): using node-clone creates a dependency to Buffer when using browserify, + // making the final build way to big to embed in Chart.js. So let's do it manually, + // assuming that values to clone are 1 dimension arrays containing only numbers, + // except 'alpha' which is a number. + var result = new Color(); + var source = this.values; + var target = result.values; + var value, type; + + for (var prop in source) { + if (source.hasOwnProperty(prop)) { + value = source[prop]; + type = ({}).toString.call(value); + if (type === '[object Array]') { + target[prop] = value.slice(0); + } else if (type === '[object Number]') { + target[prop] = value; + } else { + console.error('unexpected color value:', value); + } + } + } + + return result; + } +}; + +Color.prototype.spaces = { + rgb: ['red', 'green', 'blue'], + hsl: ['hue', 'saturation', 'lightness'], + hsv: ['hue', 'saturation', 'value'], + hwb: ['hue', 'whiteness', 'blackness'], + cmyk: ['cyan', 'magenta', 'yellow', 'black'] +}; + +Color.prototype.maxes = { + rgb: [255, 255, 255], + hsl: [360, 100, 100], + hsv: [360, 100, 100], + hwb: [360, 100, 100], + cmyk: [100, 100, 100, 100] +}; + +Color.prototype.getValues = function (space) { + var values = this.values; + var vals = {}; + + for (var i = 0; i < space.length; i++) { + vals[space.charAt(i)] = values[space][i]; + } + + if (values.alpha !== 1) { + vals.a = values.alpha; + } + + // {r: 255, g: 255, b: 255, a: 0.4} + return vals; +}; + +Color.prototype.setValues = function (space, vals) { + var values = this.values; + var spaces = this.spaces; + var maxes = this.maxes; + var alpha = 1; + var i; + + this.valid = true; + + if (space === 'alpha') { + alpha = vals; + } else if (vals.length) { + // [10, 10, 10] + values[space] = vals.slice(0, space.length); + alpha = vals[space.length]; + } else if (vals[space.charAt(0)] !== undefined) { + // {r: 10, g: 10, b: 10} + for (i = 0; i < space.length; i++) { + values[space][i] = vals[space.charAt(i)]; + } + + alpha = vals.a; + } else if (vals[spaces[space][0]] !== undefined) { + // {red: 10, green: 10, blue: 10} + var chans = spaces[space]; + + for (i = 0; i < space.length; i++) { + values[space][i] = vals[chans[i]]; + } + + alpha = vals.alpha; + } + + values.alpha = Math.max(0, Math.min(1, (alpha === undefined ? values.alpha : alpha))); + + if (space === 'alpha') { + return false; + } + + var capped; + + // cap values of the space prior converting all values + for (i = 0; i < space.length; i++) { + capped = Math.max(0, Math.min(maxes[space][i], values[space][i])); + values[space][i] = Math.round(capped); + } + + // convert to all the other color spaces + for (var sname in spaces) { + if (sname !== space) { + values[sname] = convert[space][sname](values[space]); + } + } + + return true; +}; + +Color.prototype.setSpace = function (space, args) { + var vals = args[0]; + + if (vals === undefined) { + // color.rgb() + return this.getValues(space); + } + + // color.rgb(10, 10, 10) + if (typeof vals === 'number') { + vals = Array.prototype.slice.call(args); + } + + this.setValues(space, vals); + return this; +}; + +Color.prototype.setChannel = function (space, index, val) { + var svalues = this.values[space]; + if (val === undefined) { + // color.red() + return svalues[index]; + } else if (val === svalues[index]) { + // color.red(color.red()) + return this; + } + + // color.red(100) + svalues[index] = val; + this.setValues(space, svalues); + + return this; +}; + +if (typeof window !== 'undefined') { + window.Color = Color; +} + +module.exports = Color; + +},{"2":2,"5":5}],4:[function(require,module,exports){ +/* MIT license */ + +module.exports = { + rgb2hsl: rgb2hsl, + rgb2hsv: rgb2hsv, + rgb2hwb: rgb2hwb, + rgb2cmyk: rgb2cmyk, + rgb2keyword: rgb2keyword, + rgb2xyz: rgb2xyz, + rgb2lab: rgb2lab, + rgb2lch: rgb2lch, + + hsl2rgb: hsl2rgb, + hsl2hsv: hsl2hsv, + hsl2hwb: hsl2hwb, + hsl2cmyk: hsl2cmyk, + hsl2keyword: hsl2keyword, + + hsv2rgb: hsv2rgb, + hsv2hsl: hsv2hsl, + hsv2hwb: hsv2hwb, + hsv2cmyk: hsv2cmyk, + hsv2keyword: hsv2keyword, + + hwb2rgb: hwb2rgb, + hwb2hsl: hwb2hsl, + hwb2hsv: hwb2hsv, + hwb2cmyk: hwb2cmyk, + hwb2keyword: hwb2keyword, + + cmyk2rgb: cmyk2rgb, + cmyk2hsl: cmyk2hsl, + cmyk2hsv: cmyk2hsv, + cmyk2hwb: cmyk2hwb, + cmyk2keyword: cmyk2keyword, + + keyword2rgb: keyword2rgb, + keyword2hsl: keyword2hsl, + keyword2hsv: keyword2hsv, + keyword2hwb: keyword2hwb, + keyword2cmyk: keyword2cmyk, + keyword2lab: keyword2lab, + keyword2xyz: keyword2xyz, + + xyz2rgb: xyz2rgb, + xyz2lab: xyz2lab, + xyz2lch: xyz2lch, + + lab2xyz: lab2xyz, + lab2rgb: lab2rgb, + lab2lch: lab2lch, + + lch2lab: lch2lab, + lch2xyz: lch2xyz, + lch2rgb: lch2rgb +} + + +function rgb2hsl(rgb) { + var r = rgb[0]/255, + g = rgb[1]/255, + b = rgb[2]/255, + min = Math.min(r, g, b), + max = Math.max(r, g, b), + delta = max - min, + h, s, l; + + if (max == min) + h = 0; + else if (r == max) + h = (g - b) / delta; + else if (g == max) + h = 2 + (b - r) / delta; + else if (b == max) + h = 4 + (r - g)/ delta; + + h = Math.min(h * 60, 360); + + if (h < 0) + h += 360; + + l = (min + max) / 2; + + if (max == min) + s = 0; + else if (l <= 0.5) + s = delta / (max + min); + else + s = delta / (2 - max - min); + + return [h, s * 100, l * 100]; +} + +function rgb2hsv(rgb) { + var r = rgb[0], + g = rgb[1], + b = rgb[2], + min = Math.min(r, g, b), + max = Math.max(r, g, b), + delta = max - min, + h, s, v; + + if (max == 0) + s = 0; + else + s = (delta/max * 1000)/10; + + if (max == min) + h = 0; + else if (r == max) + h = (g - b) / delta; + else if (g == max) + h = 2 + (b - r) / delta; + else if (b == max) + h = 4 + (r - g) / delta; + + h = Math.min(h * 60, 360); + + if (h < 0) + h += 360; + + v = ((max / 255) * 1000) / 10; + + return [h, s, v]; +} + +function rgb2hwb(rgb) { + var r = rgb[0], + g = rgb[1], + b = rgb[2], + h = rgb2hsl(rgb)[0], + w = 1/255 * Math.min(r, Math.min(g, b)), + b = 1 - 1/255 * Math.max(r, Math.max(g, b)); + + return [h, w * 100, b * 100]; +} + +function rgb2cmyk(rgb) { + var r = rgb[0] / 255, + g = rgb[1] / 255, + b = rgb[2] / 255, + c, m, y, k; + + k = Math.min(1 - r, 1 - g, 1 - b); + c = (1 - r - k) / (1 - k) || 0; + m = (1 - g - k) / (1 - k) || 0; + y = (1 - b - k) / (1 - k) || 0; + return [c * 100, m * 100, y * 100, k * 100]; +} + +function rgb2keyword(rgb) { + return reverseKeywords[JSON.stringify(rgb)]; +} + +function rgb2xyz(rgb) { + var r = rgb[0] / 255, + g = rgb[1] / 255, + b = rgb[2] / 255; + + // assume sRGB + r = r > 0.04045 ? Math.pow(((r + 0.055) / 1.055), 2.4) : (r / 12.92); + g = g > 0.04045 ? Math.pow(((g + 0.055) / 1.055), 2.4) : (g / 12.92); + b = b > 0.04045 ? Math.pow(((b + 0.055) / 1.055), 2.4) : (b / 12.92); + + var x = (r * 0.4124) + (g * 0.3576) + (b * 0.1805); + var y = (r * 0.2126) + (g * 0.7152) + (b * 0.0722); + var z = (r * 0.0193) + (g * 0.1192) + (b * 0.9505); + + return [x * 100, y *100, z * 100]; +} + +function rgb2lab(rgb) { + var xyz = rgb2xyz(rgb), + x = xyz[0], + y = xyz[1], + z = xyz[2], + l, a, b; + + x /= 95.047; + y /= 100; + z /= 108.883; + + x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + (16 / 116); + y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + (16 / 116); + z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + (16 / 116); + + l = (116 * y) - 16; + a = 500 * (x - y); + b = 200 * (y - z); + + return [l, a, b]; +} + +function rgb2lch(args) { + return lab2lch(rgb2lab(args)); +} + +function hsl2rgb(hsl) { + var h = hsl[0] / 360, + s = hsl[1] / 100, + l = hsl[2] / 100, + t1, t2, t3, rgb, val; + + if (s == 0) { + val = l * 255; + return [val, val, val]; + } + + if (l < 0.5) + t2 = l * (1 + s); + else + t2 = l + s - l * s; + t1 = 2 * l - t2; + + rgb = [0, 0, 0]; + for (var i = 0; i < 3; i++) { + t3 = h + 1 / 3 * - (i - 1); + t3 < 0 && t3++; + t3 > 1 && t3--; + + if (6 * t3 < 1) + val = t1 + (t2 - t1) * 6 * t3; + else if (2 * t3 < 1) + val = t2; + else if (3 * t3 < 2) + val = t1 + (t2 - t1) * (2 / 3 - t3) * 6; + else + val = t1; + + rgb[i] = val * 255; + } + + return rgb; +} + +function hsl2hsv(hsl) { + var h = hsl[0], + s = hsl[1] / 100, + l = hsl[2] / 100, + sv, v; + + if(l === 0) { + // no need to do calc on black + // also avoids divide by 0 error + return [0, 0, 0]; + } + + l *= 2; + s *= (l <= 1) ? l : 2 - l; + v = (l + s) / 2; + sv = (2 * s) / (l + s); + return [h, sv * 100, v * 100]; +} + +function hsl2hwb(args) { + return rgb2hwb(hsl2rgb(args)); +} + +function hsl2cmyk(args) { + return rgb2cmyk(hsl2rgb(args)); +} + +function hsl2keyword(args) { + return rgb2keyword(hsl2rgb(args)); +} + + +function hsv2rgb(hsv) { + var h = hsv[0] / 60, + s = hsv[1] / 100, + v = hsv[2] / 100, + hi = Math.floor(h) % 6; + + var f = h - Math.floor(h), + p = 255 * v * (1 - s), + q = 255 * v * (1 - (s * f)), + t = 255 * v * (1 - (s * (1 - f))), + v = 255 * v; + + switch(hi) { + case 0: + return [v, t, p]; + case 1: + return [q, v, p]; + case 2: + return [p, v, t]; + case 3: + return [p, q, v]; + case 4: + return [t, p, v]; + case 5: + return [v, p, q]; + } +} + +function hsv2hsl(hsv) { + var h = hsv[0], + s = hsv[1] / 100, + v = hsv[2] / 100, + sl, l; + + l = (2 - s) * v; + sl = s * v; + sl /= (l <= 1) ? l : 2 - l; + sl = sl || 0; + l /= 2; + return [h, sl * 100, l * 100]; +} + +function hsv2hwb(args) { + return rgb2hwb(hsv2rgb(args)) +} + +function hsv2cmyk(args) { + return rgb2cmyk(hsv2rgb(args)); +} + +function hsv2keyword(args) { + return rgb2keyword(hsv2rgb(args)); +} + +// http://dev.w3.org/csswg/css-color/#hwb-to-rgb +function hwb2rgb(hwb) { + var h = hwb[0] / 360, + wh = hwb[1] / 100, + bl = hwb[2] / 100, + ratio = wh + bl, + i, v, f, n; + + // wh + bl cant be > 1 + if (ratio > 1) { + wh /= ratio; + bl /= ratio; + } + + i = Math.floor(6 * h); + v = 1 - bl; + f = 6 * h - i; + if ((i & 0x01) != 0) { + f = 1 - f; + } + n = wh + f * (v - wh); // linear interpolation + + switch (i) { + default: + case 6: + case 0: r = v; g = n; b = wh; break; + case 1: r = n; g = v; b = wh; break; + case 2: r = wh; g = v; b = n; break; + case 3: r = wh; g = n; b = v; break; + case 4: r = n; g = wh; b = v; break; + case 5: r = v; g = wh; b = n; break; + } + + return [r * 255, g * 255, b * 255]; +} + +function hwb2hsl(args) { + return rgb2hsl(hwb2rgb(args)); +} + +function hwb2hsv(args) { + return rgb2hsv(hwb2rgb(args)); +} + +function hwb2cmyk(args) { + return rgb2cmyk(hwb2rgb(args)); +} + +function hwb2keyword(args) { + return rgb2keyword(hwb2rgb(args)); +} + +function cmyk2rgb(cmyk) { + var c = cmyk[0] / 100, + m = cmyk[1] / 100, + y = cmyk[2] / 100, + k = cmyk[3] / 100, + r, g, b; + + r = 1 - Math.min(1, c * (1 - k) + k); + g = 1 - Math.min(1, m * (1 - k) + k); + b = 1 - Math.min(1, y * (1 - k) + k); + return [r * 255, g * 255, b * 255]; +} + +function cmyk2hsl(args) { + return rgb2hsl(cmyk2rgb(args)); +} + +function cmyk2hsv(args) { + return rgb2hsv(cmyk2rgb(args)); +} + +function cmyk2hwb(args) { + return rgb2hwb(cmyk2rgb(args)); +} + +function cmyk2keyword(args) { + return rgb2keyword(cmyk2rgb(args)); +} + + +function xyz2rgb(xyz) { + var x = xyz[0] / 100, + y = xyz[1] / 100, + z = xyz[2] / 100, + r, g, b; + + r = (x * 3.2406) + (y * -1.5372) + (z * -0.4986); + g = (x * -0.9689) + (y * 1.8758) + (z * 0.0415); + b = (x * 0.0557) + (y * -0.2040) + (z * 1.0570); + + // assume sRGB + r = r > 0.0031308 ? ((1.055 * Math.pow(r, 1.0 / 2.4)) - 0.055) + : r = (r * 12.92); + + g = g > 0.0031308 ? ((1.055 * Math.pow(g, 1.0 / 2.4)) - 0.055) + : g = (g * 12.92); + + b = b > 0.0031308 ? ((1.055 * Math.pow(b, 1.0 / 2.4)) - 0.055) + : b = (b * 12.92); + + r = Math.min(Math.max(0, r), 1); + g = Math.min(Math.max(0, g), 1); + b = Math.min(Math.max(0, b), 1); + + return [r * 255, g * 255, b * 255]; +} + +function xyz2lab(xyz) { + var x = xyz[0], + y = xyz[1], + z = xyz[2], + l, a, b; + + x /= 95.047; + y /= 100; + z /= 108.883; + + x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + (16 / 116); + y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + (16 / 116); + z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + (16 / 116); + + l = (116 * y) - 16; + a = 500 * (x - y); + b = 200 * (y - z); + + return [l, a, b]; +} + +function xyz2lch(args) { + return lab2lch(xyz2lab(args)); +} + +function lab2xyz(lab) { + var l = lab[0], + a = lab[1], + b = lab[2], + x, y, z, y2; + + if (l <= 8) { + y = (l * 100) / 903.3; + y2 = (7.787 * (y / 100)) + (16 / 116); + } else { + y = 100 * Math.pow((l + 16) / 116, 3); + y2 = Math.pow(y / 100, 1/3); + } + + x = x / 95.047 <= 0.008856 ? x = (95.047 * ((a / 500) + y2 - (16 / 116))) / 7.787 : 95.047 * Math.pow((a / 500) + y2, 3); + + z = z / 108.883 <= 0.008859 ? z = (108.883 * (y2 - (b / 200) - (16 / 116))) / 7.787 : 108.883 * Math.pow(y2 - (b / 200), 3); + + return [x, y, z]; +} + +function lab2lch(lab) { + var l = lab[0], + a = lab[1], + b = lab[2], + hr, h, c; + + hr = Math.atan2(b, a); + h = hr * 360 / 2 / Math.PI; + if (h < 0) { + h += 360; + } + c = Math.sqrt(a * a + b * b); + return [l, c, h]; +} + +function lab2rgb(args) { + return xyz2rgb(lab2xyz(args)); +} + +function lch2lab(lch) { + var l = lch[0], + c = lch[1], + h = lch[2], + a, b, hr; + + hr = h / 360 * 2 * Math.PI; + a = c * Math.cos(hr); + b = c * Math.sin(hr); + return [l, a, b]; +} + +function lch2xyz(args) { + return lab2xyz(lch2lab(args)); +} + +function lch2rgb(args) { + return lab2rgb(lch2lab(args)); +} + +function keyword2rgb(keyword) { + return cssKeywords[keyword]; +} + +function keyword2hsl(args) { + return rgb2hsl(keyword2rgb(args)); +} + +function keyword2hsv(args) { + return rgb2hsv(keyword2rgb(args)); +} + +function keyword2hwb(args) { + return rgb2hwb(keyword2rgb(args)); +} + +function keyword2cmyk(args) { + return rgb2cmyk(keyword2rgb(args)); +} + +function keyword2lab(args) { + return rgb2lab(keyword2rgb(args)); +} + +function keyword2xyz(args) { + return rgb2xyz(keyword2rgb(args)); +} + +var cssKeywords = { + aliceblue: [240,248,255], + antiquewhite: [250,235,215], + aqua: [0,255,255], + aquamarine: [127,255,212], + azure: [240,255,255], + beige: [245,245,220], + bisque: [255,228,196], + black: [0,0,0], + blanchedalmond: [255,235,205], + blue: [0,0,255], + blueviolet: [138,43,226], + brown: [165,42,42], + burlywood: [222,184,135], + cadetblue: [95,158,160], + chartreuse: [127,255,0], + chocolate: [210,105,30], + coral: [255,127,80], + cornflowerblue: [100,149,237], + cornsilk: [255,248,220], + crimson: [220,20,60], + cyan: [0,255,255], + darkblue: [0,0,139], + darkcyan: [0,139,139], + darkgoldenrod: [184,134,11], + darkgray: [169,169,169], + darkgreen: [0,100,0], + darkgrey: [169,169,169], + darkkhaki: [189,183,107], + darkmagenta: [139,0,139], + darkolivegreen: [85,107,47], + darkorange: [255,140,0], + darkorchid: [153,50,204], + darkred: [139,0,0], + darksalmon: [233,150,122], + darkseagreen: [143,188,143], + darkslateblue: [72,61,139], + darkslategray: [47,79,79], + darkslategrey: [47,79,79], + darkturquoise: [0,206,209], + darkviolet: [148,0,211], + deeppink: [255,20,147], + deepskyblue: [0,191,255], + dimgray: [105,105,105], + dimgrey: [105,105,105], + dodgerblue: [30,144,255], + firebrick: [178,34,34], + floralwhite: [255,250,240], + forestgreen: [34,139,34], + fuchsia: [255,0,255], + gainsboro: [220,220,220], + ghostwhite: [248,248,255], + gold: [255,215,0], + goldenrod: [218,165,32], + gray: [128,128,128], + green: [0,128,0], + greenyellow: [173,255,47], + grey: [128,128,128], + honeydew: [240,255,240], + hotpink: [255,105,180], + indianred: [205,92,92], + indigo: [75,0,130], + ivory: [255,255,240], + khaki: [240,230,140], + lavender: [230,230,250], + lavenderblush: [255,240,245], + lawngreen: [124,252,0], + lemonchiffon: [255,250,205], + lightblue: [173,216,230], + lightcoral: [240,128,128], + lightcyan: [224,255,255], + lightgoldenrodyellow: [250,250,210], + lightgray: [211,211,211], + lightgreen: [144,238,144], + lightgrey: [211,211,211], + lightpink: [255,182,193], + lightsalmon: [255,160,122], + lightseagreen: [32,178,170], + lightskyblue: [135,206,250], + lightslategray: [119,136,153], + lightslategrey: [119,136,153], + lightsteelblue: [176,196,222], + lightyellow: [255,255,224], + lime: [0,255,0], + limegreen: [50,205,50], + linen: [250,240,230], + magenta: [255,0,255], + maroon: [128,0,0], + mediumaquamarine: [102,205,170], + mediumblue: [0,0,205], + mediumorchid: [186,85,211], + mediumpurple: [147,112,219], + mediumseagreen: [60,179,113], + mediumslateblue: [123,104,238], + mediumspringgreen: [0,250,154], + mediumturquoise: [72,209,204], + mediumvioletred: [199,21,133], + midnightblue: [25,25,112], + mintcream: [245,255,250], + mistyrose: [255,228,225], + moccasin: [255,228,181], + navajowhite: [255,222,173], + navy: [0,0,128], + oldlace: [253,245,230], + olive: [128,128,0], + olivedrab: [107,142,35], + orange: [255,165,0], + orangered: [255,69,0], + orchid: [218,112,214], + palegoldenrod: [238,232,170], + palegreen: [152,251,152], + paleturquoise: [175,238,238], + palevioletred: [219,112,147], + papayawhip: [255,239,213], + peachpuff: [255,218,185], + peru: [205,133,63], + pink: [255,192,203], + plum: [221,160,221], + powderblue: [176,224,230], + purple: [128,0,128], + rebeccapurple: [102, 51, 153], + red: [255,0,0], + rosybrown: [188,143,143], + royalblue: [65,105,225], + saddlebrown: [139,69,19], + salmon: [250,128,114], + sandybrown: [244,164,96], + seagreen: [46,139,87], + seashell: [255,245,238], + sienna: [160,82,45], + silver: [192,192,192], + skyblue: [135,206,235], + slateblue: [106,90,205], + slategray: [112,128,144], + slategrey: [112,128,144], + snow: [255,250,250], + springgreen: [0,255,127], + steelblue: [70,130,180], + tan: [210,180,140], + teal: [0,128,128], + thistle: [216,191,216], + tomato: [255,99,71], + turquoise: [64,224,208], + violet: [238,130,238], + wheat: [245,222,179], + white: [255,255,255], + whitesmoke: [245,245,245], + yellow: [255,255,0], + yellowgreen: [154,205,50] +}; + +var reverseKeywords = {}; +for (var key in cssKeywords) { + reverseKeywords[JSON.stringify(cssKeywords[key])] = key; +} + +},{}],5:[function(require,module,exports){ +var conversions = require(4); + +var convert = function() { + return new Converter(); +} + +for (var func in conversions) { + // export Raw versions + convert[func + "Raw"] = (function(func) { + // accept array or plain args + return function(arg) { + if (typeof arg == "number") + arg = Array.prototype.slice.call(arguments); + return conversions[func](arg); + } + })(func); + + var pair = /(\w+)2(\w+)/.exec(func), + from = pair[1], + to = pair[2]; + + // export rgb2hsl and ["rgb"]["hsl"] + convert[from] = convert[from] || {}; + + convert[from][to] = convert[func] = (function(func) { + return function(arg) { + if (typeof arg == "number") + arg = Array.prototype.slice.call(arguments); + + var val = conversions[func](arg); + if (typeof val == "string" || val === undefined) + return val; // keyword + + for (var i = 0; i < val.length; i++) + val[i] = Math.round(val[i]); + return val; + } + })(func); +} + + +/* Converter does lazy conversion and caching */ +var Converter = function() { + this.convs = {}; +}; + +/* Either get the values for a space or + set the values for a space, depending on args */ +Converter.prototype.routeSpace = function(space, args) { + var values = args[0]; + if (values === undefined) { + // color.rgb() + return this.getValues(space); + } + // color.rgb(10, 10, 10) + if (typeof values == "number") { + values = Array.prototype.slice.call(args); + } + + return this.setValues(space, values); +}; + +/* Set the values for a space, invalidating cache */ +Converter.prototype.setValues = function(space, values) { + this.space = space; + this.convs = {}; + this.convs[space] = values; + return this; +}; + +/* Get the values for a space. If there's already + a conversion for the space, fetch it, otherwise + compute it */ +Converter.prototype.getValues = function(space) { + var vals = this.convs[space]; + if (!vals) { + var fspace = this.space, + from = this.convs[fspace]; + vals = convert[fspace][space](from); + + this.convs[space] = vals; + } + return vals; +}; + +["rgb", "hsl", "hsv", "cmyk", "keyword"].forEach(function(space) { + Converter.prototype[space] = function(vals) { + return this.routeSpace(space, arguments); + } +}); + +module.exports = convert; +},{"4":4}],6:[function(require,module,exports){ +module.exports = { + "aliceblue": [240, 248, 255], + "antiquewhite": [250, 235, 215], + "aqua": [0, 255, 255], + "aquamarine": [127, 255, 212], + "azure": [240, 255, 255], + "beige": [245, 245, 220], + "bisque": [255, 228, 196], + "black": [0, 0, 0], + "blanchedalmond": [255, 235, 205], + "blue": [0, 0, 255], + "blueviolet": [138, 43, 226], + "brown": [165, 42, 42], + "burlywood": [222, 184, 135], + "cadetblue": [95, 158, 160], + "chartreuse": [127, 255, 0], + "chocolate": [210, 105, 30], + "coral": [255, 127, 80], + "cornflowerblue": [100, 149, 237], + "cornsilk": [255, 248, 220], + "crimson": [220, 20, 60], + "cyan": [0, 255, 255], + "darkblue": [0, 0, 139], + "darkcyan": [0, 139, 139], + "darkgoldenrod": [184, 134, 11], + "darkgray": [169, 169, 169], + "darkgreen": [0, 100, 0], + "darkgrey": [169, 169, 169], + "darkkhaki": [189, 183, 107], + "darkmagenta": [139, 0, 139], + "darkolivegreen": [85, 107, 47], + "darkorange": [255, 140, 0], + "darkorchid": [153, 50, 204], + "darkred": [139, 0, 0], + "darksalmon": [233, 150, 122], + "darkseagreen": [143, 188, 143], + "darkslateblue": [72, 61, 139], + "darkslategray": [47, 79, 79], + "darkslategrey": [47, 79, 79], + "darkturquoise": [0, 206, 209], + "darkviolet": [148, 0, 211], + "deeppink": [255, 20, 147], + "deepskyblue": [0, 191, 255], + "dimgray": [105, 105, 105], + "dimgrey": [105, 105, 105], + "dodgerblue": [30, 144, 255], + "firebrick": [178, 34, 34], + "floralwhite": [255, 250, 240], + "forestgreen": [34, 139, 34], + "fuchsia": [255, 0, 255], + "gainsboro": [220, 220, 220], + "ghostwhite": [248, 248, 255], + "gold": [255, 215, 0], + "goldenrod": [218, 165, 32], + "gray": [128, 128, 128], + "green": [0, 128, 0], + "greenyellow": [173, 255, 47], + "grey": [128, 128, 128], + "honeydew": [240, 255, 240], + "hotpink": [255, 105, 180], + "indianred": [205, 92, 92], + "indigo": [75, 0, 130], + "ivory": [255, 255, 240], + "khaki": [240, 230, 140], + "lavender": [230, 230, 250], + "lavenderblush": [255, 240, 245], + "lawngreen": [124, 252, 0], + "lemonchiffon": [255, 250, 205], + "lightblue": [173, 216, 230], + "lightcoral": [240, 128, 128], + "lightcyan": [224, 255, 255], + "lightgoldenrodyellow": [250, 250, 210], + "lightgray": [211, 211, 211], + "lightgreen": [144, 238, 144], + "lightgrey": [211, 211, 211], + "lightpink": [255, 182, 193], + "lightsalmon": [255, 160, 122], + "lightseagreen": [32, 178, 170], + "lightskyblue": [135, 206, 250], + "lightslategray": [119, 136, 153], + "lightslategrey": [119, 136, 153], + "lightsteelblue": [176, 196, 222], + "lightyellow": [255, 255, 224], + "lime": [0, 255, 0], + "limegreen": [50, 205, 50], + "linen": [250, 240, 230], + "magenta": [255, 0, 255], + "maroon": [128, 0, 0], + "mediumaquamarine": [102, 205, 170], + "mediumblue": [0, 0, 205], + "mediumorchid": [186, 85, 211], + "mediumpurple": [147, 112, 219], + "mediumseagreen": [60, 179, 113], + "mediumslateblue": [123, 104, 238], + "mediumspringgreen": [0, 250, 154], + "mediumturquoise": [72, 209, 204], + "mediumvioletred": [199, 21, 133], + "midnightblue": [25, 25, 112], + "mintcream": [245, 255, 250], + "mistyrose": [255, 228, 225], + "moccasin": [255, 228, 181], + "navajowhite": [255, 222, 173], + "navy": [0, 0, 128], + "oldlace": [253, 245, 230], + "olive": [128, 128, 0], + "olivedrab": [107, 142, 35], + "orange": [255, 165, 0], + "orangered": [255, 69, 0], + "orchid": [218, 112, 214], + "palegoldenrod": [238, 232, 170], + "palegreen": [152, 251, 152], + "paleturquoise": [175, 238, 238], + "palevioletred": [219, 112, 147], + "papayawhip": [255, 239, 213], + "peachpuff": [255, 218, 185], + "peru": [205, 133, 63], + "pink": [255, 192, 203], + "plum": [221, 160, 221], + "powderblue": [176, 224, 230], + "purple": [128, 0, 128], + "rebeccapurple": [102, 51, 153], + "red": [255, 0, 0], + "rosybrown": [188, 143, 143], + "royalblue": [65, 105, 225], + "saddlebrown": [139, 69, 19], + "salmon": [250, 128, 114], + "sandybrown": [244, 164, 96], + "seagreen": [46, 139, 87], + "seashell": [255, 245, 238], + "sienna": [160, 82, 45], + "silver": [192, 192, 192], + "skyblue": [135, 206, 235], + "slateblue": [106, 90, 205], + "slategray": [112, 128, 144], + "slategrey": [112, 128, 144], + "snow": [255, 250, 250], + "springgreen": [0, 255, 127], + "steelblue": [70, 130, 180], + "tan": [210, 180, 140], + "teal": [0, 128, 128], + "thistle": [216, 191, 216], + "tomato": [255, 99, 71], + "turquoise": [64, 224, 208], + "violet": [238, 130, 238], + "wheat": [245, 222, 179], + "white": [255, 255, 255], + "whitesmoke": [245, 245, 245], + "yellow": [255, 255, 0], + "yellowgreen": [154, 205, 50] +}; +},{}],7:[function(require,module,exports){ +/** + * @namespace Chart + */ +var Chart = require(28)(); + +require(26)(Chart); +require(40)(Chart); +require(22)(Chart); +require(25)(Chart); +require(30)(Chart); +require(21)(Chart); +require(23)(Chart); +require(24)(Chart); +require(29)(Chart); +require(32)(Chart); +require(33)(Chart); +require(31)(Chart); +require(27)(Chart); +require(34)(Chart); + +require(35)(Chart); +require(36)(Chart); +require(37)(Chart); +require(38)(Chart); + +require(46)(Chart); +require(44)(Chart); +require(45)(Chart); +require(47)(Chart); +require(48)(Chart); +require(49)(Chart); + +// Controllers must be loaded after elements +// See Chart.core.datasetController.dataElementType +require(15)(Chart); +require(16)(Chart); +require(17)(Chart); +require(18)(Chart); +require(19)(Chart); +require(20)(Chart); + +require(8)(Chart); +require(9)(Chart); +require(10)(Chart); +require(11)(Chart); +require(12)(Chart); +require(13)(Chart); +require(14)(Chart); + +// Loading built-it plugins +var plugins = []; + +plugins.push( + require(41)(Chart), + require(42)(Chart), + require(43)(Chart) +); + +Chart.plugins.register(plugins); + +module.exports = Chart; +if (typeof window !== 'undefined') { + window.Chart = Chart; +} + +},{"10":10,"11":11,"12":12,"13":13,"14":14,"15":15,"16":16,"17":17,"18":18,"19":19,"20":20,"21":21,"22":22,"23":23,"24":24,"25":25,"26":26,"27":27,"28":28,"29":29,"30":30,"31":31,"32":32,"33":33,"34":34,"35":35,"36":36,"37":37,"38":38,"40":40,"41":41,"42":42,"43":43,"44":44,"45":45,"46":46,"47":47,"48":48,"49":49,"8":8,"9":9}],8:[function(require,module,exports){ +'use strict'; + +module.exports = function(Chart) { + + Chart.Bar = function(context, config) { + config.type = 'bar'; + + return new Chart(context, config); + }; + +}; + +},{}],9:[function(require,module,exports){ +'use strict'; + +module.exports = function(Chart) { + + Chart.Bubble = function(context, config) { + config.type = 'bubble'; + return new Chart(context, config); + }; + +}; + +},{}],10:[function(require,module,exports){ +'use strict'; + +module.exports = function(Chart) { + + Chart.Doughnut = function(context, config) { + config.type = 'doughnut'; + + return new Chart(context, config); + }; + +}; + +},{}],11:[function(require,module,exports){ +'use strict'; + +module.exports = function(Chart) { + + Chart.Line = function(context, config) { + config.type = 'line'; + + return new Chart(context, config); + }; + +}; + +},{}],12:[function(require,module,exports){ +'use strict'; + +module.exports = function(Chart) { + + Chart.PolarArea = function(context, config) { + config.type = 'polarArea'; + + return new Chart(context, config); + }; + +}; + +},{}],13:[function(require,module,exports){ +'use strict'; + +module.exports = function(Chart) { + + Chart.Radar = function(context, config) { + config.type = 'radar'; + + return new Chart(context, config); + }; + +}; + +},{}],14:[function(require,module,exports){ +'use strict'; + +module.exports = function(Chart) { + + var defaultConfig = { + hover: { + mode: 'single' + }, + + scales: { + xAxes: [{ + type: 'linear', // scatter should not use a category axis + position: 'bottom', + id: 'x-axis-1' // need an ID so datasets can reference the scale + }], + yAxes: [{ + type: 'linear', + position: 'left', + id: 'y-axis-1' + }] + }, + + tooltips: { + callbacks: { + title: function() { + // Title doesn't make sense for scatter since we format the data as a point + return ''; + }, + label: function(tooltipItem) { + return '(' + tooltipItem.xLabel + ', ' + tooltipItem.yLabel + ')'; + } + } + } + }; + + // Register the default config for this type + Chart.defaults.scatter = defaultConfig; + + // Scatter charts use line controllers + Chart.controllers.scatter = Chart.controllers.line; + + Chart.Scatter = function(context, config) { + config.type = 'scatter'; + return new Chart(context, config); + }; + +}; + +},{}],15:[function(require,module,exports){ +'use strict'; + +module.exports = function(Chart) { + + var helpers = Chart.helpers; + + Chart.defaults.bar = { + hover: { + mode: 'label' + }, + + scales: { + xAxes: [{ + type: 'category', + + // Specific to Bar Controller + categoryPercentage: 0.8, + barPercentage: 0.9, + + // grid line settings + gridLines: { + offsetGridLines: true + } + }], + yAxes: [{ + type: 'linear' + }] + } + }; + + Chart.controllers.bar = Chart.DatasetController.extend({ + + dataElementType: Chart.elements.Rectangle, + + initialize: function() { + var me = this; + var meta; + + Chart.DatasetController.prototype.initialize.apply(me, arguments); + + meta = me.getMeta(); + meta.stack = me.getDataset().stack; + meta.bar = true; + }, + + update: function(reset) { + var me = this; + var elements = me.getMeta().data; + var i, ilen; + + me._ruler = me.getRuler(); + + for (i = 0, ilen = elements.length; i < ilen; ++i) { + me.updateElement(elements[i], i, reset); + } + }, + + updateElement: function(rectangle, index, reset) { + var me = this; + var chart = me.chart; + var meta = me.getMeta(); + var dataset = me.getDataset(); + var custom = rectangle.custom || {}; + var rectangleOptions = chart.options.elements.rectangle; + + rectangle._xScale = me.getScaleForId(meta.xAxisID); + rectangle._yScale = me.getScaleForId(meta.yAxisID); + rectangle._datasetIndex = me.index; + rectangle._index = index; + + rectangle._model = { + datasetLabel: dataset.label, + label: chart.data.labels[index], + borderSkipped: custom.borderSkipped ? custom.borderSkipped : rectangleOptions.borderSkipped, + backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleOptions.backgroundColor), + borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleOptions.borderColor), + borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleOptions.borderWidth) + }; + + me.updateElementGeometry(rectangle, index, reset); + + rectangle.pivot(); + }, + + /** + * @private + */ + updateElementGeometry: function(rectangle, index, reset) { + var me = this; + var model = rectangle._model; + var vscale = me.getValueScale(); + var base = vscale.getBasePixel(); + var horizontal = vscale.isHorizontal(); + var ruler = me._ruler || me.getRuler(); + var vpixels = me.calculateBarValuePixels(me.index, index); + var ipixels = me.calculateBarIndexPixels(me.index, index, ruler); + + model.horizontal = horizontal; + model.base = reset? base : vpixels.base; + model.x = horizontal? reset? base : vpixels.head : ipixels.center; + model.y = horizontal? ipixels.center : reset? base : vpixels.head; + model.height = horizontal? ipixels.size : undefined; + model.width = horizontal? undefined : ipixels.size; + }, + + /** + * @private + */ + getValueScaleId: function() { + return this.getMeta().yAxisID; + }, + + /** + * @private + */ + getIndexScaleId: function() { + return this.getMeta().xAxisID; + }, + + /** + * @private + */ + getValueScale: function() { + return this.getScaleForId(this.getValueScaleId()); + }, + + /** + * @private + */ + getIndexScale: function() { + return this.getScaleForId(this.getIndexScaleId()); + }, + + /** + * Returns the effective number of stacks based on groups and bar visibility. + * @private + */ + getStackCount: function(last) { + var me = this; + var chart = me.chart; + var scale = me.getIndexScale(); + var stacked = scale.options.stacked; + var ilen = last === undefined? chart.data.datasets.length : last + 1; + var stacks = []; + var i, meta; + + for (i = 0; i < ilen; ++i) { + meta = chart.getDatasetMeta(i); + if (meta.bar && chart.isDatasetVisible(i) && + (stacked === false || + (stacked === true && stacks.indexOf(meta.stack) === -1) || + (stacked === undefined && (meta.stack === undefined || stacks.indexOf(meta.stack) === -1)))) { + stacks.push(meta.stack); + } + } + + return stacks.length; + }, + + /** + * Returns the stack index for the given dataset based on groups and bar visibility. + * @private + */ + getStackIndex: function(datasetIndex) { + return this.getStackCount(datasetIndex) - 1; + }, + + /** + * @private + */ + getRuler: function() { + var me = this; + var scale = me.getIndexScale(); + var options = scale.options; + var stackCount = me.getStackCount(); + var fullSize = scale.isHorizontal()? scale.width : scale.height; + var tickSize = fullSize / scale.ticks.length; + var categorySize = tickSize * options.categoryPercentage; + var fullBarSize = categorySize / stackCount; + var barSize = fullBarSize * options.barPercentage; + + barSize = Math.min( + helpers.getValueOrDefault(options.barThickness, barSize), + helpers.getValueOrDefault(options.maxBarThickness, Infinity)); + + return { + stackCount: stackCount, + tickSize: tickSize, + categorySize: categorySize, + categorySpacing: tickSize - categorySize, + fullBarSize: fullBarSize, + barSize: barSize, + barSpacing: fullBarSize - barSize, + scale: scale + }; + }, + + /** + * Note: pixel values are not clamped to the scale area. + * @private + */ + calculateBarValuePixels: function(datasetIndex, index) { + var me = this; + var chart = me.chart; + var meta = me.getMeta(); + var scale = me.getValueScale(); + var datasets = chart.data.datasets; + var value = Number(datasets[datasetIndex].data[index]); + var stacked = scale.options.stacked; + var stack = meta.stack; + var start = 0; + var i, imeta, ivalue, base, head, size; + + if (stacked || (stacked === undefined && stack !== undefined)) { + for (i = 0; i < datasetIndex; ++i) { + imeta = chart.getDatasetMeta(i); + + if (imeta.bar && + imeta.stack === stack && + imeta.controller.getValueScaleId() === scale.id && + chart.isDatasetVisible(i)) { + + ivalue = Number(datasets[i].data[index]); + if ((value < 0 && ivalue < 0) || (value >= 0 && ivalue > 0)) { + start += ivalue; + } + } + } + } + + base = scale.getPixelForValue(start); + head = scale.getPixelForValue(start + value); + size = (head - base) / 2; + + return { + size: size, + base: base, + head: head, + center: head + size / 2 + }; + }, + + /** + * @private + */ + calculateBarIndexPixels: function(datasetIndex, index, ruler) { + var me = this; + var scale = ruler.scale; + var isCombo = me.chart.isCombo; + var stackIndex = me.getStackIndex(datasetIndex); + var base = scale.getPixelForValue(null, index, datasetIndex, isCombo); + var size = ruler.barSize; + + base -= isCombo? ruler.tickSize / 2 : 0; + base += ruler.fullBarSize * stackIndex; + base += ruler.categorySpacing / 2; + base += ruler.barSpacing / 2; + + return { + size: size, + base: base, + head: base + size, + center: base + size / 2 + }; + }, + + draw: function() { + var me = this; + var chart = me.chart; + var elements = me.getMeta().data; + var dataset = me.getDataset(); + var ilen = elements.length; + var i = 0; + var d; + + helpers.canvas.clipArea(chart.ctx, chart.chartArea); + + for (; i 0) { + if (tooltipItems[0].yLabel) { + title = tooltipItems[0].yLabel; + } else if (data.labels.length > 0 && tooltipItems[0].index < data.labels.length) { + title = data.labels[tooltipItems[0].index]; + } + } + + return title; + }, + label: function(tooltipItem, data) { + var datasetLabel = data.datasets[tooltipItem.datasetIndex].label || ''; + return datasetLabel + ': ' + tooltipItem.xLabel; + } + } + } + }; + + Chart.controllers.horizontalBar = Chart.controllers.bar.extend({ + /** + * @private + */ + getValueScaleId: function() { + return this.getMeta().xAxisID; + }, + + /** + * @private + */ + getIndexScaleId: function() { + return this.getMeta().yAxisID; + } + }); +}; + +},{}],16:[function(require,module,exports){ +'use strict'; + +module.exports = function(Chart) { + + var helpers = Chart.helpers; + + Chart.defaults.bubble = { + hover: { + mode: 'single' + }, + + scales: { + xAxes: [{ + type: 'linear', // bubble should probably use a linear scale by default + position: 'bottom', + id: 'x-axis-0' // need an ID so datasets can reference the scale + }], + yAxes: [{ + type: 'linear', + position: 'left', + id: 'y-axis-0' + }] + }, + + tooltips: { + callbacks: { + title: function() { + // Title doesn't make sense for scatter since we format the data as a point + return ''; + }, + label: function(tooltipItem, data) { + var datasetLabel = data.datasets[tooltipItem.datasetIndex].label || ''; + var dataPoint = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]; + return datasetLabel + ': (' + tooltipItem.xLabel + ', ' + tooltipItem.yLabel + ', ' + dataPoint.r + ')'; + } + } + } + }; + + Chart.controllers.bubble = Chart.DatasetController.extend({ + + dataElementType: Chart.elements.Point, + + update: function(reset) { + var me = this; + var meta = me.getMeta(); + var points = meta.data; + + // Update Points + helpers.each(points, function(point, index) { + me.updateElement(point, index, reset); + }); + }, + + updateElement: function(point, index, reset) { + var me = this; + var meta = me.getMeta(); + var xScale = me.getScaleForId(meta.xAxisID); + var yScale = me.getScaleForId(meta.yAxisID); + + var custom = point.custom || {}; + var dataset = me.getDataset(); + var data = dataset.data[index]; + var pointElementOptions = me.chart.options.elements.point; + var dsIndex = me.index; + + helpers.extend(point, { + // Utility + _xScale: xScale, + _yScale: yScale, + _datasetIndex: dsIndex, + _index: index, + + // Desired view properties + _model: { + x: reset ? xScale.getPixelForDecimal(0.5) : xScale.getPixelForValue(typeof data === 'object' ? data : NaN, index, dsIndex, me.chart.isCombo), + y: reset ? yScale.getBasePixel() : yScale.getPixelForValue(data, index, dsIndex), + // Appearance + radius: reset ? 0 : custom.radius ? custom.radius : me.getRadius(data), + + // Tooltip + hitRadius: custom.hitRadius ? custom.hitRadius : helpers.getValueAtIndexOrDefault(dataset.hitRadius, index, pointElementOptions.hitRadius) + } + }); + + // Trick to reset the styles of the point + Chart.DatasetController.prototype.removeHoverStyle.call(me, point, pointElementOptions); + + var model = point._model; + model.skip = custom.skip ? custom.skip : (isNaN(model.x) || isNaN(model.y)); + + point.pivot(); + }, + + getRadius: function(value) { + return value.r || this.chart.options.elements.point.radius; + }, + + setHoverStyle: function(point) { + var me = this; + Chart.DatasetController.prototype.setHoverStyle.call(me, point); + + // Radius + var dataset = me.chart.data.datasets[point._datasetIndex]; + var index = point._index; + var custom = point.custom || {}; + var model = point._model; + model.radius = custom.hoverRadius ? custom.hoverRadius : (helpers.getValueAtIndexOrDefault(dataset.hoverRadius, index, me.chart.options.elements.point.hoverRadius)) + me.getRadius(dataset.data[index]); + }, + + removeHoverStyle: function(point) { + var me = this; + Chart.DatasetController.prototype.removeHoverStyle.call(me, point, me.chart.options.elements.point); + + var dataVal = me.chart.data.datasets[point._datasetIndex].data[point._index]; + var custom = point.custom || {}; + var model = point._model; + + model.radius = custom.radius ? custom.radius : me.getRadius(dataVal); + } + }); +}; + +},{}],17:[function(require,module,exports){ +'use strict'; + +module.exports = function(Chart) { + + var helpers = Chart.helpers, + defaults = Chart.defaults; + + defaults.doughnut = { + animation: { + // Boolean - Whether we animate the rotation of the Doughnut + animateRotate: true, + // Boolean - Whether we animate scaling the Doughnut from the centre + animateScale: false + }, + aspectRatio: 1, + hover: { + mode: 'single' + }, + legendCallback: function(chart) { + var text = []; + text.push('
    '); + + var data = chart.data; + var datasets = data.datasets; + var labels = data.labels; + + if (datasets.length) { + for (var i = 0; i < datasets[0].data.length; ++i) { + text.push('
  • '); + if (labels[i]) { + text.push(labels[i]); + } + text.push('
  • '); + } + } + + text.push('
'); + return text.join(''); + }, + legend: { + labels: { + generateLabels: function(chart) { + var data = chart.data; + if (data.labels.length && data.datasets.length) { + return data.labels.map(function(label, i) { + var meta = chart.getDatasetMeta(0); + var ds = data.datasets[0]; + var arc = meta.data[i]; + var custom = arc && arc.custom || {}; + var getValueAtIndexOrDefault = helpers.getValueAtIndexOrDefault; + var arcOpts = chart.options.elements.arc; + var fill = custom.backgroundColor ? custom.backgroundColor : getValueAtIndexOrDefault(ds.backgroundColor, i, arcOpts.backgroundColor); + var stroke = custom.borderColor ? custom.borderColor : getValueAtIndexOrDefault(ds.borderColor, i, arcOpts.borderColor); + var bw = custom.borderWidth ? custom.borderWidth : getValueAtIndexOrDefault(ds.borderWidth, i, arcOpts.borderWidth); + + return { + text: label, + fillStyle: fill, + strokeStyle: stroke, + lineWidth: bw, + hidden: isNaN(ds.data[i]) || meta.data[i].hidden, + + // Extra data used for toggling the correct item + index: i + }; + }); + } + return []; + } + }, + + onClick: function(e, legendItem) { + var index = legendItem.index; + var chart = this.chart; + var i, ilen, meta; + + for (i = 0, ilen = (chart.data.datasets || []).length; i < ilen; ++i) { + meta = chart.getDatasetMeta(i); + // toggle visibility of index if exists + if (meta.data[index]) { + meta.data[index].hidden = !meta.data[index].hidden; + } + } + + chart.update(); + } + }, + + // The percentage of the chart that we cut out of the middle. + cutoutPercentage: 50, + + // The rotation of the chart, where the first data arc begins. + rotation: Math.PI * -0.5, + + // The total circumference of the chart. + circumference: Math.PI * 2.0, + + // Need to override these to give a nice default + tooltips: { + callbacks: { + title: function() { + return ''; + }, + label: function(tooltipItem, data) { + var dataLabel = data.labels[tooltipItem.index]; + var value = ': ' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]; + + if (helpers.isArray(dataLabel)) { + // show value on first line of multiline label + // need to clone because we are changing the value + dataLabel = dataLabel.slice(); + dataLabel[0] += value; + } else { + dataLabel += value; + } + + return dataLabel; + } + } + } + }; + + defaults.pie = helpers.clone(defaults.doughnut); + helpers.extend(defaults.pie, { + cutoutPercentage: 0 + }); + + + Chart.controllers.doughnut = Chart.controllers.pie = Chart.DatasetController.extend({ + + dataElementType: Chart.elements.Arc, + + linkScales: helpers.noop, + + // Get index of the dataset in relation to the visible datasets. This allows determining the inner and outer radius correctly + getRingIndex: function(datasetIndex) { + var ringIndex = 0; + + for (var j = 0; j < datasetIndex; ++j) { + if (this.chart.isDatasetVisible(j)) { + ++ringIndex; + } + } + + return ringIndex; + }, + + update: function(reset) { + var me = this; + var chart = me.chart, + chartArea = chart.chartArea, + opts = chart.options, + arcOpts = opts.elements.arc, + availableWidth = chartArea.right - chartArea.left - arcOpts.borderWidth, + availableHeight = chartArea.bottom - chartArea.top - arcOpts.borderWidth, + minSize = Math.min(availableWidth, availableHeight), + offset = { + x: 0, + y: 0 + }, + meta = me.getMeta(), + cutoutPercentage = opts.cutoutPercentage, + circumference = opts.circumference; + + // If the chart's circumference isn't a full circle, calculate minSize as a ratio of the width/height of the arc + if (circumference < Math.PI * 2.0) { + var startAngle = opts.rotation % (Math.PI * 2.0); + startAngle += Math.PI * 2.0 * (startAngle >= Math.PI ? -1 : startAngle < -Math.PI ? 1 : 0); + var endAngle = startAngle + circumference; + var start = {x: Math.cos(startAngle), y: Math.sin(startAngle)}; + var end = {x: Math.cos(endAngle), y: Math.sin(endAngle)}; + var contains0 = (startAngle <= 0 && 0 <= endAngle) || (startAngle <= Math.PI * 2.0 && Math.PI * 2.0 <= endAngle); + var contains90 = (startAngle <= Math.PI * 0.5 && Math.PI * 0.5 <= endAngle) || (startAngle <= Math.PI * 2.5 && Math.PI * 2.5 <= endAngle); + var contains180 = (startAngle <= -Math.PI && -Math.PI <= endAngle) || (startAngle <= Math.PI && Math.PI <= endAngle); + var contains270 = (startAngle <= -Math.PI * 0.5 && -Math.PI * 0.5 <= endAngle) || (startAngle <= Math.PI * 1.5 && Math.PI * 1.5 <= endAngle); + var cutout = cutoutPercentage / 100.0; + var min = {x: contains180 ? -1 : Math.min(start.x * (start.x < 0 ? 1 : cutout), end.x * (end.x < 0 ? 1 : cutout)), y: contains270 ? -1 : Math.min(start.y * (start.y < 0 ? 1 : cutout), end.y * (end.y < 0 ? 1 : cutout))}; + var max = {x: contains0 ? 1 : Math.max(start.x * (start.x > 0 ? 1 : cutout), end.x * (end.x > 0 ? 1 : cutout)), y: contains90 ? 1 : Math.max(start.y * (start.y > 0 ? 1 : cutout), end.y * (end.y > 0 ? 1 : cutout))}; + var size = {width: (max.x - min.x) * 0.5, height: (max.y - min.y) * 0.5}; + minSize = Math.min(availableWidth / size.width, availableHeight / size.height); + offset = {x: (max.x + min.x) * -0.5, y: (max.y + min.y) * -0.5}; + } + + chart.borderWidth = me.getMaxBorderWidth(meta.data); + chart.outerRadius = Math.max((minSize - chart.borderWidth) / 2, 0); + chart.innerRadius = Math.max(cutoutPercentage ? (chart.outerRadius / 100) * (cutoutPercentage) : 0, 0); + chart.radiusLength = (chart.outerRadius - chart.innerRadius) / chart.getVisibleDatasetCount(); + chart.offsetX = offset.x * chart.outerRadius; + chart.offsetY = offset.y * chart.outerRadius; + + meta.total = me.calculateTotal(); + + me.outerRadius = chart.outerRadius - (chart.radiusLength * me.getRingIndex(me.index)); + me.innerRadius = Math.max(me.outerRadius - chart.radiusLength, 0); + + helpers.each(meta.data, function(arc, index) { + me.updateElement(arc, index, reset); + }); + }, + + updateElement: function(arc, index, reset) { + var me = this; + var chart = me.chart, + chartArea = chart.chartArea, + opts = chart.options, + animationOpts = opts.animation, + centerX = (chartArea.left + chartArea.right) / 2, + centerY = (chartArea.top + chartArea.bottom) / 2, + startAngle = opts.rotation, // non reset case handled later + endAngle = opts.rotation, // non reset case handled later + dataset = me.getDataset(), + circumference = reset && animationOpts.animateRotate ? 0 : arc.hidden ? 0 : me.calculateCircumference(dataset.data[index]) * (opts.circumference / (2.0 * Math.PI)), + innerRadius = reset && animationOpts.animateScale ? 0 : me.innerRadius, + outerRadius = reset && animationOpts.animateScale ? 0 : me.outerRadius, + valueAtIndexOrDefault = helpers.getValueAtIndexOrDefault; + + helpers.extend(arc, { + // Utility + _datasetIndex: me.index, + _index: index, + + // Desired view properties + _model: { + x: centerX + chart.offsetX, + y: centerY + chart.offsetY, + startAngle: startAngle, + endAngle: endAngle, + circumference: circumference, + outerRadius: outerRadius, + innerRadius: innerRadius, + label: valueAtIndexOrDefault(dataset.label, index, chart.data.labels[index]) + } + }); + + var model = arc._model; + // Resets the visual styles + this.removeHoverStyle(arc); + + // Set correct angles if not resetting + if (!reset || !animationOpts.animateRotate) { + if (index === 0) { + model.startAngle = opts.rotation; + } else { + model.startAngle = me.getMeta().data[index - 1]._model.endAngle; + } + + model.endAngle = model.startAngle + model.circumference; + } + + arc.pivot(); + }, + + removeHoverStyle: function(arc) { + Chart.DatasetController.prototype.removeHoverStyle.call(this, arc, this.chart.options.elements.arc); + }, + + calculateTotal: function() { + var dataset = this.getDataset(); + var meta = this.getMeta(); + var total = 0; + var value; + + helpers.each(meta.data, function(element, index) { + value = dataset.data[index]; + if (!isNaN(value) && !element.hidden) { + total += Math.abs(value); + } + }); + + /* if (total === 0) { + total = NaN; + }*/ + + return total; + }, + + calculateCircumference: function(value) { + var total = this.getMeta().total; + if (total > 0 && !isNaN(value)) { + return (Math.PI * 2.0) * (value / total); + } + return 0; + }, + + // gets the max border or hover width to properly scale pie charts + getMaxBorderWidth: function(elements) { + var max = 0, + index = this.index, + length = elements.length, + borderWidth, + hoverWidth; + + for (var i = 0; i < length; i++) { + borderWidth = elements[i]._model ? elements[i]._model.borderWidth : 0; + hoverWidth = elements[i]._chart ? elements[i]._chart.config.data.datasets[index].hoverBorderWidth : 0; + + max = borderWidth > max ? borderWidth : max; + max = hoverWidth > max ? hoverWidth : max; + } + return max; + } + }); +}; + +},{}],18:[function(require,module,exports){ +'use strict'; + +module.exports = function(Chart) { + + var helpers = Chart.helpers; + + Chart.defaults.line = { + showLines: true, + spanGaps: false, + + hover: { + mode: 'label' + }, + + scales: { + xAxes: [{ + type: 'category', + id: 'x-axis-0' + }], + yAxes: [{ + type: 'linear', + id: 'y-axis-0' + }] + } + }; + + function lineEnabled(dataset, options) { + return helpers.getValueOrDefault(dataset.showLine, options.showLines); + } + + Chart.controllers.line = Chart.DatasetController.extend({ + + datasetElementType: Chart.elements.Line, + + dataElementType: Chart.elements.Point, + + update: function(reset) { + var me = this; + var meta = me.getMeta(); + var line = meta.dataset; + var points = meta.data || []; + var options = me.chart.options; + var lineElementOptions = options.elements.line; + var scale = me.getScaleForId(meta.yAxisID); + var i, ilen, custom; + var dataset = me.getDataset(); + var showLine = lineEnabled(dataset, options); + + // Update Line + if (showLine) { + custom = line.custom || {}; + + // Compatibility: If the properties are defined with only the old name, use those values + if ((dataset.tension !== undefined) && (dataset.lineTension === undefined)) { + dataset.lineTension = dataset.tension; + } + + // Utility + line._scale = scale; + line._datasetIndex = me.index; + // Data + line._children = points; + // Model + line._model = { + // Appearance + // The default behavior of lines is to break at null values, according + // to https://github.com/chartjs/Chart.js/issues/2435#issuecomment-216718158 + // This option gives lines the ability to span gaps + spanGaps: dataset.spanGaps ? dataset.spanGaps : options.spanGaps, + tension: custom.tension ? custom.tension : helpers.getValueOrDefault(dataset.lineTension, lineElementOptions.tension), + backgroundColor: custom.backgroundColor ? custom.backgroundColor : (dataset.backgroundColor || lineElementOptions.backgroundColor), + borderWidth: custom.borderWidth ? custom.borderWidth : (dataset.borderWidth || lineElementOptions.borderWidth), + borderColor: custom.borderColor ? custom.borderColor : (dataset.borderColor || lineElementOptions.borderColor), + borderCapStyle: custom.borderCapStyle ? custom.borderCapStyle : (dataset.borderCapStyle || lineElementOptions.borderCapStyle), + borderDash: custom.borderDash ? custom.borderDash : (dataset.borderDash || lineElementOptions.borderDash), + borderDashOffset: custom.borderDashOffset ? custom.borderDashOffset : (dataset.borderDashOffset || lineElementOptions.borderDashOffset), + borderJoinStyle: custom.borderJoinStyle ? custom.borderJoinStyle : (dataset.borderJoinStyle || lineElementOptions.borderJoinStyle), + fill: custom.fill ? custom.fill : (dataset.fill !== undefined ? dataset.fill : lineElementOptions.fill), + steppedLine: custom.steppedLine ? custom.steppedLine : helpers.getValueOrDefault(dataset.steppedLine, lineElementOptions.stepped), + cubicInterpolationMode: custom.cubicInterpolationMode ? custom.cubicInterpolationMode : helpers.getValueOrDefault(dataset.cubicInterpolationMode, lineElementOptions.cubicInterpolationMode), + }; + + line.pivot(); + } + + // Update Points + for (i=0, ilen=points.length; i'); + + var data = chart.data; + var datasets = data.datasets; + var labels = data.labels; + + if (datasets.length) { + for (var i = 0; i < datasets[0].data.length; ++i) { + text.push('
  • '); + if (labels[i]) { + text.push(labels[i]); + } + text.push('
  • '); + } + } + + text.push(''); + return text.join(''); + }, + legend: { + labels: { + generateLabels: function(chart) { + var data = chart.data; + if (data.labels.length && data.datasets.length) { + return data.labels.map(function(label, i) { + var meta = chart.getDatasetMeta(0); + var ds = data.datasets[0]; + var arc = meta.data[i]; + var custom = arc.custom || {}; + var getValueAtIndexOrDefault = helpers.getValueAtIndexOrDefault; + var arcOpts = chart.options.elements.arc; + var fill = custom.backgroundColor ? custom.backgroundColor : getValueAtIndexOrDefault(ds.backgroundColor, i, arcOpts.backgroundColor); + var stroke = custom.borderColor ? custom.borderColor : getValueAtIndexOrDefault(ds.borderColor, i, arcOpts.borderColor); + var bw = custom.borderWidth ? custom.borderWidth : getValueAtIndexOrDefault(ds.borderWidth, i, arcOpts.borderWidth); + + return { + text: label, + fillStyle: fill, + strokeStyle: stroke, + lineWidth: bw, + hidden: isNaN(ds.data[i]) || meta.data[i].hidden, + + // Extra data used for toggling the correct item + index: i + }; + }); + } + return []; + } + }, + + onClick: function(e, legendItem) { + var index = legendItem.index; + var chart = this.chart; + var i, ilen, meta; + + for (i = 0, ilen = (chart.data.datasets || []).length; i < ilen; ++i) { + meta = chart.getDatasetMeta(i); + meta.data[index].hidden = !meta.data[index].hidden; + } + + chart.update(); + } + }, + + // Need to override these to give a nice default + tooltips: { + callbacks: { + title: function() { + return ''; + }, + label: function(tooltipItem, data) { + return data.labels[tooltipItem.index] + ': ' + tooltipItem.yLabel; + } + } + } + }; + + Chart.controllers.polarArea = Chart.DatasetController.extend({ + + dataElementType: Chart.elements.Arc, + + linkScales: helpers.noop, + + update: function(reset) { + var me = this; + var chart = me.chart; + var chartArea = chart.chartArea; + var meta = me.getMeta(); + var opts = chart.options; + var arcOpts = opts.elements.arc; + var minSize = Math.min(chartArea.right - chartArea.left, chartArea.bottom - chartArea.top); + chart.outerRadius = Math.max((minSize - arcOpts.borderWidth / 2) / 2, 0); + chart.innerRadius = Math.max(opts.cutoutPercentage ? (chart.outerRadius / 100) * (opts.cutoutPercentage) : 1, 0); + chart.radiusLength = (chart.outerRadius - chart.innerRadius) / chart.getVisibleDatasetCount(); + + me.outerRadius = chart.outerRadius - (chart.radiusLength * me.index); + me.innerRadius = me.outerRadius - chart.radiusLength; + + meta.count = me.countVisibleElements(); + + helpers.each(meta.data, function(arc, index) { + me.updateElement(arc, index, reset); + }); + }, + + updateElement: function(arc, index, reset) { + var me = this; + var chart = me.chart; + var dataset = me.getDataset(); + var opts = chart.options; + var animationOpts = opts.animation; + var scale = chart.scale; + var getValueAtIndexOrDefault = helpers.getValueAtIndexOrDefault; + var labels = chart.data.labels; + + var circumference = me.calculateCircumference(dataset.data[index]); + var centerX = scale.xCenter; + var centerY = scale.yCenter; + + // If there is NaN data before us, we need to calculate the starting angle correctly. + // We could be way more efficient here, but its unlikely that the polar area chart will have a lot of data + var visibleCount = 0; + var meta = me.getMeta(); + for (var i = 0; i < index; ++i) { + if (!isNaN(dataset.data[i]) && !meta.data[i].hidden) { + ++visibleCount; + } + } + + // var negHalfPI = -0.5 * Math.PI; + var datasetStartAngle = opts.startAngle; + var distance = arc.hidden ? 0 : scale.getDistanceFromCenterForValue(dataset.data[index]); + var startAngle = datasetStartAngle + (circumference * visibleCount); + var endAngle = startAngle + (arc.hidden ? 0 : circumference); + + var resetRadius = animationOpts.animateScale ? 0 : scale.getDistanceFromCenterForValue(dataset.data[index]); + + helpers.extend(arc, { + // Utility + _datasetIndex: me.index, + _index: index, + _scale: scale, + + // Desired view properties + _model: { + x: centerX, + y: centerY, + innerRadius: 0, + outerRadius: reset ? resetRadius : distance, + startAngle: reset && animationOpts.animateRotate ? datasetStartAngle : startAngle, + endAngle: reset && animationOpts.animateRotate ? datasetStartAngle : endAngle, + label: getValueAtIndexOrDefault(labels, index, labels[index]) + } + }); + + // Apply border and fill style + me.removeHoverStyle(arc); + + arc.pivot(); + }, + + removeHoverStyle: function(arc) { + Chart.DatasetController.prototype.removeHoverStyle.call(this, arc, this.chart.options.elements.arc); + }, + + countVisibleElements: function() { + var dataset = this.getDataset(); + var meta = this.getMeta(); + var count = 0; + + helpers.each(meta.data, function(element, index) { + if (!isNaN(dataset.data[index]) && !element.hidden) { + count++; + } + }); + + return count; + }, + + calculateCircumference: function(value) { + var count = this.getMeta().count; + if (count > 0 && !isNaN(value)) { + return (2 * Math.PI) / count; + } + return 0; + } + }); +}; + +},{}],20:[function(require,module,exports){ +'use strict'; + +module.exports = function(Chart) { + + var helpers = Chart.helpers; + + Chart.defaults.radar = { + aspectRatio: 1, + scale: { + type: 'radialLinear' + }, + elements: { + line: { + tension: 0 // no bezier in radar + } + } + }; + + Chart.controllers.radar = Chart.DatasetController.extend({ + + datasetElementType: Chart.elements.Line, + + dataElementType: Chart.elements.Point, + + linkScales: helpers.noop, + + update: function(reset) { + var me = this; + var meta = me.getMeta(); + var line = meta.dataset; + var points = meta.data; + var custom = line.custom || {}; + var dataset = me.getDataset(); + var lineElementOptions = me.chart.options.elements.line; + var scale = me.chart.scale; + + // Compatibility: If the properties are defined with only the old name, use those values + if ((dataset.tension !== undefined) && (dataset.lineTension === undefined)) { + dataset.lineTension = dataset.tension; + } + + helpers.extend(meta.dataset, { + // Utility + _datasetIndex: me.index, + _scale: scale, + // Data + _children: points, + _loop: true, + // Model + _model: { + // Appearance + tension: custom.tension ? custom.tension : helpers.getValueOrDefault(dataset.lineTension, lineElementOptions.tension), + backgroundColor: custom.backgroundColor ? custom.backgroundColor : (dataset.backgroundColor || lineElementOptions.backgroundColor), + borderWidth: custom.borderWidth ? custom.borderWidth : (dataset.borderWidth || lineElementOptions.borderWidth), + borderColor: custom.borderColor ? custom.borderColor : (dataset.borderColor || lineElementOptions.borderColor), + fill: custom.fill ? custom.fill : (dataset.fill !== undefined ? dataset.fill : lineElementOptions.fill), + borderCapStyle: custom.borderCapStyle ? custom.borderCapStyle : (dataset.borderCapStyle || lineElementOptions.borderCapStyle), + borderDash: custom.borderDash ? custom.borderDash : (dataset.borderDash || lineElementOptions.borderDash), + borderDashOffset: custom.borderDashOffset ? custom.borderDashOffset : (dataset.borderDashOffset || lineElementOptions.borderDashOffset), + borderJoinStyle: custom.borderJoinStyle ? custom.borderJoinStyle : (dataset.borderJoinStyle || lineElementOptions.borderJoinStyle), + } + }); + + meta.dataset.pivot(); + + // Update Points + helpers.each(points, function(point, index) { + me.updateElement(point, index, reset); + }, me); + + // Update bezier control points + me.updateBezierControlPoints(); + }, + updateElement: function(point, index, reset) { + var me = this; + var custom = point.custom || {}; + var dataset = me.getDataset(); + var scale = me.chart.scale; + var pointElementOptions = me.chart.options.elements.point; + var pointPosition = scale.getPointPositionForValue(index, dataset.data[index]); + + // Compatibility: If the properties are defined with only the old name, use those values + if ((dataset.radius !== undefined) && (dataset.pointRadius === undefined)) { + dataset.pointRadius = dataset.radius; + } + if ((dataset.hitRadius !== undefined) && (dataset.pointHitRadius === undefined)) { + dataset.pointHitRadius = dataset.hitRadius; + } + + helpers.extend(point, { + // Utility + _datasetIndex: me.index, + _index: index, + _scale: scale, + + // Desired view properties + _model: { + x: reset ? scale.xCenter : pointPosition.x, // value not used in dataset scale, but we want a consistent API between scales + y: reset ? scale.yCenter : pointPosition.y, + + // Appearance + tension: custom.tension ? custom.tension : helpers.getValueOrDefault(dataset.lineTension, me.chart.options.elements.line.tension), + radius: custom.radius ? custom.radius : helpers.getValueAtIndexOrDefault(dataset.pointRadius, index, pointElementOptions.radius), + backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointBackgroundColor, index, pointElementOptions.backgroundColor), + borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.pointBorderColor, index, pointElementOptions.borderColor), + borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, pointElementOptions.borderWidth), + pointStyle: custom.pointStyle ? custom.pointStyle : helpers.getValueAtIndexOrDefault(dataset.pointStyle, index, pointElementOptions.pointStyle), + + // Tooltip + hitRadius: custom.hitRadius ? custom.hitRadius : helpers.getValueAtIndexOrDefault(dataset.pointHitRadius, index, pointElementOptions.hitRadius) + } + }); + + point._model.skip = custom.skip ? custom.skip : (isNaN(point._model.x) || isNaN(point._model.y)); + }, + updateBezierControlPoints: function() { + var chartArea = this.chart.chartArea; + var meta = this.getMeta(); + + helpers.each(meta.data, function(point, index) { + var model = point._model; + var controlPoints = helpers.splineCurve( + helpers.previousItem(meta.data, index, true)._model, + model, + helpers.nextItem(meta.data, index, true)._model, + model.tension + ); + + // Prevent the bezier going outside of the bounds of the graph + model.controlPointPreviousX = Math.max(Math.min(controlPoints.previous.x, chartArea.right), chartArea.left); + model.controlPointPreviousY = Math.max(Math.min(controlPoints.previous.y, chartArea.bottom), chartArea.top); + + model.controlPointNextX = Math.max(Math.min(controlPoints.next.x, chartArea.right), chartArea.left); + model.controlPointNextY = Math.max(Math.min(controlPoints.next.y, chartArea.bottom), chartArea.top); + + // Now pivot the point for animation + point.pivot(); + }); + }, + + setHoverStyle: function(point) { + // Point + var dataset = this.chart.data.datasets[point._datasetIndex]; + var custom = point.custom || {}; + var index = point._index; + var model = point._model; + + model.radius = custom.hoverRadius ? custom.hoverRadius : helpers.getValueAtIndexOrDefault(dataset.pointHoverRadius, index, this.chart.options.elements.point.hoverRadius); + model.backgroundColor = custom.hoverBackgroundColor ? custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBackgroundColor, index, helpers.getHoverColor(model.backgroundColor)); + model.borderColor = custom.hoverBorderColor ? custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.pointHoverBorderColor, index, helpers.getHoverColor(model.borderColor)); + model.borderWidth = custom.hoverBorderWidth ? custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.pointHoverBorderWidth, index, model.borderWidth); + }, + + removeHoverStyle: function(point) { + var dataset = this.chart.data.datasets[point._datasetIndex]; + var custom = point.custom || {}; + var index = point._index; + var model = point._model; + var pointElementOptions = this.chart.options.elements.point; + + model.radius = custom.radius ? custom.radius : helpers.getValueAtIndexOrDefault(dataset.pointRadius, index, pointElementOptions.radius); + model.backgroundColor = custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.pointBackgroundColor, index, pointElementOptions.backgroundColor); + model.borderColor = custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.pointBorderColor, index, pointElementOptions.borderColor); + model.borderWidth = custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, pointElementOptions.borderWidth); + } + }); +}; + +},{}],21:[function(require,module,exports){ +/* global window: false */ +'use strict'; + +module.exports = function(Chart) { + + var helpers = Chart.helpers; + + Chart.defaults.global.animation = { + duration: 1000, + easing: 'easeOutQuart', + onProgress: helpers.noop, + onComplete: helpers.noop + }; + + Chart.Animation = Chart.Element.extend({ + chart: null, // the animation associated chart instance + currentStep: 0, // the current animation step + numSteps: 60, // default number of steps + easing: '', // the easing to use for this animation + render: null, // render function used by the animation service + + onAnimationProgress: null, // user specified callback to fire on each step of the animation + onAnimationComplete: null, // user specified callback to fire when the animation finishes + }); + + Chart.animationService = { + frameDuration: 17, + animations: [], + dropFrames: 0, + request: null, + + /** + * @param {Chart} chart - The chart to animate. + * @param {Chart.Animation} animation - The animation that we will animate. + * @param {Number} duration - The animation duration in ms. + * @param {Boolean} lazy - if true, the chart is not marked as animating to enable more responsive interactions + */ + addAnimation: function(chart, animation, duration, lazy) { + var animations = this.animations; + var i, ilen; + + animation.chart = chart; + + if (!lazy) { + chart.animating = true; + } + + for (i=0, ilen=animations.length; i < ilen; ++i) { + if (animations[i].chart === chart) { + animations[i] = animation; + return; + } + } + + animations.push(animation); + + // If there are no animations queued, manually kickstart a digest, for lack of a better word + if (animations.length === 1) { + this.requestAnimationFrame(); + } + }, + + cancelAnimation: function(chart) { + var index = helpers.findIndex(this.animations, function(animation) { + return animation.chart === chart; + }); + + if (index !== -1) { + this.animations.splice(index, 1); + chart.animating = false; + } + }, + + requestAnimationFrame: function() { + var me = this; + if (me.request === null) { + // Skip animation frame requests until the active one is executed. + // This can happen when processing mouse events, e.g. 'mousemove' + // and 'mouseout' events will trigger multiple renders. + me.request = helpers.requestAnimFrame.call(window, function() { + me.request = null; + me.startDigest(); + }); + } + }, + + /** + * @private + */ + startDigest: function() { + var me = this; + var startTime = Date.now(); + var framesToDrop = 0; + + if (me.dropFrames > 1) { + framesToDrop = Math.floor(me.dropFrames); + me.dropFrames = me.dropFrames % 1; + } + + me.advance(1 + framesToDrop); + + var endTime = Date.now(); + + me.dropFrames += (endTime - startTime) / me.frameDuration; + + // Do we have more stuff to animate? + if (me.animations.length > 0) { + me.requestAnimationFrame(); + } + }, + + /** + * @private + */ + advance: function(count) { + var animations = this.animations; + var animation, chart; + var i = 0; + + while (i < animations.length) { + animation = animations[i]; + chart = animation.chart; + + animation.currentStep = (animation.currentStep || 0) + count; + animation.currentStep = Math.min(animation.currentStep, animation.numSteps); + + helpers.callback(animation.render, [chart, animation], chart); + helpers.callback(animation.onAnimationProgress, [animation], chart); + + if (animation.currentStep >= animation.numSteps) { + helpers.callback(animation.onAnimationComplete, [animation], chart); + chart.animating = false; + animations.splice(i, 1); + } else { + ++i; + } + } + } + }; + + /** + * Provided for backward compatibility, use Chart.Animation instead + * @prop Chart.Animation#animationObject + * @deprecated since version 2.6.0 + * @todo remove at version 3 + */ + Object.defineProperty(Chart.Animation.prototype, 'animationObject', { + get: function() { + return this; + } + }); + + /** + * Provided for backward compatibility, use Chart.Animation#chart instead + * @prop Chart.Animation#chartInstance + * @deprecated since version 2.6.0 + * @todo remove at version 3 + */ + Object.defineProperty(Chart.Animation.prototype, 'chartInstance', { + get: function() { + return this.chart; + }, + set: function(value) { + this.chart = value; + } + }); + +}; + +},{}],22:[function(require,module,exports){ +'use strict'; + +module.exports = function(Chart) { + // Global Chart canvas helpers object for drawing items to canvas + var helpers = Chart.canvasHelpers = {}; + + helpers.drawPoint = function(ctx, pointStyle, radius, x, y) { + var type, edgeLength, xOffset, yOffset, height, size; + + if (typeof pointStyle === 'object') { + type = pointStyle.toString(); + if (type === '[object HTMLImageElement]' || type === '[object HTMLCanvasElement]') { + ctx.drawImage(pointStyle, x - pointStyle.width / 2, y - pointStyle.height / 2, pointStyle.width, pointStyle.height); + return; + } + } + + if (isNaN(radius) || radius <= 0) { + return; + } + + switch (pointStyle) { + // Default includes circle + default: + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.closePath(); + ctx.fill(); + break; + case 'triangle': + ctx.beginPath(); + edgeLength = 3 * radius / Math.sqrt(3); + height = edgeLength * Math.sqrt(3) / 2; + ctx.moveTo(x - edgeLength / 2, y + height / 3); + ctx.lineTo(x + edgeLength / 2, y + height / 3); + ctx.lineTo(x, y - 2 * height / 3); + ctx.closePath(); + ctx.fill(); + break; + case 'rect': + size = 1 / Math.SQRT2 * radius; + ctx.beginPath(); + ctx.fillRect(x - size, y - size, 2 * size, 2 * size); + ctx.strokeRect(x - size, y - size, 2 * size, 2 * size); + break; + case 'rectRounded': + var offset = radius / Math.SQRT2; + var leftX = x - offset; + var topY = y - offset; + var sideSize = Math.SQRT2 * radius; + Chart.helpers.drawRoundedRectangle(ctx, leftX, topY, sideSize, sideSize, radius / 2); + ctx.fill(); + break; + case 'rectRot': + size = 1 / Math.SQRT2 * radius; + ctx.beginPath(); + ctx.moveTo(x - size, y); + ctx.lineTo(x, y + size); + ctx.lineTo(x + size, y); + ctx.lineTo(x, y - size); + ctx.closePath(); + ctx.fill(); + break; + case 'cross': + ctx.beginPath(); + ctx.moveTo(x, y + radius); + ctx.lineTo(x, y - radius); + ctx.moveTo(x - radius, y); + ctx.lineTo(x + radius, y); + ctx.closePath(); + break; + case 'crossRot': + ctx.beginPath(); + xOffset = Math.cos(Math.PI / 4) * radius; + yOffset = Math.sin(Math.PI / 4) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.moveTo(x - xOffset, y + yOffset); + ctx.lineTo(x + xOffset, y - yOffset); + ctx.closePath(); + break; + case 'star': + ctx.beginPath(); + ctx.moveTo(x, y + radius); + ctx.lineTo(x, y - radius); + ctx.moveTo(x - radius, y); + ctx.lineTo(x + radius, y); + xOffset = Math.cos(Math.PI / 4) * radius; + yOffset = Math.sin(Math.PI / 4) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + ctx.moveTo(x - xOffset, y + yOffset); + ctx.lineTo(x + xOffset, y - yOffset); + ctx.closePath(); + break; + case 'line': + ctx.beginPath(); + ctx.moveTo(x - radius, y); + ctx.lineTo(x + radius, y); + ctx.closePath(); + break; + case 'dash': + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x + radius, y); + ctx.closePath(); + break; + } + + ctx.stroke(); + }; + + helpers.clipArea = function(ctx, clipArea) { + ctx.save(); + ctx.beginPath(); + ctx.rect(clipArea.left, clipArea.top, clipArea.right - clipArea.left, clipArea.bottom - clipArea.top); + ctx.clip(); + }; + + helpers.unclipArea = function(ctx) { + ctx.restore(); + }; + + helpers.lineTo = function(ctx, previous, target, flip) { + if (target.steppedLine) { + if (target.steppedLine === 'after') { + ctx.lineTo(previous.x, target.y); + } else { + ctx.lineTo(target.x, previous.y); + } + ctx.lineTo(target.x, target.y); + return; + } + + if (!target.tension) { + ctx.lineTo(target.x, target.y); + return; + } + + ctx.bezierCurveTo( + flip? previous.controlPointPreviousX : previous.controlPointNextX, + flip? previous.controlPointPreviousY : previous.controlPointNextY, + flip? target.controlPointNextX : target.controlPointPreviousX, + flip? target.controlPointNextY : target.controlPointPreviousY, + target.x, + target.y); + }; + + Chart.helpers.canvas = helpers; +}; + +},{}],23:[function(require,module,exports){ +'use strict'; + +module.exports = function(Chart) { + + var helpers = Chart.helpers; + var plugins = Chart.plugins; + var platform = Chart.platform; + + // Create a dictionary of chart types, to allow for extension of existing types + Chart.types = {}; + + // Store a reference to each instance - allowing us to globally resize chart instances on window resize. + // Destroy method on the chart will remove the instance of the chart from this reference. + Chart.instances = {}; + + // Controllers available for dataset visualization eg. bar, line, slice, etc. + Chart.controllers = {}; + + /** + * Initializes the given config with global and chart default values. + */ + function initConfig(config) { + config = config || {}; + + // Do NOT use configMerge() for the data object because this method merges arrays + // and so would change references to labels and datasets, preventing data updates. + var data = config.data = config.data || {}; + data.datasets = data.datasets || []; + data.labels = data.labels || []; + + config.options = helpers.configMerge( + Chart.defaults.global, + Chart.defaults[config.type], + config.options || {}); + + return config; + } + + /** + * Updates the config of the chart + * @param chart {Chart} chart to update the options for + */ + function updateConfig(chart) { + var newOptions = chart.options; + + // Update Scale(s) with options + if (newOptions.scale) { + chart.scale.options = newOptions.scale; + } else if (newOptions.scales) { + newOptions.scales.xAxes.concat(newOptions.scales.yAxes).forEach(function(scaleOptions) { + chart.scales[scaleOptions.id].options = scaleOptions; + }); + } + + // Tooltip + chart.tooltip._options = newOptions.tooltips; + } + + function positionIsHorizontal(position) { + return position === 'top' || position === 'bottom'; + } + + helpers.extend(Chart.prototype, /** @lends Chart */ { + /** + * @private + */ + construct: function(item, config) { + var me = this; + + config = initConfig(config); + + var context = platform.acquireContext(item, config); + var canvas = context && context.canvas; + var height = canvas && canvas.height; + var width = canvas && canvas.width; + + me.id = helpers.uid(); + me.ctx = context; + me.canvas = canvas; + me.config = config; + me.width = width; + me.height = height; + me.aspectRatio = height? width / height : null; + me.options = config.options; + me._bufferedRender = false; + + /** + * Provided for backward compatibility, Chart and Chart.Controller have been merged, + * the "instance" still need to be defined since it might be called from plugins. + * @prop Chart#chart + * @deprecated since version 2.6.0 + * @todo remove at version 3 + * @private + */ + me.chart = me; + me.controller = me; // chart.chart.controller #inception + + // Add the chart instance to the global namespace + Chart.instances[me.id] = me; + + // Define alias to the config data: `chart.data === chart.config.data` + Object.defineProperty(me, 'data', { + get: function() { + return me.config.data; + }, + set: function(value) { + me.config.data = value; + } + }); + + if (!context || !canvas) { + // The given item is not a compatible context2d element, let's return before finalizing + // the chart initialization but after setting basic chart / controller properties that + // can help to figure out that the chart is not valid (e.g chart.canvas !== null); + // https://github.com/chartjs/Chart.js/issues/2807 + console.error("Failed to create chart: can't acquire context from the given item"); + return; + } + + me.initialize(); + me.update(); + }, + + /** + * @private + */ + initialize: function() { + var me = this; + + // Before init plugin notification + plugins.notify(me, 'beforeInit'); + + helpers.retinaScale(me); + + me.bindEvents(); + + if (me.options.responsive) { + // Initial resize before chart draws (must be silent to preserve initial animations). + me.resize(true); + } + + // Make sure scales have IDs and are built before we build any controllers. + me.ensureScalesHaveIDs(); + me.buildScales(); + me.initToolTip(); + + // After init plugin notification + plugins.notify(me, 'afterInit'); + + return me; + }, + + clear: function() { + helpers.clear(this); + return this; + }, + + stop: function() { + // Stops any current animation loop occurring + Chart.animationService.cancelAnimation(this); + return this; + }, + + resize: function(silent) { + var me = this; + var options = me.options; + var canvas = me.canvas; + var aspectRatio = (options.maintainAspectRatio && me.aspectRatio) || null; + + // the canvas render width and height will be casted to integers so make sure that + // the canvas display style uses the same integer values to avoid blurring effect. + var newWidth = Math.floor(helpers.getMaximumWidth(canvas)); + var newHeight = Math.floor(aspectRatio? newWidth / aspectRatio : helpers.getMaximumHeight(canvas)); + + if (me.width === newWidth && me.height === newHeight) { + return; + } + + canvas.width = me.width = newWidth; + canvas.height = me.height = newHeight; + canvas.style.width = newWidth + 'px'; + canvas.style.height = newHeight + 'px'; + + helpers.retinaScale(me); + + if (!silent) { + // Notify any plugins about the resize + var newSize = {width: newWidth, height: newHeight}; + plugins.notify(me, 'resize', [newSize]); + + // Notify of resize + if (me.options.onResize) { + me.options.onResize(me, newSize); + } + + me.stop(); + me.update(me.options.responsiveAnimationDuration); + } + }, + + ensureScalesHaveIDs: function() { + var options = this.options; + var scalesOptions = options.scales || {}; + var scaleOptions = options.scale; + + helpers.each(scalesOptions.xAxes, function(xAxisOptions, index) { + xAxisOptions.id = xAxisOptions.id || ('x-axis-' + index); + }); + + helpers.each(scalesOptions.yAxes, function(yAxisOptions, index) { + yAxisOptions.id = yAxisOptions.id || ('y-axis-' + index); + }); + + if (scaleOptions) { + scaleOptions.id = scaleOptions.id || 'scale'; + } + }, + + /** + * Builds a map of scale ID to scale object for future lookup. + */ + buildScales: function() { + var me = this; + var options = me.options; + var scales = me.scales = {}; + var items = []; + + if (options.scales) { + items = items.concat( + (options.scales.xAxes || []).map(function(xAxisOptions) { + return {options: xAxisOptions, dtype: 'category', dposition: 'bottom'}; + }), + (options.scales.yAxes || []).map(function(yAxisOptions) { + return {options: yAxisOptions, dtype: 'linear', dposition: 'left'}; + }) + ); + } + + if (options.scale) { + items.push({ + options: options.scale, + dtype: 'radialLinear', + isDefault: true, + dposition: 'chartArea' + }); + } + + helpers.each(items, function(item) { + var scaleOptions = item.options; + var scaleType = helpers.getValueOrDefault(scaleOptions.type, item.dtype); + var scaleClass = Chart.scaleService.getScaleConstructor(scaleType); + if (!scaleClass) { + return; + } + + if (positionIsHorizontal(scaleOptions.position) !== positionIsHorizontal(item.dposition)) { + scaleOptions.position = item.dposition; + } + + var scale = new scaleClass({ + id: scaleOptions.id, + options: scaleOptions, + ctx: me.ctx, + chart: me + }); + + scales[scale.id] = scale; + + // TODO(SB): I think we should be able to remove this custom case (options.scale) + // and consider it as a regular scale part of the "scales"" map only! This would + // make the logic easier and remove some useless? custom code. + if (item.isDefault) { + me.scale = scale; + } + }); + + Chart.scaleService.addScalesToLayout(this); + }, + + buildOrUpdateControllers: function() { + var me = this; + var types = []; + var newControllers = []; + + helpers.each(me.data.datasets, function(dataset, datasetIndex) { + var meta = me.getDatasetMeta(datasetIndex); + if (!meta.type) { + meta.type = dataset.type || me.config.type; + } + + types.push(meta.type); + + if (meta.controller) { + meta.controller.updateIndex(datasetIndex); + } else { + var ControllerClass = Chart.controllers[meta.type]; + if (ControllerClass === undefined) { + throw new Error('"' + meta.type + '" is not a chart type.'); + } + + meta.controller = new ControllerClass(me, datasetIndex); + newControllers.push(meta.controller); + } + }, me); + + if (types.length > 1) { + for (var i = 1; i < types.length; i++) { + if (types[i] !== types[i - 1]) { + me.isCombo = true; + break; + } + } + } + + return newControllers; + }, + + /** + * Reset the elements of all datasets + * @private + */ + resetElements: function() { + var me = this; + helpers.each(me.data.datasets, function(dataset, datasetIndex) { + me.getDatasetMeta(datasetIndex).controller.reset(); + }, me); + }, + + /** + * Resets the chart back to it's state before the initial animation + */ + reset: function() { + this.resetElements(); + this.tooltip.initialize(); + }, + + update: function(animationDuration, lazy) { + var me = this; + + updateConfig(me); + + if (plugins.notify(me, 'beforeUpdate') === false) { + return; + } + + // In case the entire data object changed + me.tooltip._data = me.data; + + // Make sure dataset controllers are updated and new controllers are reset + var newControllers = me.buildOrUpdateControllers(); + + // Make sure all dataset controllers have correct meta data counts + helpers.each(me.data.datasets, function(dataset, datasetIndex) { + me.getDatasetMeta(datasetIndex).controller.buildOrUpdateElements(); + }, me); + + me.updateLayout(); + + // Can only reset the new controllers after the scales have been updated + helpers.each(newControllers, function(controller) { + controller.reset(); + }); + + me.updateDatasets(); + + // Do this before render so that any plugins that need final scale updates can use it + plugins.notify(me, 'afterUpdate'); + + if (me._bufferedRender) { + me._bufferedRequest = { + lazy: lazy, + duration: animationDuration + }; + } else { + me.render(animationDuration, lazy); + } + }, + + /** + * Updates the chart layout unless a plugin returns `false` to the `beforeLayout` + * hook, in which case, plugins will not be called on `afterLayout`. + * @private + */ + updateLayout: function() { + var me = this; + + if (plugins.notify(me, 'beforeLayout') === false) { + return; + } + + Chart.layoutService.update(this, this.width, this.height); + + /** + * Provided for backward compatibility, use `afterLayout` instead. + * @method IPlugin#afterScaleUpdate + * @deprecated since version 2.5.0 + * @todo remove at version 3 + * @private + */ + plugins.notify(me, 'afterScaleUpdate'); + plugins.notify(me, 'afterLayout'); + }, + + /** + * Updates all datasets unless a plugin returns `false` to the `beforeDatasetsUpdate` + * hook, in which case, plugins will not be called on `afterDatasetsUpdate`. + * @private + */ + updateDatasets: function() { + var me = this; + + if (plugins.notify(me, 'beforeDatasetsUpdate') === false) { + return; + } + + for (var i = 0, ilen = me.data.datasets.length; i < ilen; ++i) { + me.updateDataset(i); + } + + plugins.notify(me, 'afterDatasetsUpdate'); + }, + + /** + * Updates dataset at index unless a plugin returns `false` to the `beforeDatasetUpdate` + * hook, in which case, plugins will not be called on `afterDatasetUpdate`. + * @private + */ + updateDataset: function(index) { + var me = this; + var meta = me.getDatasetMeta(index); + var args = { + meta: meta, + index: index + }; + + if (plugins.notify(me, 'beforeDatasetUpdate', [args]) === false) { + return; + } + + meta.controller.update(); + + plugins.notify(me, 'afterDatasetUpdate', [args]); + }, + + render: function(duration, lazy) { + var me = this; + + if (plugins.notify(me, 'beforeRender') === false) { + return; + } + + var animationOptions = me.options.animation; + var onComplete = function(animation) { + plugins.notify(me, 'afterRender'); + helpers.callback(animationOptions && animationOptions.onComplete, [animation], me); + }; + + if (animationOptions && ((typeof duration !== 'undefined' && duration !== 0) || (typeof duration === 'undefined' && animationOptions.duration !== 0))) { + var animation = new Chart.Animation({ + numSteps: (duration || animationOptions.duration) / 16.66, // 60 fps + easing: animationOptions.easing, + + render: function(chart, animationObject) { + var easingFunction = helpers.easingEffects[animationObject.easing]; + var currentStep = animationObject.currentStep; + var stepDecimal = currentStep / animationObject.numSteps; + + chart.draw(easingFunction(stepDecimal), stepDecimal, currentStep); + }, + + onAnimationProgress: animationOptions.onProgress, + onAnimationComplete: onComplete + }); + + Chart.animationService.addAnimation(me, animation, duration, lazy); + } else { + me.draw(); + + // See https://github.com/chartjs/Chart.js/issues/3781 + onComplete(new Chart.Animation({numSteps: 0, chart: me})); + } + + return me; + }, + + draw: function(easingValue) { + var me = this; + + me.clear(); + + if (easingValue === undefined || easingValue === null) { + easingValue = 1; + } + + me.transition(easingValue); + + if (plugins.notify(me, 'beforeDraw', [easingValue]) === false) { + return; + } + + // Draw all the scales + helpers.each(me.boxes, function(box) { + box.draw(me.chartArea); + }, me); + + if (me.scale) { + me.scale.draw(); + } + + me.drawDatasets(easingValue); + + // Finally draw the tooltip + me.tooltip.draw(); + + plugins.notify(me, 'afterDraw', [easingValue]); + }, + + /** + * @private + */ + transition: function(easingValue) { + var me = this; + + for (var i=0, ilen=(me.data.datasets || []).length; i= 0; --i) { + if (me.isDatasetVisible(i)) { + me.drawDataset(i, easingValue); + } + } + + plugins.notify(me, 'afterDatasetsDraw', [easingValue]); + }, + + /** + * Draws dataset at index unless a plugin returns `false` to the `beforeDatasetDraw` + * hook, in which case, plugins will not be called on `afterDatasetDraw`. + * @private + */ + drawDataset: function(index, easingValue) { + var me = this; + var meta = me.getDatasetMeta(index); + var args = { + meta: meta, + index: index, + easingValue: easingValue + }; + + if (plugins.notify(me, 'beforeDatasetDraw', [args]) === false) { + return; + } + + meta.controller.draw(easingValue); + + plugins.notify(me, 'afterDatasetDraw', [args]); + }, + + // Get the single element that was clicked on + // @return : An object containing the dataset index and element index of the matching element. Also contains the rectangle that was draw + getElementAtEvent: function(e) { + return Chart.Interaction.modes.single(this, e); + }, + + getElementsAtEvent: function(e) { + return Chart.Interaction.modes.label(this, e, {intersect: true}); + }, + + getElementsAtXAxis: function(e) { + return Chart.Interaction.modes['x-axis'](this, e, {intersect: true}); + }, + + getElementsAtEventForMode: function(e, mode, options) { + var method = Chart.Interaction.modes[mode]; + if (typeof method === 'function') { + return method(this, e, options); + } + + return []; + }, + + getDatasetAtEvent: function(e) { + return Chart.Interaction.modes.dataset(this, e, {intersect: true}); + }, + + getDatasetMeta: function(datasetIndex) { + var me = this; + var dataset = me.data.datasets[datasetIndex]; + if (!dataset._meta) { + dataset._meta = {}; + } + + var meta = dataset._meta[me.id]; + if (!meta) { + meta = dataset._meta[me.id] = { + type: null, + data: [], + dataset: null, + controller: null, + hidden: null, // See isDatasetVisible() comment + xAxisID: null, + yAxisID: null + }; + } + + return meta; + }, + + getVisibleDatasetCount: function() { + var count = 0; + for (var i = 0, ilen = this.data.datasets.length; i