diff --git a/.gitignore b/.gitignore
index 7f8262b..d01ece3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,7 @@ dist/
downloads/
test/
tmp/
+temp/
var/
*.log
*.list
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 28e25af..3ec4bde 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -30,6 +30,19 @@
],
"console": "integratedTerminal",
"justMyCode": true
+ },
+ {
+ "name": "Python: cctv-scheduler -c -w -3",
+ "type": "python",
+ "request": "launch",
+ "program": "${file}",
+ "args": [
+ "-c",
+ "-w",
+ "-1"
+ ],
+ "console": "integratedTerminal",
+ "justMyCode": true
}
]
}
\ No newline at end of file
diff --git a/README.md b/README.md
index 21d3a03..0b9631d 100644
--- a/README.md
+++ b/README.md
@@ -4,12 +4,11 @@ PTZ IP-Camera management
____
- [`cctv-scheduler.py`](https://git.hmp.today/pavel.muhortov/cctv-scheduler#cctv-scheduler-py)
-- [`converter.sh`](https://git.hmp.today/pavel.muhortov/cctv-scheduler#converter-sh)
- [`publisher.sh`](https://git.hmp.today/pavel.muhortov/cctv-scheduler#publisher-sh)
____
-![cctv-scheduler](info/images/cctv-scheduler-0.3.png)
+![cctv-scheduler](info/images/cctv-scheduler-0.4.png)
## `Installation`
@@ -31,8 +30,6 @@ Download scripts and configs.
```bash
wget https://git.hmp.today/pavel.muhortov/cctv-scheduler/raw/branch/master/cctv-scheduler.py -O /home/user/cctv-scheduler/cctv-scheduler.py
wget https://git.hmp.today/pavel.muhortov/cctv-scheduler/raw/branch/master/cctv-scheduler.conf -O /home/user/cctv-scheduler/cctv-scheduler.conf
-wget https://git.hmp.today/pavel.muhortov/cctv-scheduler/raw/branch/master/converter.sh -O /home/user/cctv-scheduler/converter.sh
-wget https://git.hmp.today/pavel.muhortov/cctv-scheduler/raw/branch/master/converter.conf -O /home/user/cctv-scheduler/converter.conf
wget https://git.hmp.today/pavel.muhortov/cctv-scheduler/raw/branch/master/publisher.sh -O /home/user/cctv-scheduler/publisher.sh
wget https://git.hmp.today/pavel.muhortov/cctv-scheduler/raw/branch/master/publisher.conf -O /home/user/cctv-scheduler/publisher.conf
wget https://git.hmp.today/pavel.muhortov/cctv-scheduler/raw/branch/master/publisher-template-page-1007.xml -O /home/user/cctv-scheduler/publisher-template-page-1007.xml
@@ -44,7 +41,6 @@ Edit configs.
```bash
nano /home/user/cctv-scheduler/cctv-scheduler.conf
-nano /home/user/cctv-scheduler/converter.conf
nano /home/user/cctv-scheduler/publisher.conf
```
@@ -59,59 +55,24 @@ crontab -e
## `cctv-scheduler`.py
**Description:**
-> [Hikvision](https://git.hmp.today/pavel.muhortov/cctv-scheduler/src/branch/master/info/hikvision/manual/isapi.pdf) PTZ IP-Camera management. Media streaming.
+> [Hikvision](https://git.hmp.today/pavel.muhortov/cctv-scheduler/src/branch/master/info/hikvision/manual/isapi.pdf) PTZ IP-Camera management. Media streaming. Images to video converting.
> Additionally:
>
> - getting temperature from DS18B20 over SSH,
> - saving pictures to FTP.
+> - converting picture collection to video.
>
-> This is only a local "proof of conept" for testing and debugging.
+> This is only a local "proof of concept" for testing and debugging.
**Dependencies:**
>
> - [Python 3](https://www.python.org/downloads/) (tested version 3.9.5 on [Debian GNU/Linux 11](http://ftp.debian.org/debian/dists/bullseye/))
> - [paramiko](https://www.paramiko.org/) Python 3 module (tested version 3.1.0)
> - [ffmpeg](https://ffmpeg.org) (tested version 4.3.4 on [Debian GNU/Linux 11](http://ftp.debian.org/debian/dists/bullseye/))
-
-| PARAMETERS | DESCRIPTION | DEFAULT|
-|-------------|-------------|--------|
-|**[-h]**|print help and exit||
-|**[-b, --broadcast]**|streaming media to destination|`None`|
-|**[-s, --sequences]**|run sequences from config file|`None`|
-|**[--config]**|custom configuration file path|`./cctv-scheduler.conf`|
-
-Example usage in terminal with make the script executable:
-
-```bash
-chmod u+x ./cctv-scheduler.py
-./cctv-scheduler.py -s --config /home/user/cctv-scheduler/cctv-scheduler.conf
-```
-
-Example usage with cron:
-
-```bash
-# crontab -e
-* * * * * /usr/bin/python3 /home/user/cctv-scheduler/cctv-scheduler.py -b
-0 * * * * /usr/bin/python3 /home/user/cctv-scheduler/cctv-scheduler.py -s
-```
-
-____
-
-## `converter`.sh
-
-**Description:**
-> JPEG to MP4 converter.
->
-> This is only a local "proof of conept" for testing and debugging.
-
-**Dependencies:**
->
-> - [bash](https://www.gnu.org/software/bash/) (tested version 5.1.4 on [Debian GNU/Linux 11](http://ftp.debian.org/debian/dists/bullseye/))
-> - [ffmpeg](https://ffmpeg.org/download.html) (tested version 4.3.4 on [Debian GNU/Linux 11](http://ftp.debian.org/debian/dists/bullseye/))
-> - filesystem organization:
+> - specified record pictures filesystem organization
>
>```bash
-> # filesystem organisation example
+> # record pictures filesystem organization example
>/root/
> /2022/
> /12/
@@ -130,26 +91,35 @@ ____
> /image-02_2023.01.03_time.jpeg
>```
-| POSITION | PARAMETERS | DESCRIPTION | DEFAULT |
-|-----------|--------------|------------------------|---------------|
-| 1 | **[qn]** |execution without pauses||
-| 2 | **[/path/to/conf]** |path to config| `./converter.conf` |
-| 3 | **[-d\|-w\|-m\|-y]** |periods: '' - today \| '-d' - yesterday \| '-w' - last week \| '-m' - last month \| '-y' - last year|`''`|
+| PARAMETERS | DESCRIPTION | DEFAULT|
+|-------------|-------------|--------|
+|**[-h]**|print help and exit||
+|**[-b, --broadcast]**|streaming media to destination|`None`|
+|**[-s, --sequences]**|run sequences from config file|`None`|
+|**[--config]**|custom configuration file path|`./cctv-scheduler.conf`|
+|**[-c, --converter]**|convert JPEG collection to MP4|`None`|
+|**[-d, --day]**|day in amount of days from today, for which the publication or conversion is made|`0`|
+|**[-w, --week]**|week in amount of weeks from today, for which the publication or conversion is made|`0`|
+|**[-m, --month]**|month in amount of months from today, for which the publication or conversion is made|`0`|
+|**[-y, --year]**|year in amount of years from today, for which the publication or conversion is made|`0`|
-Example usage in terminal with bash for today's MP4 making:
+Example usage in terminal with make the script executable:
```bash
-bash ./converter.sh - ./converter.conf
+chmod u+x ./cctv-scheduler.py
+./cctv-scheduler.py -s --config /home/user/cctv-scheduler/cctv-scheduler.conf
```
Example usage with cron:
```bash
# crontab -e
-1 0 * * * bash /home/user/cctv-scheduler/converter.sh qn - -d
-7 0 * * 1 bash /home/user/cctv-scheduler/converter.sh qn - -w
-30 0 1 * * bash /home/user/cctv-scheduler/converter.sh qn - -m
-36 0 1 1 * bash /home/user/cctv-scheduler/converter.sh qn - -y
+* * * * * /usr/bin/python3 /home/user/cctv-scheduler/cctv-scheduler.py -b
+0 * * * * /usr/bin/python3 /home/user/cctv-scheduler/cctv-scheduler.py -s
+1 0 * * * /usr/bin/python3 /home/user/cctv-scheduler/cctv-scheduler.py -c -d -1
+7 0 * * 1 /usr/bin/python3 /home/user/cctv-scheduler/cctv-scheduler.py -c -w -1
+30 0 1 * * /usr/bin/python3 /home/user/cctv-scheduler/cctv-scheduler.py -c -m -1
+36 0 1 1 * /usr/bin/python3 /home/user/cctv-scheduler/cctv-scheduler.py -c -y -1
```
____
diff --git a/archive/0.3/README.md b/archive/0.3/README.md
new file mode 100644
index 0000000..21d3a03
--- /dev/null
+++ b/archive/0.3/README.md
@@ -0,0 +1,199 @@
+# cctv-scheduler
+
+PTZ IP-Camera management
+____
+
+- [`cctv-scheduler.py`](https://git.hmp.today/pavel.muhortov/cctv-scheduler#cctv-scheduler-py)
+- [`converter.sh`](https://git.hmp.today/pavel.muhortov/cctv-scheduler#converter-sh)
+- [`publisher.sh`](https://git.hmp.today/pavel.muhortov/cctv-scheduler#publisher-sh)
+
+____
+
+![cctv-scheduler](info/images/cctv-scheduler-0.3.png)
+
+## `Installation`
+
+### `Requirements`
+
+Cameras settings:
+>
+> - Configuration -> System -> Security -> Authentication -> RTSP Authentication: digest/basic
+> - Configuration -> System -> Security -> Authentication -> WEB Authentication: digest/basic
+> - Configuration -> Network -> Advanced Settings -> Integration Protocol -> Enable Hikvision-CGI: Enabled
+> - Configuration -> Network -> Advanced Settings -> Integration Protocol -> Hikvision-CGI Authentication: digest/basic
+
+Look at the description of dependencies and install the necessary.
+
+### `Downloading`
+
+Download scripts and configs.
+
+```bash
+wget https://git.hmp.today/pavel.muhortov/cctv-scheduler/raw/branch/master/cctv-scheduler.py -O /home/user/cctv-scheduler/cctv-scheduler.py
+wget https://git.hmp.today/pavel.muhortov/cctv-scheduler/raw/branch/master/cctv-scheduler.conf -O /home/user/cctv-scheduler/cctv-scheduler.conf
+wget https://git.hmp.today/pavel.muhortov/cctv-scheduler/raw/branch/master/converter.sh -O /home/user/cctv-scheduler/converter.sh
+wget https://git.hmp.today/pavel.muhortov/cctv-scheduler/raw/branch/master/converter.conf -O /home/user/cctv-scheduler/converter.conf
+wget https://git.hmp.today/pavel.muhortov/cctv-scheduler/raw/branch/master/publisher.sh -O /home/user/cctv-scheduler/publisher.sh
+wget https://git.hmp.today/pavel.muhortov/cctv-scheduler/raw/branch/master/publisher.conf -O /home/user/cctv-scheduler/publisher.conf
+wget https://git.hmp.today/pavel.muhortov/cctv-scheduler/raw/branch/master/publisher-template-page-1007.xml -O /home/user/cctv-scheduler/publisher-template-page-1007.xml
+```
+
+### `Configuration`
+
+Edit configs.
+
+```bash
+nano /home/user/cctv-scheduler/cctv-scheduler.conf
+nano /home/user/cctv-scheduler/converter.conf
+nano /home/user/cctv-scheduler/publisher.conf
+```
+
+### `Scheduler`
+
+Look at examples and edit scheduler tasks:
+
+```bash
+crontab -e
+```
+
+## `cctv-scheduler`.py
+
+**Description:**
+> [Hikvision](https://git.hmp.today/pavel.muhortov/cctv-scheduler/src/branch/master/info/hikvision/manual/isapi.pdf) PTZ IP-Camera management. Media streaming.
+> Additionally:
+>
+> - getting temperature from DS18B20 over SSH,
+> - saving pictures to FTP.
+>
+> This is only a local "proof of conept" for testing and debugging.
+
+**Dependencies:**
+>
+> - [Python 3](https://www.python.org/downloads/) (tested version 3.9.5 on [Debian GNU/Linux 11](http://ftp.debian.org/debian/dists/bullseye/))
+> - [paramiko](https://www.paramiko.org/) Python 3 module (tested version 3.1.0)
+> - [ffmpeg](https://ffmpeg.org) (tested version 4.3.4 on [Debian GNU/Linux 11](http://ftp.debian.org/debian/dists/bullseye/))
+
+| PARAMETERS | DESCRIPTION | DEFAULT|
+|-------------|-------------|--------|
+|**[-h]**|print help and exit||
+|**[-b, --broadcast]**|streaming media to destination|`None`|
+|**[-s, --sequences]**|run sequences from config file|`None`|
+|**[--config]**|custom configuration file path|`./cctv-scheduler.conf`|
+
+Example usage in terminal with make the script executable:
+
+```bash
+chmod u+x ./cctv-scheduler.py
+./cctv-scheduler.py -s --config /home/user/cctv-scheduler/cctv-scheduler.conf
+```
+
+Example usage with cron:
+
+```bash
+# crontab -e
+* * * * * /usr/bin/python3 /home/user/cctv-scheduler/cctv-scheduler.py -b
+0 * * * * /usr/bin/python3 /home/user/cctv-scheduler/cctv-scheduler.py -s
+```
+
+____
+
+## `converter`.sh
+
+**Description:**
+> JPEG to MP4 converter.
+>
+> This is only a local "proof of conept" for testing and debugging.
+
+**Dependencies:**
+>
+> - [bash](https://www.gnu.org/software/bash/) (tested version 5.1.4 on [Debian GNU/Linux 11](http://ftp.debian.org/debian/dists/bullseye/))
+> - [ffmpeg](https://ffmpeg.org/download.html) (tested version 4.3.4 on [Debian GNU/Linux 11](http://ftp.debian.org/debian/dists/bullseye/))
+> - filesystem organization:
+>
+>```bash
+> # filesystem organisation example
+>/root/
+> /2022/
+> /12/
+> /52/
+> /31/
+> /image-01_2022.12.31_time.jpeg
+> /image-02_2022.12.31_time.jpeg
+> /2023/
+> /01/
+> /01/
+> /02/
+> /image-01_2023.01.02_time.jpeg
+> /image-02_2023.01.02_time.jpeg
+> /03/
+> /image-01_2023.01.03_time.jpeg
+> /image-02_2023.01.03_time.jpeg
+>```
+
+| POSITION | PARAMETERS | DESCRIPTION | DEFAULT |
+|-----------|--------------|------------------------|---------------|
+| 1 | **[qn]** |execution without pauses||
+| 2 | **[/path/to/conf]** |path to config| `./converter.conf` |
+| 3 | **[-d\|-w\|-m\|-y]** |periods: '' - today \| '-d' - yesterday \| '-w' - last week \| '-m' - last month \| '-y' - last year|`''`|
+
+Example usage in terminal with bash for today's MP4 making:
+
+```bash
+bash ./converter.sh - ./converter.conf
+```
+
+Example usage with cron:
+
+```bash
+# crontab -e
+1 0 * * * bash /home/user/cctv-scheduler/converter.sh qn - -d
+7 0 * * 1 bash /home/user/cctv-scheduler/converter.sh qn - -w
+30 0 1 * * bash /home/user/cctv-scheduler/converter.sh qn - -m
+36 0 1 1 * bash /home/user/cctv-scheduler/converter.sh qn - -y
+```
+
+____
+
+## `publisher`.sh
+
+**Description:**
+> Uploading MP4 to [Wordpress](https://wordpress.com/) and [Telegram](https://web.telegram.org/).
+> Additionally:
+>
+> - editing [Wordpress](https://codex.wordpress.org/XML-RPC_WordPress_API) page from template
+> - recompressing video if size [over 50MB](https://core.telegram.org/bots/api#sendvideo)
+>
+> This is only a local "proof of conept" for testing and debugging.
+
+**Dependencies:**
+>
+> - [bash](https://www.gnu.org/software/bash/) (tested version 5.1.4 on [Debian GNU/Linux 11](http://ftp.debian.org/debian/dists/bullseye/))
+> - [curl](https://curl.se/download.html) (tested version 7.74 on [Debian GNU/Linux 11](http://ftp.debian.org/debian/dists/bullseye/))
+> - [ffmpeg](https://ffmpeg.org/download.html) (tested version 4.3.4 on [Debian GNU/Linux 11](http://ftp.debian.org/debian/dists/bullseye/))
+> - [libxml2-utils](https://gitlab.gnome.org/GNOME/libxml2) (tested version 2.9.10 on [Debian GNU/Linux 11](http://ftp.debian.org/debian/dists/bullseye/))
+> - [jq](https://stedolan.github.io/jq/download/) (tested version 1.6 on [Debian GNU/Linux 11](http://ftp.debian.org/debian/dists/bullseye/))
+>
+
+| POSITION | PARAMETERS | DESCRIPTION | DEFAULT |
+|-----------|--------------|------------------------|---------------|
+| 1 | **[qn]** |execution without pauses||
+| 2 | **[/path/to/conf]** |path to config| `./publisher.conf` |
+| 3 | **[-d\|-w\|-m\|-y]** |periods: '' - 0 day \| '-d' - -X day \| '-w' - -X week \| '-m' - -X month \| '-y' - -X year|`''`|
+| 4 | **[1\|2\|3..XXX]** |multiplier for period: '' - 1 day\|week\|month\|year|`1`|
+| 5 | **[--onlytg\|--onlywp]** |'--onlytg' - only publish to Telegram \|'--onlywp' - only publish to Wordpress||
+
+Example usage in terminal with bash for publish to Telegram today's MP4:
+
+```bash
+bash ./publisher.sh - ./publisher.conf - - - - --onlytg
+```
+
+Example usage with cron:
+
+```bash
+# crontab -e
+1 1 * * * bash /home/user/cctv-scheduler/publisher.sh qn - -d
+7 1 * * 1 bash /home/user/cctv-scheduler/publisher.sh qn - -w
+30 1 1 * * bash /home/user/cctv-scheduler/publisher.sh qn - -m
+36 1 1 1 * bash /home/user/cctv-scheduler/publisher.sh qn - -y
+```
diff --git a/archive/0.3/cctv-scheduler.conf b/archive/0.3/cctv-scheduler.conf
new file mode 100755
index 0000000..702868f
--- /dev/null
+++ b/archive/0.3/cctv-scheduler.conf
@@ -0,0 +1,112 @@
+[common]
+# By default, a temporary files directory is created in the same path where the script is located.
+# If you need change it, uncomment the parameter and set the path you want.
+#temp_path = /tmp/cctv-scheduler
+#
+# By default, logs use the same directory where the script is located.
+# If you need change it, uncomment the parameter and set the path you want.
+#log_root = /var/log/cctv-scheduler
+#
+# The default log level is "INFO".
+# If you get errors or want to change the logging level, uncomment the parameter and set the level you want:
+# DEBUG, INFO, WARNING, ERROR, CRITICAL.
+#log_level = DEBUG
+
+
+[enable-broadcast]
+# List the broadcast block names. Only blocks with the TRUE value will be used.
+camera.test.local = true
+
+
+[enable-sequences]
+# List the sequence/camera block names. Only blocks with the TRUE value will be used.
+camera.test.local = true
+
+
+[enable-sensors]
+# List the sensor block names. Only blocks with the TRUE value will be used.
+sensor.test.local = true
+
+
+[broadcast-config:camera.test.local]
+# Broadcast parameter description block always starts with "broadcast-config:".
+src = rtsp://user:pass@192.168.254.253:554/Streaming/Channels/101,http://radio.fm:8000/stream.mp3
+dst = rtp://239.0.0.1:5554
+# Optionality you can change video stream framerate.
+#fps = 25
+#
+# Optionality you can set YouTube recommended preset:
+# 240p, 360p, 480p, 720p, 1080p, 1440p, 2160p.
+#preset = 1080p
+#
+# By default ffmpeg expected in /usr/bin/ffmpeg or C:\Program Files\ffmpeg\bin\ffmpeg.exe.
+# If you need change it, uncomment the parameter and set the path you want.
+#ffpath = /usr/bin/ffmpeg
+#
+# If you get program freezes because your network unstable, uncomment the parameter.
+#watchdog = true
+#
+# By default, watchdog waits 5 seconds before terminating the program.
+# If you need change it, uncomment the parameter and set the path you want.
+#watchsec = 15
+#
+# If you use crontab or other scheduler to run, uncomment the parameter to prevent process overlap.
+#onlyonce = true
+
+
+[sensor-config:sensor.test.local]
+# Remote host's sensor parameter description block always starts with "sensor-config:".
+hostname = 192.168.254.252
+username = user
+userpass = pass
+# To recognize options for polling a sensor, you must specify the type of sensor.
+# Supported types:
+# ds18b20
+nodetype = ds18b20
+nodename = 28-1a2b3c4d5e6f
+
+
+[camera-config:camera.test.local]
+# Camera parameter description block always starts with "camera-config:".
+hostname = 192.168.254.253
+username = user
+userpass = pass
+# If a record directory on a remote host is used, a username and password must be specified.
+# Supported protocols:
+# FTP, SFTP.
+records_root_path = ftp://192.168.254.254/Records/camera.test.local
+records_root_user = user
+records_root_pass = pass
+
+
+[camera-sequences:camera.test.local]
+# Camera sequence description block always starts with "camera-sequences:".
+# Place only the sequence of PTZ-actions in this block!
+# Variable name can be anything. Only 'downloadjpeg' is using this for filename prefix.
+# Available actions:
+# setcamerapos, setcameramov, settextonosd, downloadjpeg, capabilities, getcamerapos,
+# setptzmovyyu, setptzmovyyd, setptzmovxxl, setptzmovxxr, setptzmovzzi, setptzmovzzo,
+# setptzpreset, setptztostop, setmovtohome, setposashome, rebootcamera.
+#
+# Format (spaces are used for readability only):
+# name = action, x, y, zoom, preset, speed, d(ms), w(s), text, notes or anything
+step001 = capabilities, -, -, -, -, -, -, 3, ,
+step002 = getcamerapos, -, -, -, -, -, -, 3, ,
+step011 = setmovtohome, -, -, -, -, -, -, 15, ,
+step021 = setptzmovyyd, -, -, -, -, 2, -, 5, , 'speed: 1..7'
+step022 = setptzmovyyu, -, -, -, -, 4, -, 3, , 'speed: 1..7'
+step023 = setptzmovxxl, -, -, -, -, 4, -, 3, , 'speed: 1..7'
+step024 = setptzmovxxr, -, -, -, -, 2, -, 5, , 'speed: 1..7'
+step025 = setptzmovzzi, -, -, -, -, 7, -, 3, , 'speed: 1..7'
+step026 = setptzmovzzo, -, -, -, -, 7, -, 3, , 'speed: 1..7'
+step027 = setptztostop, -, -, -, -, -, -, 3, ,
+step031 = setptzpreset, -, -, -, 2, 1, -, 15, , 'speed: 1..7'
+step041 = setcameramov, 33, 33, 66, -, -, -, 5, , 'x: -100..100, y: -100..100, z: -100..100, duration: 0..180000'
+step042 = setcameramov, -66, -66, -99, -, -, 5000, 0, , 'x: -100..100, y: -100..100, z: -100..100, duration: 0..180000'
+step043 = setcamerapos, 0, 0, 0, -, -, -, 15, , 'x: 0..3600, y: -900..2700, z: 0..1000'
+step051 = setposashome, -, -, -, -, -, -, 3.5, ,
+step061 = settextonosd, 0, 0, -, -, -, -, 3, hello, 'x|y: osd text position, text: text for osd without quotes or commas'
+step062 = settextonosd, 0, 0, -, -, -, -, 3, sensor-config:sensor.test.local, 'if a sensor configuration is specified, the sensor value is used instead of text'
+step063 = settextonosd, 0, 0, -, -, -, -, 3, , 'an empty text value is used to clear the osd'
+step071 = downloadjpeg, 1280, 720, -, -, -, -, 5, , 'name: filename prefix, x|y: camera width|height resolution'
+step999 = rebootcamera, -, -, -, -, -, -, 120, ,
diff --git a/archive/0.3/cctv-scheduler.py b/archive/0.3/cctv-scheduler.py
new file mode 100755
index 0000000..6463e41
--- /dev/null
+++ b/archive/0.3/cctv-scheduler.py
@@ -0,0 +1,1632 @@
+#!/usr/bin/env python3
+
+
+import logging
+import urllib.request
+from argparse import ArgumentParser
+from datetime import datetime
+from ftplib import FTP
+from multiprocessing import Process, Queue
+from os import path, sep, makedirs, remove, replace, environ
+from subprocess import Popen, PIPE, STDOUT
+from sys import platform
+from time import sleep
+from paramiko import SSHClient, AutoAddPolicy
+
+
+class Parse:
+ """Parser of configs, arguments, parameters.
+ """
+ def __init__(self, parameters, block: str = None) -> None:
+ """Object constructor.
+
+ Args:
+ parameters: dictionary as "key":"value" or
+ ArgumentParser class object or
+ string path to the file or
+ string as "var1=val1;var2=val2".
+ block (str, optional): name of target block from text. Defaults to None.
+ """
+ self.path = ''
+ self.data = {}
+ if type(parameters) is dict:
+ self._dict2dict(parameters)
+ if type(parameters) is ArgumentParser:
+ self._dict2dict(self.argv2dict(parameters))
+ if type(parameters) is str:
+ if path.exists(parameters):
+ self._dict2dict(
+ self.strs2dict(
+ self.conf2strs(parameters),
+ block
+ )
+ )
+ self.path = parameters
+ else:
+ self._dict2dict(self.strs2dict(parameters, block))
+
+ def __str__(self) -> str:
+ """Overrides method for print(object).
+
+ Returns:
+ str: string with contents of the object's dictionary.
+ """
+ string = ''
+ for key, val in self.data.items():
+ string += str(type(val)) + ' ' + str(key) + ' = ' + str(val) + '\n'
+ return string
+
+ def _dict2dict(self, dictionary: dict) -> None:
+ """Updates or adds dictionary data.
+
+ Args:
+ dictionary (dict): dictionary as "key":"value".
+ """
+ self.data.update(dictionary)
+
+ def expand(self, store: str = None) -> dict:
+ """Expand dictionary "key":"name.conf" to dictionary "key":{subkey: subval}.
+
+ Args:
+ store (str, optional): path to directory with name.conf. Defaults to None.
+
+ Returns:
+ dict: expanded dictionary as "key":{subkey: subval}.
+ """
+ for key in self.data:
+ if store:
+ config = store + sep + self.data[key]
+ else:
+ config = self.data[key]
+ with open(config) as file:
+ self.data[key] = Parse(file.read()).data
+ return self.data
+
+ @classmethod
+ def argv2dict(cls, parser: ArgumentParser) -> dict:
+ """Converts startup arguments to a dictionary.
+
+ Args:
+ parser (ArgumentParser): argparse.ArgumentParser class object.
+
+ Returns:
+ dict: dictionary as "key":"value".
+ """
+ parser = ArgumentParser(add_help=False, parents=[parser])
+ return vars(parser.parse_args())
+
+ @classmethod
+ def conf2strs(cls, config: str) -> str:
+ """Builds a dictionary from a file containing parameters.
+
+ Args:
+ config (str): path to the config file.
+
+ Returns:
+ str: string as "var1=val1;\nvar2=val2;".
+ """
+ with open(config) as file:
+ raw = file.read()
+ strs = ''
+ for line in raw.splitlines():
+ if not line.lstrip().startswith('#'):
+ strs += line + '\n'
+ return strs
+
+ @classmethod
+ def strs2dict(cls, strings: str, blockname: str) -> dict:
+ """Builds a dictionary from a strings containing parameters.
+
+ Args:
+ strings (str): string as "var1=val1;var2=val2;".
+ blockname (str): name of target block from text.
+
+ Returns:
+ dict: dictionary as "key":"value".
+ """
+ dictionary = {}
+ if blockname:
+ strings = cls.block(blockname, strings)
+ for line in strings.replace('\n', ';').split(';'):
+ if not line.lstrip().startswith('#') and "=" in line:
+ dictionary[line.split('=')[0].strip()] = line.split('=')[1].strip().split(';')[0].strip()
+ return dictionary
+
+ @classmethod
+ def str2bool(cls, value: str) -> bool:
+ """Converts a string value to boolean.
+
+ Args:
+ value (str): string containing "true" or "false", "yes" or "no", "1" or "0".
+
+ Returns:
+ bool: bool True or False.
+ """
+ return str(value).lower() in ("true", "yes", "1")
+
+ @classmethod
+ def block(cls, blockname: str, text: str) -> str:
+ """Cuts a block of text between line [blockname] and line [next block] or EOF.
+
+ Args:
+ blockname (str): string in [] after which the block starts.
+ text (str): string of text from which the block is needed.
+
+ Returns:
+ str: string of text between line [block name] and line [next block].
+ """
+ level = 1
+ save = False
+ result = ''
+ for line in text.splitlines():
+ if line.startswith('[') and blockname in line:
+ level = line.count('[')
+ save = True
+ elif line.startswith('[') and '['*level in line:
+ save = False
+ elif save:
+ result += line + '\n'
+ return result
+
+
+class Connect:
+ """Set of connection methods (functions) for various protocols.
+ """
+ @staticmethod
+ def http(
+ url: str, method: str = 'GET',
+ username: str = '', password: str = '', authtype: str = None,
+ contenttype: str = 'text/plain', contentdata: str = ''
+ ) -> str:
+ """Handling HTTP request.
+
+ Args:
+ url (str): request url.
+ method (str, optional): HTTP request method. Defaults to 'GET'.
+ username (str, optional): username for url authentication. Defaults to ''.
+ password (str, optional): password for url authentication. Defaults to ''.
+ authtype (str, optional): digest|basic authentication type. Defaults to None.
+ contenttype (str, optional): 'Content-Type' header. Defaults to 'text/plain'.
+ contentdata (str, optional): content data. Defaults to ''.
+
+ Returns:
+ str: HTTP response or 'ERROR'.
+ """
+
+ # Preparing authorization
+ if authtype:
+ pswd = urllib.request.HTTPPasswordMgrWithDefaultRealm()
+ pswd.add_password(None, url, username, password)
+ if authtype == 'basic':
+ auth = urllib.request.HTTPBasicAuthHandler(pswd)
+ if authtype == 'digest':
+ auth = urllib.request.HTTPDigestAuthHandler(pswd)
+ urllib.request.install_opener(urllib.request.build_opener(auth))
+
+ # Preparing request
+ request = urllib.request.Request(url=url, data=bytes(contentdata.encode('utf-8')), method=method)
+ request.add_header('Content-Type', contenttype)
+
+ # Response
+ try:
+ response = urllib.request.urlopen(request).read()
+ logging.debug(
+ msg=''
+ + '\n' + 'uri: ' + url
+ + '\n' + 'method: ' + method
+ + '\n' + 'username: ' + username
+ + '\n' + 'password: ' + password
+ + '\n' + 'authtype: ' + authtype
+ + '\n' + 'content-type: ' + contenttype
+ + '\n' + 'content-data: ' + contentdata
+ )
+ if response.startswith(b'\xff\xd8'):
+ return response
+ else:
+ return str(response.decode('utf-8'))
+ except Exception as error:
+ logging.debug(msg='\n' + 'error: ' + str(error))
+ return 'ERROR'
+
+ @staticmethod
+ def ssh_commands(command: str, hostname: str, username: str, password: str, port: int = 22) -> str:
+ """Handling SSH command executing.
+
+ Args:
+ command (str): command for executing.
+ hostname (str): remote hostname or ip address.
+ username (str): remote host username.
+ password (str): remote host password.
+ port (int, optional): remote host connection port. Defaults to 22.
+
+ Returns:
+ str: terminal response or 'ERROR'.
+ """
+ client = SSHClient()
+ client.set_missing_host_key_policy(AutoAddPolicy())
+ try:
+ client.connect(hostname=hostname, username=username, password=password, port=port)
+ stdin, stdout, stderr = client.exec_command(command=command, get_pty=True)
+ if 'sudo' in command:
+ stdin.write(password + '\n')
+ stdin.flush()
+ stdout.flush()
+ data = stdout.read() + stderr.read()
+ client.close()
+ return data.decode('utf-8')
+ except Exception as error:
+ logging.debug(
+ msg=''
+ + '\n' + 'host: ' + hostname + ':' + str(port)
+ + '\n' + 'user: ' + username
+ + '\n' + 'pass: ' + password
+ + '\n' + 'command: ' + command
+ + '\n' + 'error: ' + str(error)
+ )
+ return 'ERROR'
+
+ @staticmethod
+ def ssh_put_file(src_file: str, dst_file: str, hostname: str, username: str, password: str, port: int = 22) -> str:
+ """Handling SFTP upload file.
+
+ Args:
+ src_file (str): /local/path/to/file.
+ dst_file (str): /remote/path/to/file.
+ hostname (str): remote hostname or ip address.
+ username (str): remote host username.
+ password (str): remote host password.
+ port (int, optional): remote host connection port. Defaults to 22.
+
+ Returns:
+ str: '/remote/path/to/file' or 'ERROR'.
+ """
+ client = SSHClient()
+ client.set_missing_host_key_policy(AutoAddPolicy())
+ try:
+ client.connect(hostname=hostname, username=username, password=password, port=port)
+ client.exec_command('mkdir -p ' + path.dirname(dst_file))
+ try:
+ sftp = client.open_sftp()
+ sftp.put(localpath=src_file, remotepath=dst_file)
+ sftp.stat(dst_file)
+ sftp.close()
+ return dst_file
+ except Exception as error:
+ logging.debug(
+ msg=''
+ + '\n' + 'dst_file: ' + dst_file
+ + '\n' + 'error: ' + str(error)
+ )
+ return 'ERROR'
+ except Exception as error:
+ logging.debug(
+ msg=''
+ + '\n' + 'host: ' + hostname + ':' + str(port)
+ + '\n' + 'user: ' + username
+ + '\n' + 'pass: ' + password
+ + '\n' + 'src_file: ' + src_file
+ + '\n' + 'dst_file: ' + dst_file
+ + '\n' + 'error: ' + str(error)
+ )
+ return 'ERROR'
+ '''
+ @staticmethod
+ def ssh_get_file(src_file: str, dst_file: str, hostname: str, username: str, password: str, port: int = 22) -> str:
+ """Handling SFTP download file.
+
+ Args:
+ src_file (str): /remote/path/to/file.
+ dst_file (str): /local/path/to/file.
+ hostname (str): remote hostname or ip address.
+ username (str): remote host username.
+ password (str): remote host password.
+ port (int, optional): remote host connection port. Defaults to 22.
+
+ Returns:
+ str: '/local/path/to/file' or 'ERROR'.
+ """
+ client = SSHClient()
+ client.set_missing_host_key_policy(AutoAddPolicy())
+ try:
+ client.connect(hostname=hostname, username=username, password=password, port=port)
+ with client.open_sftp() as sftp:
+ sftp.get(remotepath=src_file, localpath=dst_file)
+ client.close()
+ except Exception as error:
+ logging.debug(
+ msg=''
+ + '\n' + 'host: ' + hostname + ':' + str(port)
+ + '\n' + 'user: ' + username
+ + '\n' + 'pass: ' + password
+ + '\n' + 'src_file: ' + src_file
+ + '\n' + 'dst_file: ' + dst_file
+ + '\n' + 'error: ' + str(error)
+ )
+ return 'ERROR'
+ '''
+ @staticmethod
+ def ftp_put_file(src_file: str, dst_file: str, hostname: str, username: str, password: str) -> bool:
+ dst_path = dst_file.split('/')[:-1]
+ ftp = FTP(host=hostname)
+ try:
+ ftp.login(user=username, passwd=password)
+ for path_item in dst_path:
+ if path_item.strip() == '':
+ continue
+ path_item = path_item.replace('/', '')
+ try:
+ ftp.cwd(path_item)
+ except Exception:
+ ftp.mkd(path_item)
+ ftp.cwd(path_item)
+ except Exception:
+ pass
+
+ with open(src_file, "rb") as file:
+ ftp.storbinary(f"STOR {dst_file}", file)
+ ftp.quit()
+ return True
+ '''
+ @staticmethod
+ def ftp_get_file(src_file: str, dst_file: str, hostname: str, username: str, password: str):
+ ftp = FTP(host=hostname)
+ try:
+ ftp.login(user=username, passwd=password)
+ with open(dst_file, "wb") as file:
+ ftp.retrbinary(f"RETR {src_file}", file.write)
+ ftp.quit()
+ except Exception:
+ pass
+ '''
+ '''
+ @staticmethod
+ def xmlrpc():
+ pass
+ '''
+
+
+class HikISAPI(Connect):
+ """Representing Hikvision device with ISAPI.
+ The class inherits the necessary connection methods of the Connect class
+ """
+ def __init__(
+ self,
+ hostname: str,
+ username: str, userpass: str,
+ authtype: str = 'digest',
+ hostport: int = 80, protocol: str = 'http',
+ channel: int = 101, videoid: int = 1
+ ) -> None:
+ """Object constructor.
+
+ Args:
+ hostname (str): camera hostname or ip address.
+ username (str): camera admin username.
+ userpass (str): camera admin password.
+ authtype (str, optional): digest|basic camera authentication type. Defaults to 'digest'.
+ hostport (int, optional): camera connection port. Defaults to 80.
+ protocol (str, optional): camera connection protocol. Defaults to 'http'.
+ channel (int, optional): camera channel id. Defaults to 101.
+ videoid (int, optional): camera video id. Defaults to 1.
+ """
+ self._host = hostname
+ self._port = hostport
+ self._user = username
+ self._pswd = userpass
+ self._auth = authtype
+ self._prot = protocol
+ self._chan = channel
+ self._viid = videoid
+
+ def __call(
+ self,
+ url: str, method: str = 'GET',
+ contenttype: str = 'application/x-www-form-urlencoded',
+ contentdata: str = ''
+ ) -> str:
+ """Send request to camera.
+
+ Args:
+ url (str): API path for request.
+ method (str, optional): HTTP request method. Defaults to 'GET'.
+ contenttype (str, optional): Content-Type header. Defaults to 'application/x-www-form-urlencoded'.
+ contentdata (str, optional): data for send with request. Defaults to ''.
+
+ Returns:
+ str: HTTP response content.
+ """
+ return self.http(
+ url=url, method=method,
+ username=self._user, password=self._pswd, authtype=self._auth,
+ contenttype=contenttype, contentdata=contentdata
+ )
+
+ def capabilities(self) -> bool:
+ """Get camera capabilities.
+
+ Returns:
+ bool: True if successed. Printing a response with a logger at the INFO level.
+ """
+ url = (
+ self._prot + '://' + self._host + ':' + str(self._port)
+ + "/ISAPI/PTZCtrl/channels/" + str(self._viid) + "/capabilities"
+ )
+ response = self.__call(url=url, method='GET')
+ if response != 'ERROR':
+ logging.info(msg='\n' + response + '\n')
+ return True
+ else:
+ return False
+
+ def downloadjpeg(self, dst_file: str = path.splitext(__file__)[0] + '.jpeg', x: int = 1920, y: int = 1080) -> bool:
+ """Get static picture from camera.
+
+ Args:
+ dst_file (str, optional): absolute path of picture to save. Defaults to scriptname+'.jpeg'.
+ x (int, optional): picture width. Defaults to 1920.
+ y (int, optional): picture height. Defaults to 1080.
+
+ Returns:
+ bool: True if successed.
+ """
+ url = (
+ self._prot + '://' + self._host + ':' + str(self._port)
+ + "/Streaming/channels/" + str(self._viid)
+ + "/picture?snapShotImageType=JPEG&videoResolutionWidth="
+ + str(x) + "&videoResolutionHeight=" + str(y)
+ )
+ with open(dst_file, "wb") as file:
+ response = self.__call(url=url, method='GET')
+ if response != 'ERROR':
+ file.write(response)
+ logging.debug(msg='\n' + dst_file + '\n')
+ return True
+ else:
+ return False
+
+ def getcamerapos(self) -> bool:
+ """Get current camera position.
+
+ Returns:
+ bool: True if successed. Printing a response with a logger at the INFO level.
+ """
+ url = (self._prot + '://' + self._host + ':' + str(self._port)
+ + "/ISAPI/PTZCtrl/channels/" + str(self._chan) + "/status")
+ response = self.__call(url=url, method='GET')
+ if response != 'ERROR':
+ logging.info(msg='\n' + response + '\n')
+ return True
+ else:
+ return False
+
+ def rebootcamera(self) -> bool:
+ """Set camera reboot command.
+
+ Returns:
+ bool: True if successed.
+ """
+ url = (
+ self._prot + '://' + self._host + ':' + str(self._port)
+ + "/ISAPI/System/reboot"
+ )
+ response = self.__call(url=url, method="PUT")
+ if response != 'ERROR':
+ logging.debug(msg='\n' + response + '\n')
+ return True
+ else:
+ return False
+
+ def setptzmovyyu(self, speed: int = 1) -> bool:
+ """Start camera moving to up.
+
+ Args:
+ speed (int, optional): moving speed from 1 to 7. Defaults to 1.
+
+ Returns:
+ bool: True if successed.
+ """
+ url = (
+ self._prot + '://' + self._host + ':' + str(self._port)
+ + "/PTZ/channels/" + str(self._viid)
+ + "/PTZControl?command=TILT_UP&speed=" + str(speed)
+ + "&mode=start"
+ )
+ response = self.__call(url=url, method='GET')
+ if response != 'ERROR':
+ logging.debug(msg='\n' + response + '\n')
+ return True
+ else:
+ return False
+
+ def setptzmovyyd(self, speed: int = 1) -> bool:
+ """Start camera moving to down.
+
+ Args:
+ speed (int, optional): moving speed from 1 to 7. Defaults to 1.
+
+ Returns:
+ bool: True if successed.
+ """
+ url = (
+ self._prot + '://' + self._host + ':' + str(self._port)
+ + "/PTZ/channels/" + str(self._viid)
+ + "/PTZControl?command=TILT_DOWN&speed=" + str(speed)
+ + "&mode=start"
+ )
+ response = self.__call(url=url, method='GET')
+ if response != 'ERROR':
+ logging.debug(msg='\n' + response + '\n')
+ return True
+ else:
+ return False
+
+ def setptzmovxxl(self, speed: int = 1) -> bool:
+ """Start camera moving to left.
+
+ Args:
+ speed (int, optional): moving speed from 1 to 7. Defaults to 1.
+
+ Returns:
+ bool: True if successed.
+ """
+ url = (
+ self._prot + '://' + self._host + ':' + str(self._port)
+ + "/PTZ/channels/" + str(self._viid)
+ + "/PTZControl?command=PAN_LEFT&speed=" + str(speed)
+ + "&mode=start"
+ )
+ response = self.__call(url=url, method='GET')
+ if response != 'ERROR':
+ logging.debug(msg='\n' + response + '\n')
+ return True
+ else:
+ return False
+
+ def setptzmovxxr(self, speed: int = 1) -> bool:
+ """Start camera moving to right.
+
+ Args:
+ speed (int, optional): moving speed from 1 to 7. Defaults to 1.
+
+ Returns:
+ bool: True if successed.
+ """
+ url = (
+ self._prot + '://' + self._host + ':' + str(self._port)
+ + "/PTZ/channels/" + str(self._viid)
+ + "/PTZControl?command=PAN_RIGHT&speed=" + str(speed)
+ + "&mode=start"
+ )
+ response = self.__call(url=url, method='GET')
+ if response != 'ERROR':
+ logging.debug(msg='\n' + response + '\n')
+ return True
+ else:
+ return False
+
+ def setptzmovzzi(self, speed: int = 1) -> bool:
+ """Start camera zoom in.
+
+ Args:
+ speed (int, optional): moving speed from 1 to 7. Defaults to 1.
+
+ Returns:
+ bool: True if successed.
+ """
+ url = (
+ self._prot + '://' + self._host + ':' + str(self._port)
+ + "/PTZ/channels/" + str(self._viid)
+ + "/PTZControl?command=ZOOM_OUT&speed=" + str(speed)
+ + "&mode=start"
+ )
+ response = self.__call(url=url, method='GET')
+ if response != 'ERROR':
+ logging.debug(msg='\n' + response + '\n')
+ return True
+ else:
+ return False
+
+ def setptzmovzzo(self, speed: int = 1) -> bool:
+ """Start camera zoom out.
+
+ Args:
+ speed (int, optional): moving speed from 1 to 7. Defaults to 1.
+
+ Returns:
+ bool: True if successed.
+ """
+ url = (
+ self._prot + '://' + self._host + ':' + str(self._port)
+ + "/PTZ/channels/" + str(self._viid)
+ + "/PTZControl?command=ZOOM_IN&speed=" + str(speed)
+ + "&mode=start"
+ )
+ response = self.__call(url=url, method='GET')
+ if response != 'ERROR':
+ logging.debug(msg='\n' + response + '\n')
+ return True
+ else:
+ return False
+
+ def setptzpreset(self, preset: int, speed: int = 1) -> bool:
+ """Start camera moving to preset.
+
+ Args:
+ preset (int): saved preset number.
+ speed (int, optional): moving speed from 1 to 7. Defaults to 1.
+
+ Returns:
+ bool: True if successed.
+ """
+ url = (
+ self._prot + '://' + self._host + ':' + str(self._port)
+ + "/PTZ/channels/" + str(self._viid)
+ + "/PTZControl?command=GOTO_PRESET&presetNo=" + str(preset)
+ + "&speed=" + str(speed)
+ + "&mode=start"
+ )
+ response = self.__call(url=url, method='GET')
+ if response != 'ERROR':
+ logging.debug(msg='\n' + response + '\n')
+ return True
+ else:
+ return False
+
+ def setptztostop(self) -> bool:
+ """Stop any camera moving.
+
+ Returns:
+ bool: True if successed.
+ """
+ url = (
+ self._prot + '://' + self._host + ':' + str(self._port)
+ + "/PTZ/channels/" + str(self._viid)
+ + "/PTZControl?command=GOTO_PRESET&mode=stop"
+ )
+ response = self.__call(url=url, method='GET')
+ if response != 'ERROR':
+ logging.debug(msg='\n' + response + '\n')
+ return True
+ else:
+ return False
+
+ def setcamerapos(self, x: int = 0, y: int = 0, z: int = 0) -> bool:
+ """Set camera moving to absolute position.
+
+ Args:
+ x (int, optional): horisontal camera position from 0 to 3600. Defaults to 0.
+ y (int, optional): vertical camera position from -900 to 2700. Defaults to 0.
+ z (int, optional): zoom camera position from 0 to 1000. Defaults to 0.
+
+ Returns:
+ bool: True if successed.
+ """
+ url = (
+ self._prot + '://' + self._host + ':' + str(self._port)
+ + "/ISAPI/PTZCtrl/channels/" + str(self._chan)
+ + "/absolute"
+ )
+ xml = ''.join(
+ ''
+ + ''
+ + '' + str(y) + ''
+ + '' + str(x) + ''
+ + '' + str(z) + ''
+ + ''
+ )
+ response = self.__call(url=url, method="PUT", contenttype="text/xml", contentdata=xml)
+ if response != 'ERROR':
+ logging.debug(msg='\n' + response + '\n')
+ return True
+ else:
+ return False
+
+ def __setcameramovcon(self, x: int = 0, y: int = 0, z: int = 0) -> bool:
+ """Set camera moving to direction until other signal or 180 seconds elapse.
+
+ Args:
+ x (int, optional): acceleration of horizontal camera movement from -100 to 100. Defaults to 0.
+ y (int, optional): acceleration of vertical camera movement from -100 to 100. Defaults to 0.
+ z (int, optional): acceleration of zoom camera movement from -100 to 100. Defaults to 0.
+
+ Returns:
+ bool: True if successed.
+ """
+ url = (
+ self._prot + '://' + self._host + ':' + str(self._port)
+ + "/ISAPI/PTZCtrl/channels/" + str(self._chan)
+ + "/continuous"
+ )
+ xml = ''.join(
+ ''
+ + ''
+ + '' + str(x) + ''
+ + '' + str(y) + ''
+ + '' + str(z) + ''
+ + ''
+ )
+ response = self.__call(url=url, method="PUT", contenttype="text/xml", contentdata=xml)
+ if response != 'ERROR':
+ logging.debug(msg='\n' + response + '\n')
+ return True
+ else:
+ return False
+
+ def __setcameramovmom(self, x: int = 0, y: int = 0, z: int = 0, t: int = 180000) -> bool:
+ """Set camera moving to direction until other signal or duration elapse.
+
+ Args:
+ x (int, optional): acceleration of horizontal camera movement from -100 to 100. Defaults to 0.
+ y (int, optional): acceleration of vertical camera movement from -100 to 100. Defaults to 0.
+ z (int, optional): acceleration of zoom camera movement from -100 to 100. Defaults to 0.
+ t (int, optional): duration in ms of acceleration from 0 to 180000. Defaults to 180000.
+
+ Returns:
+ bool: True if successed.
+ """
+ url = (
+ self._prot + '://' + self._host + ':' + str(self._port)
+ + "/ISAPI/PTZCtrl/channels/" + str(self._chan)
+ + "/momentary"
+ )
+ xml = ''.join(
+ ''
+ + ''
+ + '' + str(x) + ''
+ + '' + str(y) + ''
+ + '' + str(z) + ''
+ + '' + str(t) + ''
+ + ''
+ )
+ response = self.__call(url=url, method="PUT", contenttype="text/xml", contentdata=xml)
+ if response != 'ERROR':
+ sleep(t/1000)
+ logging.debug(msg='\n' + response + '\n')
+ return True
+ else:
+ return False
+
+ def setcameramov(self, x: int = 0, y: int = 0, z: int = 0, t: int = 0) -> bool:
+ """Set camera moving to direction (polymorph abstraction).
+
+ Args:
+ x (int, optional): acceleration of horizontal camera movement from -100 to 100. Defaults to 0.
+ y (int, optional): acceleration of vertical camera movement from -100 to 100. Defaults to 0.
+ z (int, optional): acceleration of zoom camera movement from -100 to 100. Defaults to 0.
+ t (int, optional): duration in ms of acceleration from 0 to 180000. Defaults to 0.
+
+ Returns:
+ bool: True if successed.
+ """
+ if t == '-' or int(t) == 0:
+ return self.__setcameramovcon(x=int(x), y=int(y), z=int(z))
+ else:
+ return self.__setcameramovmom(x=int(x), y=int(y), z=int(z), t=int(t))
+
+ def setmovtohome(self) -> bool:
+ """Set camera moving to homeposition.
+
+ Returns:
+ bool: True if successed.
+ """
+ url = (
+ self._prot + '://' + self._host + ':' + str(self._port)
+ + "/ISAPI/PTZCtrl/channels/" + str(self._chan)
+ + "/homeposition/goto"
+ )
+ xml = ''.join(
+ ''
+ + ''
+ )
+ response = self.__call(url=url, method="PUT", contenttype="text/xml", contentdata=xml)
+ if response != 'ERROR':
+ logging.debug(msg='\n' + response + '\n')
+ return True
+ else:
+ return False
+
+ def setposashome(self) -> bool:
+ """Save current camera position as homeposition.
+
+ Returns:
+ bool: True if successed.
+ """
+ url = (
+ self._prot + '://' + self._host + ':' + str(self._port)
+ + "/ISAPI/PTZCtrl/channels/" + str(self._chan)
+ + "/homeposition"
+ )
+ xml = ''.join(
+ ''
+ + ''
+ )
+ response = self.__call(url=url, method="PUT", contenttype="text/xml", contentdata=xml)
+ if response != 'ERROR':
+ logging.debug(msg='\n' + response + '\n')
+ return True
+ else:
+ return False
+
+ def settextonosd(self, enabled: str = "true", x: int = 0, y: int = 0, message: str = "") -> bool:
+ """Set message as video overlay text.
+
+ Args:
+ enabled (str, optional): true or false. Defaults to "true".
+ x (int, optional): horizontal text position from 0 to video width. Defaults to 0.
+ y (int, optional): vertical text position from 0 to video heith. Defaults to 0.
+ message (str, optional): overlay text content. Defaults to "".
+
+ Returns:
+ bool: True if successed.
+ """
+ if message == '-':
+ message = ""
+ url = (
+ self._prot + '://' + self._host + ':' + str(self._port)
+ + "/ISAPI/System/Video/inputs/channels/" + str(self._chan)
+ + "/overlays/text"
+ )
+ xml = ''.join(
+ ''
+ + ''
+ + ''
+ + '1'
+ + '' + enabled + ''
+ + '' + str(x) + ''
+ + '' + str(y) + ''
+ + '' + message + ''
+ + ''
+ + ''
+ )
+ response = self.__call(url=url, method="PUT", contenttype="text/xml", contentdata=xml)
+ if response != 'ERROR':
+ logging.debug(msg='\n' + response + '\n')
+ return True
+ else:
+ return False
+
+
+class Sensor(Connect):
+ """Representing sensor connected to remote host.
+ The class inherits the necessary connection methods of the Connect class
+ """
+ def __init__(
+ self,
+ hostname: str, username: str, userpass: str,
+ nodetype: str, nodename: str,
+ hostport: int = 22
+ ) -> None:
+ """Object constructor.
+
+ Args:
+ hostname (str): sensor's remote host hostname or ip address.
+ username (str): sensor's remote host username.
+ userpass (str): sensor's remote host password.
+ nodetype (str): 'ds18b20' or other sensor type.
+ nodename (str): 28-1a2b3c4d5e6f (ds18b20 example).
+ hostport (int, optional): sensor's remote host connection port. Defaults to 22.
+ """
+ self._host = hostname
+ self._port = hostport
+ self._user = username
+ self._pswd = userpass
+ self._type = nodetype
+ self._node = nodename
+
+ def __call(self, command: str) -> str:
+ """Send request to sensor's remote host.
+
+ Args:
+ command (str): command to poll the sensor.
+
+ Returns:
+ str: sensor's remote host response content.
+ """
+ return self.ssh_commands(
+ command=command,
+ hostname=self._host, port=self._port,
+ username=self._user, password=self._pswd
+ )
+
+ def __temperature(self, nodename: str) -> str:
+ """Preparating request for ds18b20 sensor type.
+
+ Args:
+ nodename (str): 28-1a2b3c4d5e6f (ds18b20 example).
+
+ Returns:
+ str: formatted string with temperature in Celsius.
+ """
+ command = 'cat /sys/bus/w1/devices/' + nodename + '/temperature'
+ response = self.__call(command=command)
+ if response != 'ERROR':
+ try:
+ temperature = str(int(response)//1000) + "'C"
+ return temperature
+ except Exception as error:
+ logging.debug(
+ msg=''
+ + '\n' + 'host: ' + self._host + ':' + str(self._port)
+ + '\n' + 'user: ' + self._user
+ + '\n' + 'pass: ' + self._pswd
+ + '\n' + 'command: ' + command
+ + '\n' + 'error: ' + str(error)
+ )
+ return 'ERROR'
+
+ def value(self) -> str:
+ """Public method to get sensor value.
+
+ Returns:
+ str: sensor value.
+ """
+ if self._type == 'ds18b20':
+ return self.__temperature(nodename=self._node)
+
+
+class Sequence:
+ """Sequence handling.
+ """
+ @staticmethod
+ def run(
+ device: HikISAPI, sensors: dict, sequence: dict,
+ records_root_path: str = None,
+ records_root_user: str = None,
+ records_root_pass: str = None
+ ) -> None:
+ """Sequences executor.
+
+ Args:
+ device (HikISAPI): HikISAPI object.
+ sensors (dict): collection as key=sensorname:value=Sensor object.
+ sequence (dict): sequence steps collection.
+ records_root_path (str, optional): path (local|smb|ftp,sftp) to records directory. Defaults to None.
+ records_root_user (str, optional): username if path on remote host. Defaults to None.
+ records_root_pass (str, optional): password if path on remote host. Defaults to None.
+ """
+ for key, value in sequence.items():
+ action = value.split(',')[0].strip()
+ x = value.split(',')[1].strip()
+ y = value.split(',')[2].strip()
+ z = value.split(',')[3].strip()
+ p = value.split(',')[4].strip()
+ s = value.split(',')[5].strip()
+ t = value.split(',')[6].strip()
+ w = value.split(',')[7].strip()
+ m = value.split(',')[8].strip()
+ if 'sensor-config:' in m:
+ sensor_name = m.split(':')[1].strip()
+ sensor_value = sensors[sensor_name].value()
+ if sensor_value != 'ERROR':
+ m = sensor_value
+ else:
+ m = ''
+ logging.info(
+ msg=' action:' + key + ' = ' + action
+ + ',' + x + ',' + y + ',' + z
+ + ',' + p + ',' + s + ',' + t
+ + ',' + w + ',' + m
+ )
+ if action == 'capabilities':
+ response = device.capabilities()
+ elif action == 'getcamerapos':
+ response = device.getcamerapos()
+ elif action == 'rebootcamera':
+ response = device.rebootcamera()
+ elif action == 'setptzmovyyu':
+ response = device.setptzmovyyu(speed=int(s))
+ elif action == 'setptzmovyyd':
+ response = device.setptzmovyyd(speed=int(s))
+ elif action == 'setptzmovxxl':
+ response = device.setptzmovxxl(speed=int(s))
+ elif action == 'setptzmovxxr':
+ response = device.setptzmovxxr(speed=int(s))
+ elif action == 'setptzmovzzi':
+ response = device.setptzmovzzi(speed=int(s))
+ elif action == 'setptzmovzzo':
+ response = device.setptzmovzzo(speed=int(s))
+ elif action == 'setptzpreset':
+ response = device.setptzpreset(preset=int(p), speed=int(s))
+ elif action == 'setptztostop':
+ response = device.setptztostop()
+ elif action == 'setcamerapos':
+ response = device.setcamerapos(x=int(x), y=int(y), z=int(z))
+ elif action == 'setcameramov':
+ response = device.setcameramov(x=int(x), y=int(y), z=int(z), t=t)
+ elif action == 'setmovtohome':
+ response = device.setmovtohome()
+ elif action == 'setposashome':
+ response = device.setposashome()
+ elif action == 'settextonosd':
+ response = device.settextonosd(x=int(x), y=int(y), message=m)
+ elif action == 'downloadjpeg':
+ records_root_temp = records_root_path
+ if records_root_temp != path.dirname(path.realpath(__file__)):
+ records_root_temp = path.dirname(path.realpath(__file__)) + sep + 'temp'
+ makedirs(records_root_temp, exist_ok=True)
+ dy = datetime.now().strftime('%Y')
+ dm = datetime.now().strftime('%m')
+ dv = datetime.now().strftime('%V')
+ dd = datetime.now().strftime('%d')
+ th = datetime.now().strftime('%H')
+ tm = datetime.now().strftime('%M')
+ ts = datetime.now().strftime('%S')
+ records_file_name = (key + '_' + dy + '-' + dm + '-' + dd + '_' + th + '.' + tm + '.' + ts + '.jpeg')
+ if device.downloadjpeg(x=int(x), y=int(y), dst_file=records_root_temp + sep + records_file_name):
+ hostname = 'localhost'
+ hostport, hosttype = None, None
+ username = records_root_user
+ userpass = records_root_pass
+ hostpath = records_root_path
+ if '://' in records_root_path:
+ hostname = records_root_path.split('/')[2]
+ hosttype = records_root_path.split('://')[0]
+ if hosttype == 'ftp':
+ hostport = 21
+ if hosttype == 'sftp':
+ hostport = 22
+ if hosttype == 'smb':
+ hostport = 445
+ hostpath = records_root_path.replace(hosttype + '://' + hostname, '')
+ if '@' in hostname:
+ username = hostname.split('@')[0].split(':')[0]
+ userpass = hostname.split('@')[0].split(':')[1]
+ hostname = hostname.split('@')[1]
+ if ':' in hostname:
+ hostport = int(hostname.split(':')[1])
+ hostname = hostname.split(':')[0]
+ if hosttype == 'ftp':
+ src_file = records_root_temp + sep + records_file_name
+ dst_file = hostpath + '/' + dy + '/' + dm + '/' + dv + '/' + dd + '/' + records_file_name
+ if Connect.ftp_put_file(
+ src_file=src_file,
+ dst_file=dst_file,
+ hostname=hostname,
+ username=username,
+ password=userpass
+ ):
+ try:
+ remove(src_file)
+ except OSError:
+ pass
+ elif hosttype == 'sftp':
+ src_file = records_root_temp + sep + records_file_name
+ dst_file = hostpath + '/' + dy + '/' + dm + '/' + dv + '/' + dd + '/' + records_file_name
+ response = Connect.ssh_put_file(
+ src_file=src_file, dst_file=dst_file,
+ hostname=hostname, port=hostport,
+ username=username, password=userpass)
+ if response != 'ERROR':
+ try:
+ remove(src_file)
+ except OSError:
+ pass
+ response = True
+ else:
+ response = False
+ else:
+ src_file = records_root_temp + sep + records_file_name
+ dst_file = hostpath + sep + dy + sep + dm + sep + dv + sep + dd + sep + records_file_name
+ try:
+ makedirs(hostpath + sep + dy + sep + dm + sep + dv + sep + dd, exist_ok=True)
+ replace(src=src_file, dst=dst_file)
+ response = True
+ except Exception as error:
+ logging.debug(
+ msg=''
+ + '\n' + 'src_file: ' + src_file
+ + '\n' + 'dst_file: ' + dst_file
+ + '\n' + 'error: ' + str(error)
+ )
+ response = False
+ else:
+ response = False
+ if w != '-' or float(w) != 0:
+ sleep(float(w))
+ if response:
+ logging.info(msg=' result:' + key + ' = OK')
+ else:
+ logging.warning(msg='result:' + key + ' = ERROR')
+
+
+class Proc:
+ """Find a running process from Python.
+ """
+ @classmethod
+ def _list_windows(cls) -> list:
+ """Find all running process with wmi.
+
+ Returns:
+ list: dictionaries with descriptions of found processes.
+ """
+ execlist = []
+ separate = b'\r\r\n'
+ out, err = Popen(
+ [
+ 'wmic', 'process', 'get',
+ 'CommandLine,ExecutablePath,Name,ProcessId',
+ '/format:list'
+ ],
+ stdout=PIPE,
+ stderr=PIPE
+ ).communicate()
+ for line in out.split(separate + separate):
+ execpid, exename, exepath, cmdline = None, None, None, None
+ for subline in line.split(separate):
+ if b'ProcessId=' in subline:
+ execpid = subline.split(b'=')[1].decode('utf-8')
+ if b'Name=' in subline:
+ exename = subline.split(b'=')[1].decode('utf-8')
+ if b'ExecutablePath=' in subline:
+ exepath = subline.split(b'=')[1].decode('utf-8')
+ if b'CommandLine=' in subline:
+ cmdline = subline.split(b'=')[1].decode('utf-8')
+ if execpid and exename:
+ execlist.append(
+ {
+ 'execpid': execpid,
+ 'exename': exename,
+ 'exepath': exepath,
+ 'cmdline': cmdline
+ }
+ )
+ return execlist
+
+ @classmethod
+ def _list_linux(cls) -> list:
+ """Find all running process with ps.
+
+ Returns:
+ list: dictionaries with descriptions of found processes.
+ """
+ execlist = []
+ out, err = Popen(
+ [
+ '/bin/ps', '-eo', 'pid,args'
+ ],
+ stdout=PIPE,
+ stderr=PIPE
+ ).communicate()
+ for line in out.splitlines():
+ execpid = line.split()[0].decode('utf-8')
+ exepath = line.split()[1].decode('utf-8')
+ exename = path.basename(exepath)
+ cmdline = line.split(None, 1)[1].decode('utf-8')
+ if execpid and exename:
+ execlist.append(
+ {
+ 'execpid': execpid,
+ 'exename': exename,
+ 'exepath': exepath,
+ 'cmdline': cmdline
+ }
+ )
+ return execlist
+
+ @classmethod
+ def list(cls) -> list:
+ """Find all running process.
+
+ Returns:
+ list: dictionaries with descriptions of found processes.
+ """
+ if platform.startswith('linux') or platform.startswith('darwin'):
+ return cls._list_linux()
+ elif platform.startswith('win32'):
+ return cls._list_windows()
+ else:
+ return None
+
+ @classmethod
+ def search(cls, find: str, exclude: str = None) -> list:
+ """Find specified processes.
+
+ Args:
+ find (str): find process pid, name or arguments.
+ exclude (str, optional): exclude process pid, name or arguments. Defaults to None.
+
+ Returns:
+ list: dictionaries with descriptions of found processes.
+ """
+ proc_found = []
+ try:
+ for proc in cls.list():
+ if exclude and (
+ exclude in proc['execpid'] or
+ exclude in proc['exename'] or
+ exclude in proc['exepath'] or
+ exclude in proc['cmdline']
+ ):
+ pass
+ elif (
+ find in proc['execpid'] or
+ find in proc['exename'] or
+ find in proc['exepath'] or
+ find in proc['cmdline']
+ ):
+ proc_found.append(proc)
+ except TypeError as ex:
+ print('ON', platform, 'PLATFORM', 'search ERROR:', ex)
+ finally:
+ if len(proc_found) == 0:
+ return None
+ else:
+ return proc_found
+
+ @classmethod
+ def kill(cls, pid: int) -> None:
+ """Kill the process by means of the OS.
+
+ Args:
+ pid (int): process ID.
+ """
+ if platform.startswith('linux') or platform.startswith('darwin'):
+ Popen(['kill', '-s', 'SIGKILL', str(pid)])
+ elif platform.startswith('win32'):
+ Popen(['taskkill', '/PID', str(pid), '/F'])
+
+
+class FFmpeg:
+ """FFmpeg management from Python.
+ """
+ @classmethod
+ def run(
+ cls,
+ src: str,
+ dst: str = None,
+ fps: int = None,
+ preset: str = None,
+ ffpath: str = None,
+ watchdog: bool = False,
+ watchsec: int = None,
+ onlyonce: bool = False
+ ) -> None:
+ """Running the installed ffmpeg
+
+ Args:
+ src (str): sources urls (example: "rtsp://user:pass@host:554/Streaming/Channels/101, anull").
+ dst (str, optional): destination url (example: rtp://239.0.0.1:5554). Defaults to None.
+ fps (int, optional): frame per second encoding output. Defaults to None.
+ preset (str, optional): 240p, 360p, 480p, 720p, 1080p, 1440p, 2160p. Defaults to None.
+ ffpath (str, optional): alternative path to bin (example: /usr/bin/ffmpeg). Defaults to None.
+ watchdog (bool, optional): detect ffmpeg freeze and terminate. Defaults to False.
+ watchsec (int, optional): seconds to wait before the watchdog terminates. Defaults to None.
+ onlyonce (bool, optional): detect ffmpeg running copy and terminate. Defaults to False.
+ """
+
+ process = (
+ cls._bin(ffpath).split() +
+ cls._src(src).split() +
+ cls._preset(preset, fps).split() +
+ cls._dst(dst).split()
+ )
+ if onlyonce and Proc.search(' '.join(process)):
+ print('Process already exist, exit...')
+ else:
+ logging.info(msg='Starting ' + ' '.join(process))
+ with Popen(process, stdout=PIPE, stderr=STDOUT) as proc:
+ que = None
+ if watchdog:
+ que = Queue()
+ Process(
+ target=cls._watchdog,
+ args=(proc.pid, watchsec, que,),
+ daemon=True
+ ).start()
+ for line in proc.stdout:
+ if not que:
+ print(line, flush=True)
+ else:
+ que.put(line)
+ exit()
+
+ @classmethod
+ def _bin(cls, path_ffmpeg: str) -> str:
+ """Returns the path to the ffmpeg depending on the OS.
+
+ Args:
+ path_ffmpeg (str): alternative path to bin.
+
+ Returns:
+ str: path to ffmpeg.
+ """
+ faq = (
+ '\n'
+ 'Main download page: https://ffmpeg.org/download.html\n'
+ '\n'
+ 'Install on Linux (Debian):\n'
+ '\tsudo apt install -y ffmpeg\n'
+ '\tTarget: /usr/bin/ffmpeg\n'
+ '\n'
+ 'Install on Windows:\n'
+ '\tDownload and extract archive from: https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z\n'
+ '\tTarget: "%PROGRAMFILES%\\ffmpeg\\bin\\ffmpeg.exe"\n'
+ '\n'
+ 'Install on MacOS:\n'
+ '\tDownload and extract archive from: https://evermeet.cx/ffmpeg/\n'
+ '\tTarget: /usr/bin/ffmpeg\n'
+ )
+ if not path_ffmpeg:
+ if platform.startswith('linux') or platform.startswith('darwin'):
+ path_ffmpeg = '/usr/bin/ffmpeg'
+ elif platform.startswith('win32'):
+ path_ffmpeg = environ['PROGRAMFILES'] + "\\ffmpeg\\bin\\ffmpeg.exe"
+ if path.exists(path_ffmpeg):
+ return path_ffmpeg
+ else:
+ print('ON', platform, 'PLATFORM', 'not found ffmpeg', faq)
+ return None
+
+ @classmethod
+ def _src(cls, sources: str) -> list:
+ """Parsing sources into ffmpeg format.
+
+ Args:
+ sources (str): comma-separated list of sources in string format.
+
+ Returns:
+ list: ffmpeg format list of sources.
+ """
+ list_sources = []
+ for src in sources.split(','):
+ src = src.strip()
+ if 'null' in src:
+ src = ' '.join(['-f lavfi -i', src])
+ elif 'rtsp' in src:
+ src = ' '.join(['-rtsp_transport tcp -i', src])
+ else:
+ src = ' '.join(['-stream_loop -1 -re -i', src])
+ list_sources.append(src)
+ return ' '.join(list_sources)
+
+ @classmethod
+ def _preset(cls, choice: str, fps: int) -> str:
+ """Parsing preset into ffmpeg format.
+
+ Args:
+ choice (str): preset selection.
+ fps (int): frame per second encoding output.
+
+ Returns:
+ str: ffmpeg format encoding parameters.
+ """
+ tune = '-tune zerolatency'
+ video = '-c:v copy'
+ audio = '-c:a aac -b:a 128k'
+ width, height, kbps = None, None, None
+ if choice:
+ if choice == '240p':
+ width, height, kbps = 426, 240, 480
+ if choice == '360p':
+ width, height, kbps = 640, 360, 720
+ if choice == '480p':
+ width, height, kbps = 854, 480, 1920
+ if choice == '720p':
+ width, height, kbps = 1280, 720, 3960
+ if choice == '1080p':
+ width, height, kbps = 1920, 1080, 5940
+ if choice == '1440p':
+ width, height, kbps = 2560, 1440, 12960
+ if choice == '2160p':
+ width, height, kbps = 3840, 2160, 32400
+ if width and height and kbps:
+ video = ''.join(
+ [
+ '-vf scale=',
+ str(width), ':', str(height),
+ ',setsar=1:1'
+ ]
+ )
+ video = ' '.join(
+ [
+ video,
+ '-c:v libx264 -pix_fmt yuv420p -preset ultrafast'
+ ]
+ )
+ if fps:
+ video = ' '.join([video, '-r', str(fps), '-g', str(fps * 2)])
+ video = ' '.join([video, '-b:v', str(kbps) + 'k'])
+ return ' '.join([tune, video, audio])
+
+ @classmethod
+ def _dst(cls, destination: str) -> str:
+ """Parsing destination into ffmpeg format.
+
+ Args:
+ destination (str): destination path or url.
+
+ Returns:
+ str: ffmpeg format destination.
+ """
+ container = '-f null'
+ stdout = '-v debug' # '-nostdin -nostats' # '-report'
+ if destination:
+ if 'rtmp' in destination:
+ container = '-f flv'
+ elif "rtp" in destination:
+ container = '-f rtp_mpegts'
+ else:
+ destination = '-'
+ return ' '.join([container, destination, stdout])
+
+ @classmethod
+ def _watchdog(cls, pid: int, sec: int, que: Queue = None) -> None:
+ """If no data arrives in the queue, kill the process.
+
+ Args:
+ pid (int): process ID.
+ sec (int): seconds to wait for data.
+ que (Queue, optional): queue pointer. Defaults to None.
+ """
+ if not sec:
+ sec = 5
+ if que:
+ while True:
+ while not que.empty():
+ print(que.get())
+ sleep(sec)
+ if que.empty():
+ Proc.kill(pid)
+ print('exit by watchdog')
+ break
+ exit()
+
+
+if __name__ == "__main__":
+ time_start = datetime.now()
+
+ args = ArgumentParser(
+ prog='cctv-scheduler',
+ description='Hikvision PTZ IP-Camera management.',
+ epilog='Dependencies: '
+ '- Python 3 (tested version 3.9.5), '
+ '- Python 3 modules: paramiko '
+ )
+ args.add_argument('--config', type=str, default=path.splitext(__file__)[0] + '.conf', required=False,
+ help='custom configuration file path')
+ args.add_argument('-b', '--broadcast', action='store_true', required=False,
+ help='streaming media to destination')
+ args.add_argument('-s', '--sequences', action='store_true', required=False,
+ help='run sequences from config file')
+ args.add_argument('-c', '--converter', action='store_true', required=False,
+ help='convert JPEG collection to MP4')
+ args.add_argument('-p', '--publisher', action='store_true', required=False,
+ help='publish content from templates')
+ args = vars(args.parse_args())
+
+ log_root = path.dirname(path.realpath(__file__))
+ log_level = 'INFO'
+ if path.exists(args['config']):
+ conf = Parse(parameters=args['config'], block='common')
+ if 'log_root' in conf.data:
+ log_root = conf.data['log_root']
+ if 'log_level' in conf.data:
+ if conf.data['log_level'] == 'DEBUG':
+ log_level = logging.DEBUG
+ elif conf.data['log_level'] == 'INFO':
+ log_level = logging.INFO
+ elif conf.data['log_level'] == 'WARNING':
+ log_level = logging.WARNING
+ elif conf.data['log_level'] == 'ERROR':
+ log_level = logging.ERROR
+ elif conf.data['log_level'] == 'CRITICAL':
+ log_level = logging.CRITICAL
+ logging.basicConfig(
+ format='%(asctime)s %(levelname)s: %(message)s',
+ datefmt='%Y-%m-%d_%H.%M.%S',
+ handlers=[
+ logging.FileHandler(
+ filename=log_root + sep + path.splitext(path.basename(__file__))[0] + '.log',
+ mode='a'
+ ),
+ logging.StreamHandler()
+ ],
+ level=log_level
+ )
+ logging.getLogger("paramiko").setLevel(logging.WARNING)
+
+ if args['broadcast']:
+ logging.info(msg='Starting streaming media to destination')
+ broadcasts = {}
+ conf = Parse(parameters=args['config'], block='enable-broadcast')
+ for key, value in conf.data.items():
+ if value == 'true':
+ broadcast_config = Parse(
+ parameters=args['config'],
+ block='broadcast-config:' + key
+ ).data
+ src = None
+ if 'src' in broadcast_config:
+ src = broadcast_config['src']
+ dst = None
+ if 'dst' in broadcast_config:
+ dst = broadcast_config['dst']
+ fps = None
+ if 'fps' in broadcast_config:
+ fps = broadcast_config['fps']
+ preset = None
+ if 'preset' in broadcast_config:
+ preset = broadcast_config['preset']
+ ffpath = None
+ if 'ffpath' in broadcast_config:
+ ffpath = broadcast_config['ffpath']
+ watchdog = None
+ if 'watchdog' in broadcast_config:
+ watchdog = broadcast_config['watchdog']
+ watchsec = None
+ if 'watchsec' in broadcast_config:
+ watchsec = int(broadcast_config['watchsec'])
+ monopoly = None
+ if 'onlyonce' in broadcast_config:
+ onlyonce = broadcast_config['onlyonce']
+ FFmpeg.run(
+ src=src,
+ dst=dst,
+ fps=fps,
+ preset=preset,
+ ffpath=ffpath,
+ watchdog=watchdog,
+ watchsec=watchsec,
+ onlyonce=onlyonce
+ )
+ elif args['sequences']:
+ logging.info(msg='Starting PTZ sequences from config file')
+ sensors = {}
+ conf = Parse(parameters=args['config'], block='enable-sensor')
+ for key, value in conf.data.items():
+ if value == 'true':
+ device_config = Parse(
+ parameters=args['config'],
+ block='sensor-config:' + key
+ ).data
+ device_entity = Sensor(
+ hostname=device_config['hostname'],
+ username=device_config['username'],
+ userpass=device_config['userpass'],
+ nodetype=device_config['nodetype'],
+ nodename=device_config['nodename']
+ )
+ sensors[key] = device_entity
+
+ conf = Parse(parameters=args['config'], block='enable-sequences')
+ for key, value in conf.data.items():
+ if value == 'true':
+ device_sequence = Parse(
+ parameters=args['config'],
+ block='camera-sequences:' + key
+ ).data
+ device_config = Parse(
+ parameters=args['config'],
+ block='camera-config:' + key
+ ).data
+ device_entity = HikISAPI(
+ hostname=device_config['hostname'],
+ username=device_config['username'],
+ userpass=device_config['userpass']
+ )
+ records_root_path = path.dirname(path.realpath(__file__))
+ records_root_user = None
+ records_root_pass = None
+ if 'records_root_path' in device_config:
+ records_root_path = device_config['records_root_path']
+ if 'records_root_user' in device_config:
+ records_root_user = device_config['records_root_user']
+ if 'records_root_pass' in device_config:
+ records_root_pass = device_config['records_root_pass']
+ Sequence.run(
+ device=device_entity,
+ sensors=sensors,
+ sequence=device_sequence,
+ records_root_path=records_root_path,
+ records_root_user=records_root_user,
+ records_root_pass=records_root_pass
+ )
+ elif args['converter']:
+ logging.info(msg='Starting convert JPEG collection to MP4')
+ elif args['publisher']:
+ logging.info(msg='Starting publish content from templates')
+ else:
+ logging.info(msg='Start arguments was not selected. Exit.')
+
+ time_execute = datetime.now() - time_start
+ logging.info(msg='execution time is ' + str(time_execute) + '. Exit.')
diff --git a/converter.conf b/archive/0.3/converter.conf
similarity index 100%
rename from converter.conf
rename to archive/0.3/converter.conf
diff --git a/converter.sh b/archive/0.3/converter.sh
similarity index 100%
rename from converter.sh
rename to archive/0.3/converter.sh
diff --git a/archive/0.3/publisher-template-page-1007.xml b/archive/0.3/publisher-template-page-1007.xml
new file mode 100644
index 0000000..4b8ab60
--- /dev/null
+++ b/archive/0.3/publisher-template-page-1007.xml
@@ -0,0 +1,525 @@
+
+wp.editPage
+
+ 1
+ wpeditpageid
+ wpxmlrpcuser
+ wpxmlrpcpass
+
+ dateCreatedwpeditdateis
+ useridwpedituserid
+ page_idwpeditpageid
+ page_statuspublish
+ description
+
+
+
+ <!-- wp:heading {"level":3} --><h3>Now</h3><!-- /wp:heading -->
+ <!-- wp:embed {"url":"youtubelink","type":"video","providerNameSlug":"youtube",
+ "responsive":true,"className":"wp-embed-aspect-4-3 wp-has-aspect-ratio"} -->
+ <figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio">
+ <div class="wp-block-embed__wrapper">
+ youtubelink
+ </div></figure><!-- /wp:embed -->
+
+
+ <!-- wp:heading {"level":3} --><h3>Yesterday</h3><!-- /wp:heading -->
+ <!-- wp:uagb/tabs {"block_id":"yesterday",
+ "tabHeaders":["currentdp01[0]",
+ "currentdp02[0]",
+ "currentdp04[0]",
+ "currentdp05[0]",
+ "currentdp11[0]",
+ "currentdp12[0]"],"tabActive":5,"activeTabBgColor":"#4b4b4b",
+ "tabTitleLeftMargin":1,"tabTitleRightMargin":1,"tabTitleTopMargin":1,"tabTitleBottomMargin":1} -->
+ <div class="wp-block-uagb-tabs uagb-block-yesterday uagb-tabs__wrap uagb-tabs__hstyle1-desktop uagb-tabs__vstyle6-tablet uagb-tabs__vstyle6-mobile" data-tab-active="0">
+ <ul class="uagb-tabs__panel uagb-tabs__align-left"><li class="uagb-tab uagb-tabs__active">
+ <a href="#uagb-tabs__tab0" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="0">
+ <span>currentdp01[0]</span></a></li><li class="uagb-tab ">
+ <a href="#uagb-tabs__tab1" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="1">
+ <span>currentdp02[0]</span></a></li><li class="uagb-tab ">
+ <a href="#uagb-tabs__tab2" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="2">
+ <span>currentdp04[0]</span></a></li><li class="uagb-tab ">
+ <a href="#uagb-tabs__tab3" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="3">
+ <span>currentdp05[0]</span></a></li><li class="uagb-tab ">
+ <a href="#uagb-tabs__tab4" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="4">
+ <span>currentdp11[0]</span></a></li><li class="uagb-tab ">
+ <a href="#uagb-tabs__tab5" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="5">
+ <span>currentdp12[0]</span></a></li></ul>
+
+ <div class="uagb-tabs__body-wrap">
+ <!-- wp:uagb/tabs-child {"block_id":"daypoint01",
+ "header":"currentdp01[0]","tabActive":5,
+ "tabHeaders":["currentdp01[0]",
+ "currentdp02[0]",
+ "currentdp04[0]",
+ "currentdp05[0]",
+ "currentdp11[0]","wp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-0">
+ <div class="uagb-blocks__daypoint01 uagb-tabs__body" aria-labelledby="uagb-tabs__tab0">
+ <!-- wp:video {"id":1036} --><figure class="wp-block-video">
+ <video controls loop src="currentdp01[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child -->
+
+ <!-- wp:uagb/tabs-child {"block_id":"daypoint02","id":1,
+ "header":"currentdp02[0]","tabActive":5,
+ "tabHeaders":["currentdp01[0]",
+ "currentdp02[0]",
+ "currentdp04[0]",
+ "currentdp05[0]",
+ "currentdp11[0]","wp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-1">
+ <div class="uagb-blocks__daypoint02 uagb-tabs__body" aria-labelledby="uagb-tabs__tab1">
+ <!-- wp:video {"id":1041} --><figure class="wp-block-video">
+ <video controls loop src="currentdp02[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child -->
+
+ <!-- wp:uagb/tabs-child {"block_id":"daypoint04","id":2,
+ "header":"currentdp04[0]","tabActive":5,
+ "tabHeaders":["currentdp01[0]",
+ "currentdp02[0]",
+ "currentdp04[0]",
+ "currentdp05[0]",
+ "currentdp11[0]","wp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-2">
+ <div class="uagb-blocks__daypoint04 uagb-tabs__body" aria-labelledby="uagb-tabs__tab2">
+ <!-- wp:video {"id":1046} --><figure class="wp-block-video">
+ <video controls loop src="currentdp04[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child -->
+
+ <!-- wp:uagb/tabs-child {"block_id":"daypoint05","id":3,
+ "header":"currentdp05[0]","tabActive":5,
+ "tabHeaders":["currentdp01[0]",
+ "currentdp02[0]",
+ "currentdp04[0]",
+ "currentdp05[0]",
+ "currentdp11[0]","wp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-3">
+ <div class="uagb-blocks__daypoint05 uagb-tabs__body" aria-labelledby="uagb-tabs__tab3">
+ <!-- wp:video {"id":1051} --><figure class="wp-block-video">
+ <video controls loop src="currentdp05[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child -->
+
+ <!-- wp:uagb/tabs-child {"block_id":"daypoint11","id":4,
+ "header":"currentdp11[0]","tabActive":5,
+ "tabHeaders":["currentdp01[0]",
+ "currentdp02[0]",
+ "currentdp04[0]",
+ "currentdp05[0]",
+ "currentdp11[0]","wp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-4">
+ <div class="uagb-blocks__daypoint11 uagb-tabs__body" aria-labelledby="uagb-tabs__tab4">
+ <!-- wp:video {"id":1056} --><figure class="wp-block-video">
+ <video controls loop src="currentdp11[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child -->
+
+ <!-- wp:uagb/tabs-child {"block_id":"daypoint12","id":5,
+ "header":"currentdp12[0]","tabActive":5,
+ "tabHeaders":["currentdp01[0]",
+ "currentdp02[0]",
+ "currentdp04[0]",
+ "currentdp05[0]",
+ "currentdp11[0]","wp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-5">
+ <div class="uagb-blocks__daypoint12 uagb-tabs__body" aria-labelledby="uagb-tabs__tab5">
+ <!-- wp:video {"id":1061} --><figure class="wp-block-video">
+ <video controls loop src="currentdp12[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child --></div></div>
+ <!-- /wp:uagb/tabs -->
+
+
+ <!-- wp:heading {"level":3} --><h3>Last week</h3><!-- /wp:heading -->
+ <!-- wp:uagb/tabs {"block_id":"lastweek",
+ "tabHeaders":["currentwp01[0]",
+ "currentwp02[0]",
+ "currentwp04[0]",
+ "currentwp05[0]",
+ "currentwp11[0]",
+ "currentwp12[0]"],"tabActive":5,"activeTabBgColor":"#4b4b4b",
+ "tabTitleLeftMargin":1,"tabTitleRightMargin":1,"tabTitleTopMargin":1,"tabTitleBottomMargin":1} -->
+ <div class="wp-block-uagb-tabs uagb-block-lastweek uagb-tabs__wrap uagb-tabs__hstyle1-desktop uagb-tabs__vstyle6-tablet uagb-tabs__vstyle6-mobile" data-tab-active="0">
+ <ul class="uagb-tabs__panel uagb-tabs__align-left"><li class="uagb-tab uagb-tabs__active">
+ <a href="#uagb-tabs__tab0" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="0">
+ <span>currentwp01[0]</span></a></li><li class="uagb-tab ">
+ <a href="#uagb-tabs__tab1" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="1">
+ <span>currentwp02[0]</span></a></li><li class="uagb-tab ">
+ <a href="#uagb-tabs__tab2" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="2">
+ <span>currentwp04[0]</span></a></li><li class="uagb-tab ">
+ <a href="#uagb-tabs__tab3" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="3">
+ <span>currentwp05[0]</span></a></li><li class="uagb-tab ">
+ <a href="#uagb-tabs__tab4" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="4">
+ <span>currentwp11[0]</span></a></li><li class="uagb-tab ">
+ <a href="#uagb-tabs__tab5" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="5">
+ <span>currentwp12[0]</span></a></li></ul>
+
+ <div class="uagb-tabs__body-wrap">
+ <!-- wp:uagb/tabs-child {"block_id":"weekpoint01",
+ "header":"currentwp01[0]","tabActive":5,
+ "tabHeaders":["currentwp01[0]",
+ "currentwp02[0]",
+ "currentwp04[0]",
+ "currentwp05[0]",
+ "currentwp11[0]","wp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-0">
+ <div class="uagb-blocks__weekpoint01 uagb-tabs__body" aria-labelledby="uagb-tabs__tab0">
+ <!-- wp:video {"id":1036} --><figure class="wp-block-video">
+ <video controls loop src="currentwp01[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child -->
+
+ <!-- wp:uagb/tabs-child {"block_id":"weekpoint02","id":1,
+ "header":"currentwp02[0]","tabActive":5,
+ "tabHeaders":["currentwp01[0]",
+ "currentwp02[0]",
+ "currentwp04[0]",
+ "currentwp05[0]",
+ "currentwp11[0]","wp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-1">
+ <div class="uagb-blocks__weekpoint02 uagb-tabs__body" aria-labelledby="uagb-tabs__tab1">
+ <!-- wp:video {"id":1041} --><figure class="wp-block-video">
+ <video controls loop src="currentwp02[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child -->
+
+ <!-- wp:uagb/tabs-child {"block_id":"weekpoint04","id":2,
+ "header":"currentwp04[0]","tabActive":5,
+ "tabHeaders":["currentwp01[0]",
+ "currentwp02[0]",
+ "currentwp04[0]",
+ "currentwp05[0]",
+ "currentwp11[0]","wp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-2">
+ <div class="uagb-blocks__weekpoint04 uagb-tabs__body" aria-labelledby="uagb-tabs__tab2">
+ <!-- wp:video {"id":1046} --><figure class="wp-block-video">
+ <video controls loop src="currentwp04[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child -->
+
+ <!-- wp:uagb/tabs-child {"block_id":"weekpoint05","id":3,
+ "header":"currentwp05[0]","tabActive":5,
+ "tabHeaders":["currentwp01[0]",
+ "currentwp02[0]",
+ "currentwp04[0]",
+ "currentwp05[0]",
+ "currentwp11[0]","wp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-3">
+ <div class="uagb-blocks__weekpoint05 uagb-tabs__body" aria-labelledby="uagb-tabs__tab3">
+ <!-- wp:video {"id":1051} --><figure class="wp-block-video">
+ <video controls loop src="currentwp05[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child -->
+
+ <!-- wp:uagb/tabs-child {"block_id":"weekpoint11","id":4,
+ "header":"currentwp11[0]","tabActive":5,
+ "tabHeaders":["currentwp01[0]",
+ "currentwp02[0]",
+ "currentwp04[0]",
+ "currentwp05[0]",
+ "currentwp11[0]","wp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-4">
+ <div class="uagb-blocks__weekpoint11 uagb-tabs__body" aria-labelledby="uagb-tabs__tab4">
+ <!-- wp:video {"id":1056} --><figure class="wp-block-video">
+ <video controls loop src="currentwp11[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child -->
+
+ <!-- wp:uagb/tabs-child {"block_id":"weekpoint12","id":5,
+ "header":"currentwp12[0]","tabActive":5,
+ "tabHeaders":["currentwp01[0]",
+ "currentwp02[0]",
+ "currentwp04[0]",
+ "currentwp05[0]",
+ "currentwp11[0]","wp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-5">
+ <div class="uagb-blocks__weekpoint12 uagb-tabs__body" aria-labelledby="uagb-tabs__tab5">
+ <!-- wp:video {"id":1061} --><figure class="wp-block-video">
+ <video controls loop src="currentwp12[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child --></div></div>
+ <!-- /wp:uagb/tabs -->
+
+
+ <!-- wp:heading {"level":3} --><h3>Last month</h3><!-- /wp:heading -->
+ <!-- wp:uagb/tabs {"block_id":"lastmonth",
+ "tabHeaders":["currentmp01[0]",
+ "currentmp02[0]",
+ "currentmp04[0]",
+ "currentmp05[0]",
+ "currentmp11[0]",
+ "currentmp12[0]"],"tabActive":5,"activeTabBgColor":"#4b4b4b",
+ "tabTitleLeftMargin":1,"tabTitleRightMargin":1,"tabTitleTopMargin":1,"tabTitleBottomMargin":1} -->
+ <div class="wp-block-uagb-tabs uagb-block-lastmonth uagb-tabs__wrap uagb-tabs__hstyle1-desktop uagb-tabs__vstyle6-tablet uagb-tabs__vstyle6-mobile" data-tab-active="0">
+ <ul class="uagb-tabs__panel uagb-tabs__align-left"><li class="uagb-tab uagb-tabs__active">
+ <a href="#uagb-tabs__tab0" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="0">
+ <span>currentmp01[0]</span></a></li><li class="uagb-tab ">
+ <a href="#uagb-tabs__tab1" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="1">
+ <span>currentmp02[0]</span></a></li><li class="uagb-tab ">
+ <a href="#uagb-tabs__tab2" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="2">
+ <span>currentmp04[0]</span></a></li><li class="uagb-tab ">
+ <a href="#uagb-tabs__tab3" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="3">
+ <span>currentmp05[0]</span></a></li><li class="uagb-tab ">
+ <a href="#uagb-tabs__tab4" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="4">
+ <span>currentmp11[0]</span></a></li><li class="uagb-tab ">
+ <a href="#uagb-tabs__tab5" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="5">
+ <span>currentmp12[0]</span></a></li></ul>
+
+ <div class="uagb-tabs__body-wrap">
+ <!-- wp:uagb/tabs-child {"block_id":"monthpoint01",
+ "header":"currentmp01[0]","tabActive":5,
+ "tabHeaders":["currentmp01[0]",
+ "currentmp02[0]",
+ "currentmp04[0]",
+ "currentmp05[0]",
+ "currentmp11[0]","mp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-0">
+ <div class="uagb-blocks__monthpoint01 uagb-tabs__body" aria-labelledby="uagb-tabs__tab0">
+ <!-- wp:video {"id":1037} --><figure class="wp-block-video">
+ <video controls loop src="currentmp01[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child -->
+
+ <!-- wp:uagb/tabs-child {"block_id":"monthpoint02","id":1,
+ "header":"currentmp02[0]","tabActive":5,
+ "tabHeaders":["currentmp01[0]",
+ "currentmp02[0]",
+ "currentmp04[0]",
+ "currentmp05[0]",
+ "currentmp11[0]","mp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-1">
+ <div class="uagb-blocks__monthpoint02 uagb-tabs__body" aria-labelledby="uagb-tabs__tab1">
+ <!-- wp:video {"id":1042} --><figure class="wp-block-video">
+ <video controls loop src="currentmp02[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child -->
+
+ <!-- wp:uagb/tabs-child {"block_id":"monthpoint04","id":2,
+ "header":"currentmp04[0]","tabActive":5,
+ "tabHeaders":["currentmp01[0]",
+ "currentmp02[0]",
+ "currentmp04[0]",
+ "currentmp05[0]",
+ "currentmp11[0]","mp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-2">
+ <div class="uagb-blocks__monthpoint04 uagb-tabs__body" aria-labelledby="uagb-tabs__tab2">
+ <!-- wp:video {"id":1047} --><figure class="wp-block-video">
+ <video controls loop src="currentmp04[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child -->
+
+ <!-- wp:uagb/tabs-child {"block_id":"monthpoint05","id":3,
+ "header":"currentmp05[0]","tabActive":5,
+ "tabHeaders":["currentmp01[0]",
+ "currentmp02[0]",
+ "currentmp04[0]",
+ "currentmp05[0]",
+ "currentmp11[0]","mp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-3">
+ <div class="uagb-blocks__monthpoint05 uagb-tabs__body" aria-labelledby="uagb-tabs__tab3">
+ <!-- wp:video {"id":1052} --><figure class="wp-block-video">
+ <video controls loop src="currentmp05[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child -->
+
+ <!-- wp:uagb/tabs-child {"block_id":"monthpoint11","id":4,
+ "header":"currentmp11[0]","tabActive":5,
+ "tabHeaders":["currentmp01[0]",
+ "currentmp02[0]",
+ "currentmp04[0]",
+ "currentmp05[0]",
+ "currentmp11[0]","mp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-4">
+ <div class="uagb-blocks__monthpoint11 uagb-tabs__body" aria-labelledby="uagb-tabs__tab4">
+ <!-- wp:video {"id":1057} --><figure class="wp-block-video">
+ <video controls loop src="currentmp11[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child -->
+
+ <!-- wp:uagb/tabs-child {"block_id":"monthpoint12","id":5,
+ "header":"currentmp12[0]","tabActive":5,
+ "tabHeaders":["currentmp01[0]",
+ "currentmp02[0]",
+ "currentmp04[0]",
+ "currentmp05[0]",
+ "currentmp11[0]","mp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-5">
+ <div class="uagb-blocks__monthpoint12 uagb-tabs__body" aria-labelledby="uagb-tabs__tab5">
+ <!-- wp:video {"id":1062} --><figure class="wp-block-video">
+ <video controls loop src="currentmp12[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child --></div></div>
+ <!-- /wp:uagb/tabs -->
+
+
+ <!-- wp:heading {"level":3} --><h3>Last year</h3><!-- /wp:heading -->
+ <!-- wp:uagb/tabs {"block_id":"lastyear",
+ "tabHeaders":["currentyp01[0]",
+ "currentyp02[0]",
+ "currentyp04[0]",
+ "currentyp05[0]",
+ "currentyp11[0]",
+ "currentyp12[0]"],"tabActive":5,"activeTabBgColor":"#4b4b4b",
+ "tabTitleLeftMargin":1,"tabTitleRightMargin":1,"tabTitleTopMargin":1,"tabTitleBottomMargin":1} -->
+ <div class="wp-block-uagb-tabs uagb-block-lastyear uagb-tabs__wrap uagb-tabs__hstyle1-desktop uagb-tabs__vstyle6-tablet uagb-tabs__vstyle6-mobile" data-tab-active="0">
+ <ul class="uagb-tabs__panel uagb-tabs__align-left"><li class="uagb-tab uagb-tabs__active">
+ <a href="#uagb-tabs__tab0" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="0">
+ <span>currentyp01[0]</span></a></li><li class="uagb-tab ">
+ <a href="#uagb-tabs__tab1" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="1">
+ <span>currentyp02[0]</span></a></li><li class="uagb-tab ">
+ <a href="#uagb-tabs__tab2" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="2">
+ <span>currentyp04[0]</span></a></li><li class="uagb-tab ">
+ <a href="#uagb-tabs__tab3" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="3">
+ <span>currentyp05[0]</span></a></li><li class="uagb-tab ">
+ <a href="#uagb-tabs__tab4" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="4">
+ <span>currentyp11[0]</span></a></li><li class="uagb-tab ">
+ <a href="#uagb-tabs__tab5" class="uagb-tabs-list uagb-tabs__icon-position-left" data-tab="5">
+ <span>currentyp12[0]</span></a></li></ul>
+
+ <div class="uagb-tabs__body-wrap">
+ <!-- wp:uagb/tabs-child {"block_id":"yearpoint01",
+ "header":"currentyp01[0]","tabActive":5,
+ "tabHeaders":["currentyp01[0]",
+ "currentyp02[0]",
+ "currentyp04[0]",
+ "currentyp05[0]",
+ "currentyp11[0]","yp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-0">
+ <div class="uagb-blocks__yearpoint01 uagb-tabs__body" aria-labelledby="uagb-tabs__tab0">
+ <!-- wp:video {"id":1038} --><figure class="wp-block-video">
+ <video controls loop src="currentyp01[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child -->
+
+ <!-- wp:uagb/tabs-child {"block_id":"yearpoint02","id":1,
+ "header":"currentyp02[0]","tabActive":5,
+ "tabHeaders":["currentyp01[0]",
+ "currentyp02[0]",
+ "currentyp04[0]",
+ "currentyp05[0]",
+ "currentyp11[0]","yp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-1">
+ <div class="uagb-blocks__yearpoint02 uagb-tabs__body" aria-labelledby="uagb-tabs__tab1">
+ <!-- wp:video {"id":1043} --><figure class="wp-block-video">
+ <video controls loop src="currentyp02[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child -->
+
+ <!-- wp:uagb/tabs-child {"block_id":"yearpoint04","id":2,
+ "header":"currentyp04[0]","tabActive":5,
+ "tabHeaders":["currentyp01[0]",
+ "currentyp02[0]",
+ "currentyp04[0]",
+ "currentyp05[0]",
+ "currentyp11[0]","yp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-2">
+ <div class="uagb-blocks__yearpoint04 uagb-tabs__body" aria-labelledby="uagb-tabs__tab2">
+ <!-- wp:video {"id":1048} --><figure class="wp-block-video">
+ <video controls loop src="currentyp04[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child -->
+
+ <!-- wp:uagb/tabs-child {"block_id":"yearpoint05","id":3,
+ "header":"currentyp05[0]","tabActive":5,
+ "tabHeaders":["currentyp01[0]",
+ "currentyp02[0]",
+ "currentyp04[0]",
+ "currentyp05[0]",
+ "currentyp11[0]","yp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-3">
+ <div class="uagb-blocks__yearpoint05 uagb-tabs__body" aria-labelledby="uagb-tabs__tab3">
+ <!-- wp:video {"id":1053} --><figure class="wp-block-video">
+ <video controls loop src="currentyp05[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child -->
+
+ <!-- wp:uagb/tabs-child {"block_id":"yearpoint11","id":4,
+ "header":"currentyp11[0]","tabActive":5,
+ "tabHeaders":["currentyp01[0]",
+ "currentyp02[0]",
+ "currentyp04[0]",
+ "currentyp05[0]",
+ "currentyp11[0]","yp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-4">
+ <div class="uagb-blocks__yearpoint11 uagb-tabs__body" aria-labelledby="uagb-tabs__tab4">
+ <!-- wp:video {"id":1058} --><figure class="wp-block-video">
+ <video controls loop src="currentyp11[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child -->
+
+ <!-- wp:uagb/tabs-child {"block_id":"yearpoint12","id":5,
+ "header":"currentyp12[0]","tabActive":5,
+ "tabHeaders":["currentyp01[0]",
+ "currentyp02[0]",
+ "currentyp04[0]",
+ "currentyp05[0]",
+ "currentyp11[0]","yp1tab"]} -->
+ <div class="wp-block-uagb-tabs-child uagb-tabs__body-container uagb-tabs__inner-tab uagb-inner-tab-5">
+ <div class="uagb-blocks__yearpoint12 uagb-tabs__body" aria-labelledby="uagb-tabs__tab5">
+ <!-- wp:video {"id":1063} --><figure class="wp-block-video">
+ <video controls loop src="currentyp12[1]">
+ </video></figure><!-- /wp:video --></div></div><!-- /wp:uagb/tabs-child --></div></div>
+ <!-- /wp:uagb/tabs -->
+
+
+
+
+ titleMedia
+ linkwppagelinkis
+ permaLinkwppagelinkis
+ categories
+ excerpt
+ text_more
+ mt_allow_comments0
+ mt_allow_pings0
+ wp_slugmedia
+ wp_password
+ wp_authorwppageauthor
+ wp_page_parent_id0
+ wp_page_parent_title
+ wp_page_order0
+ wp_author_idwpedituserid
+ wp_author_display_namewppageauthor
+ date_created_gmtwpeditdateis
+ custom_fields
+
+ id4258
+ keycontent_display_featured_image
+ value0
+
+
+ id4257
+ keycontent_display_title
+ value0
+
+
+ id4259
+ keycontent_width
+ valuecontainer
+
+
+ id4253
+ keydisable_space_above_footer
+ value0
+
+
+ id4252
+ keydisable_space_below_header
+ value0
+
+
+ id4251
+ keyhide_footer
+ value0
+
+
+ id4250
+ keyhide_header
+ value0
+
+
+ id4254
+ keyhide_titlebar
+ value0
+
+
+ id4255
+ keypage_overwrite_defaults
+ value0
+
+
+ id4260
+ keysidebar_layout
+ valuedefault-sidebar
+
+
+ id4256
+ keytitlebar_padding
+ value80
+
+
+ wp_page_templatedefault
+
+
+
\ No newline at end of file
diff --git a/archive/0.3/publisher.conf b/archive/0.3/publisher.conf
new file mode 100644
index 0000000..e9b7d94
--- /dev/null
+++ b/archive/0.3/publisher.conf
@@ -0,0 +1,86 @@
+logs=./publisher.log
+
+pathroot=/home/user/cctv-scheduler/records
+vidnamesarray=point-01 point-02 point-04 point-05 point-11 point-12
+
+
+telegramapiurl=https://api.telegram.org/bot1234567890:YOURAPIKEY
+telegramchatid=123456789
+tgpreviewlink=https://www.hmp.today/wp-content/uploads/2021/02/site-slider_hmp-qr_bwg-3840x1705-1.png
+tgpreviewtext=https://www.hmp.today/media
+
+
+wpxmlrpclink=https://www.hmp.today/xmlrpc.php
+wpxmlrpcuser=user
+wpxmlrpcpass=pass
+wppageauthor=author
+wppagelinkis=https://www.hmp.today/media
+wpeditpageid=1007
+wpedituserid=0
+wpeditdateis=20220707T00:00:00
+wptemplateis=./publisher-template-page-1007.xml
+
+
+youtubelink=https://youtu.be/link
+
+
+defaultdp01=point-01 https://www.hmp.today/wp-content/uploads/2022/07/point-01_yyyy.mm_.dd_.mp4
+defaultwp01=point-01 https://www.hmp.today/wp-content/uploads/2022/07/point-01_w.mp4
+defaultmp01=point-01 https://www.hmp.today/wp-content/uploads/2022/07/point-01_yyyy.mm_.mp4
+defaultyp01=point-01 https://www.hmp.today/wp-content/uploads/2022/07/point-01_yyyy.mp4
+
+defaultdp02=point-02 https://www.hmp.today/wp-content/uploads/2022/07/point-02_yyyy.mm_.dd_.mp4
+defaultwp02=point-02 https://www.hmp.today/wp-content/uploads/2022/07/point-02_w.mp4
+defaultmp02=point-02 https://www.hmp.today/wp-content/uploads/2022/07/point-02_yyyy.mm_.mp4
+defaultyp02=point-02 https://www.hmp.today/wp-content/uploads/2022/07/point-02_yyyy.mp4
+
+defaultdp04=point-04 https://www.hmp.today/wp-content/uploads/2022/07/point-04_yyyy.mm_.dd_.mp4
+defaultwp04=point-04 https://www.hmp.today/wp-content/uploads/2022/07/point-04_w.mp4
+defaultmp04=point-04 https://www.hmp.today/wp-content/uploads/2022/07/point-04_yyyy.mm_.mp4
+defaultyp04=point-04 https://www.hmp.today/wp-content/uploads/2022/07/point-04_yyyy.mp4
+
+defaultdp05=point-05 https://www.hmp.today/wp-content/uploads/2022/07/point-05_yyyy.mm_.dd_.mp4
+defaultwp05=point-05 https://www.hmp.today/wp-content/uploads/2022/07/point-05_w.mp4
+defaultmp05=point-05 https://www.hmp.today/wp-content/uploads/2022/07/point-05_yyyy.mm_.mp4
+defaultyp05=point-05 https://www.hmp.today/wp-content/uploads/2022/07/point-05_yyyy.mp4
+
+defaultdp11=point-11 https://www.hmp.today/wp-content/uploads/2022/07/point-11_yyyy.mm_.dd_.mp4
+defaultwp11=point-11 https://www.hmp.today/wp-content/uploads/2022/07/point-11_w.mp4
+defaultmp11=point-11 https://www.hmp.today/wp-content/uploads/2022/07/point-11_yyyy.mm_.mp4
+defaultyp11=point-11 https://www.hmp.today/wp-content/uploads/2022/07/point-11_yyyy.mp4
+
+defaultdp12=point-12 https://www.hmp.today/wp-content/uploads/2022/07/point-12_yyyy.mm_.dd_.mp4
+defaultwp12=point-12 https://www.hmp.today/wp-content/uploads/2022/07/point-12_w.mp4
+defaultmp12=point-12 https://www.hmp.today/wp-content/uploads/2022/07/point-12_yyyy.mm_.mp4
+defaultyp12=point-12 https://www.hmp.today/wp-content/uploads/2022/07/point-12_yyyy.mp4
+
+
+currentdp01=point-01
+currentwp01=point-01
+currentmp01=point-01
+currentyp01=point-01
+
+currentdp02=point-02
+currentwp02=point-02
+currentmp02=point-02
+currentyp02=point-02
+
+currentdp04=point-04
+currentwp04=point-04
+currentmp04=point-04
+currentyp04=point-04
+
+currentdp05=point-05
+currentwp05=point-05
+currentmp05=point-05
+currentyp05=point-05
+
+currentdp11=point-11
+currentwp11=point-11
+currentmp11=point-11
+currentyp11=point-11
+
+currentdp12=point-12
+currentwp12=point-12
+currentmp12=point-12
+currentyp12=point-12
diff --git a/archive/0.3/publisher.sh b/archive/0.3/publisher.sh
new file mode 100644
index 0000000..205da6d
--- /dev/null
+++ b/archive/0.3/publisher.sh
@@ -0,0 +1,599 @@
+#! /bin/bash
+
+# DESCRIPTION:
+# Uploading MP4 to Wordpress and Telegram.
+# Additionally:
+# - editing Wordpress page from template
+# - recompressing video if size over 50MB
+# This is only a local "proof of conept" for testing and debugging.
+#
+# DEPENDENCIES:
+# - curl
+# - ffmpeg
+# - libxml2-utils
+# - jq
+#
+# PARAMETERS:
+# 1: "qn" - execution without pauses
+# 2: custom configuration file path
+# 3: periods: '' - today | '-d' - yesterday | '-w' - last week | '-m' - last month | '-y' - last year
+# 4: period multiplier: '' - 1 day|week|month|year
+# 5: publishing '--onlytg' - only to Telegram | '--onlywp' - only to Wordpress
+#
+# FUNCTIONS:
+#
+
+#######################################
+# Print message and add to log.
+# Globals:
+# logs
+# Arguments:
+# 1: message to print and logging
+#######################################
+addtologs() {
+ echo "$(date +'%Y.%m.%d-%H:%M:%S') $1" | tee -a "${logs}"
+}
+
+#######################################
+# Waiting for press [ENTER].
+# Globals:
+# None
+# Arguments:
+# None
+#######################################
+execpause() {
+ read -r -p "Press [ENTER] to continue... "
+}
+
+#######################################
+# Exit procedure.
+# Globals:
+# show
+# Arguments:
+# None
+#######################################
+execquite() {
+ addtologs "execution time is $(($(date +%s)-time)) seconds, exit"
+ if [ "${show}" != "qn" ]; then
+ execpause
+ fi
+ exit
+}
+
+#######################################
+# Error exit procedure with Telegram notification.
+# Globals:
+# telegramapiurl
+# telegramchatid
+# Arguments:
+# 1: message to print and logging
+#######################################
+execerror() {
+ addtologs "error: $1"
+ curl -s -X POST "${telegramapiurl}/sendMessage" \
+ -d "chat_id=${telegramchatid}" \
+ -d "text=$(basename -s .sh "$0") error: $1" \
+ >> /dev/null 2>&1
+ execquite
+}
+
+#######################################
+# Parsing config file and creating global vars.
+# Globals:
+# None
+# Arguments:
+# None
+#######################################
+getconfig() {
+ logs=$(grep "logs=" "${conf}" | cut -d= -f2)
+ pathroot=$(grep "pathroot=" "${conf}" | cut -d= -f2)
+ IFS=" " read -r -a vidnamesarray <<< "$(grep "vidnamesarray=" "${conf}" | cut -d= -f2)"
+ telegramapiurl=$(grep "telegramapiurl=" "${conf}" | cut -d= -f2)
+ telegramchatid=$(grep "telegramchatid=" "${conf}" | cut -d= -f2)
+ tgpreviewlink=$(grep "tgpreviewlink=" "${conf}" | cut -d= -f2)
+ tgpreviewtext=$(grep "tgpreviewtext=" "${conf}" | cut -d= -f2)
+ wpxmlrpclink=$(grep "wpxmlrpclink=" "${conf}" | cut -d= -f2)
+ wpxmlrpcuser=$(grep "wpxmlrpcuser=" "${conf}" | cut -d= -f2)
+ wpxmlrpcpass=$(grep "wpxmlrpcpass=" "${conf}" | cut -d= -f2)
+ wppageauthor=$(grep "wppageauthor=" "${conf}" | cut -d= -f2)
+ wppagelinkis=$(grep "wppagelinkis=" "${conf}" | cut -d= -f2)
+ wpeditpageid=$(grep "wpeditpageid=" "${conf}" | cut -d= -f2)
+ wpedituserid=$(grep "wpedituserid=" "${conf}" | cut -d= -f2)
+ wpeditdateis=$(grep "wpeditdateis=" "${conf}" | cut -d= -f2)
+ wptemplateis=$(grep "wptemplateis=" "${conf}" | cut -d= -f2)
+ youtubelink=$(grep "youtubelink=" "${conf}" | cut -d= -f2)
+
+ IFS=" " read -r -a defaultdp01 <<< "$(grep "defaultdp01=" "${conf}" | cut -d= -f2)"
+ IFS=" " read -r -a defaultwp01 <<< "$(grep "defaultwp01=" "${conf}" | cut -d= -f2)"
+ IFS=" " read -r -a defaultmp01 <<< "$(grep "defaultmp01=" "${conf}" | cut -d= -f2)"
+ IFS=" " read -r -a defaultyp01 <<< "$(grep "defaultyp01=" "${conf}" | cut -d= -f2)"
+
+ IFS=" " read -r -a defaultdp02 <<< "$(grep "defaultdp02=" "${conf}" | cut -d= -f2)"
+ IFS=" " read -r -a defaultwp02 <<< "$(grep "defaultwp02=" "${conf}" | cut -d= -f2)"
+ IFS=" " read -r -a defaultmp02 <<< "$(grep "defaultmp02=" "${conf}" | cut -d= -f2)"
+ IFS=" " read -r -a defaultyp02 <<< "$(grep "defaultyp02=" "${conf}" | cut -d= -f2)"
+
+ IFS=" " read -r -a defaultdp04 <<< "$(grep "defaultdp04=" "${conf}" | cut -d= -f2)"
+ IFS=" " read -r -a defaultwp04 <<< "$(grep "defaultwp04=" "${conf}" | cut -d= -f2)"
+ IFS=" " read -r -a defaultmp04 <<< "$(grep "defaultmp04=" "${conf}" | cut -d= -f2)"
+ IFS=" " read -r -a defaultyp04 <<< "$(grep "defaultyp04=" "${conf}" | cut -d= -f2)"
+
+ IFS=" " read -r -a defaultdp05 <<< "$(grep "defaultdp05=" "${conf}" | cut -d= -f2)"
+ IFS=" " read -r -a defaultwp05 <<< "$(grep "defaultwp05=" "${conf}" | cut -d= -f2)"
+ IFS=" " read -r -a defaultmp05 <<< "$(grep "defaultmp05=" "${conf}" | cut -d= -f2)"
+ IFS=" " read -r -a defaultyp05 <<< "$(grep "defaultyp05=" "${conf}" | cut -d= -f2)"
+
+ IFS=" " read -r -a defaultdp11 <<< "$(grep "defaultdp11=" "${conf}" | cut -d= -f2)"
+ IFS=" " read -r -a defaultwp11 <<< "$(grep "defaultwp11=" "${conf}" | cut -d= -f2)"
+ IFS=" " read -r -a defaultmp11 <<< "$(grep "defaultmp11=" "${conf}" | cut -d= -f2)"
+ IFS=" " read -r -a defaultyp11 <<< "$(grep "defaultyp11=" "${conf}" | cut -d= -f2)"
+
+ IFS=" " read -r -a defaultdp12 <<< "$(grep "defaultdp12=" "${conf}" | cut -d= -f2)"
+ IFS=" " read -r -a defaultwp12 <<< "$(grep "defaultwp12=" "${conf}" | cut -d= -f2)"
+ IFS=" " read -r -a defaultmp12 <<< "$(grep "defaultmp12=" "${conf}" | cut -d= -f2)"
+ IFS=" " read -r -a defaultyp12 <<< "$(grep "defaultyp12=" "${conf}" | cut -d= -f2)"
+
+ IFS=" " read -r -a currentdp01 <<< "$(grep "currentdp01=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentdp01[1]}" ]; then
+ currentdp01=${defaultdp01[*]}
+ fi
+ IFS=" " read -r -a currentwp01 <<< "$(grep "currentwp01=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentwp01[1]}" ]; then
+ currentwp01=${defaultwp01[*]}
+ fi
+ IFS=" " read -r -a currentmp01 <<< "$(grep "currentmp01=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentmp01[1]}" ]; then
+ currentmp01=${defaultmp01[*]}
+ fi
+ IFS=" " read -r -a currentyp01 <<< "$(grep "currentyp01=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentyp01[1]}" ]; then
+ currentyp01=${defaultyp01[*]}
+ fi
+
+ IFS=" " read -r -a currentdp02 <<< "$(grep "currentdp02=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentdp02[1]}" ]; then
+ currentdp02=${defaultdp02[*]}
+ fi
+ IFS=" " read -r -a currentwp02 <<< "$(grep "currentwp02=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentwp02[1]}" ]; then
+ currentwp02=${defaultwp02[*]}
+ fi
+ IFS=" " read -r -a currentmp02 <<< "$(grep "currentmp02=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentmp02[1]}" ]; then
+ currentmp02=${defaultmp02[*]}
+ fi
+ IFS=" " read -r -a currentyp02 <<< "$(grep "currentyp02=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentyp02[1]}" ]; then
+ currentyp02=${defaultyp02[*]}
+ fi
+
+ IFS=" " read -r -a currentdp04 <<< "$(grep "currentdp04=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentdp04[1]}" ]; then
+ currentdp04=${defaultdp04[*]}
+ fi
+ IFS=" " read -r -a currentwp04 <<< "$(grep "currentwp04=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentwp04[1]}" ]; then
+ currentwp04=${defaultwp04[*]}
+ fi
+ IFS=" " read -r -a currentmp04 <<< "$(grep "currentmp04=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentmp04[1]}" ]; then
+ currentmp04=${defaultmp04[*]}
+ fi
+ IFS=" " read -r -a currentyp04 <<< "$(grep "currentyp04=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentyp04[1]}" ]; then
+ currentyp04=${defaultyp04[*]}
+ fi
+
+ IFS=" " read -r -a currentdp05 <<< "$(grep "currentdp05=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentdp05[1]}" ]; then
+ currentdp05=${defaultdp05[*]}
+ fi
+ IFS=" " read -r -a currentwp05 <<< "$(grep "currentwp05=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentwp05[1]}" ]; then
+ currentwp05=${defaultwp05[*]}
+ fi
+ IFS=" " read -r -a currentmp05 <<< "$(grep "currentmp05=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentmp05[1]}" ]; then
+ currentmp05=${defaultmp05[*]}
+ fi
+ IFS=" " read -r -a currentyp05 <<< "$(grep "currentyp05=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentyp05[1]}" ]; then
+ currentyp05=${defaultyp05[*]}
+ fi
+
+ IFS=" " read -r -a currentdp11 <<< "$(grep "currentdp11=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentdp11[1]}" ]; then
+ currentdp11=${defaultdp11[*]}
+ fi
+ IFS=" " read -r -a currentwp11 <<< "$(grep "currentwp11=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentwp11[1]}" ]; then
+ currentwp11=${defaultwp11[*]}
+ fi
+ IFS=" " read -r -a currentmp11 <<< "$(grep "currentmp11=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentmp11[1]}" ]; then
+ currentmp11=${defaultmp11[*]}
+ fi
+ IFS=" " read -r -a currentyp11 <<< "$(grep "currentyp11=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentyp11[1]}" ]; then
+ currentyp11=${defaultyp11[*]}
+ fi
+
+ IFS=" " read -r -a currentdp12 <<< "$(grep "currentdp12=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentdp12[1]}" ]; then
+ currentdp12=${defaultdp12[*]}
+ fi
+ IFS=" " read -r -a currentwp12 <<< "$(grep "currentwp12=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentwp12[1]}" ]; then
+ currentwp12=${defaultwp12[*]}
+ fi
+ IFS=" " read -r -a currentmp12 <<< "$(grep "currentmp12=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentmp12[1]}" ]; then
+ currentmp12=${defaultmp12[*]}
+ fi
+ IFS=" " read -r -a currentyp12 <<< "$(grep "currentyp12=" "${conf}" | cut -d= -f2)"
+ if [ -z "${currentyp12[1]}" ]; then
+ currentyp12=${defaultyp12[*]}
+ fi
+}
+
+#######################################
+# Writing changes to configuration file.
+# Globals:
+# conf
+# when
+# vidnamesarray
+# vidlinksarray
+# Arguments:
+# None
+#######################################
+setconfig() {
+ if [ -z "${when}" ] || [ "${when}" == "-d" ]; then
+ if [ -n "${vidnamesarray[0]}" ] && [ -n "${vidlinksarray[0]}" ]; then
+ sed -i "s#$(grep 'currentdp01=' "${conf}")#currentdp01=${vidnamesarray[0]} ${vidlinksarray[0]}#" "${conf}"
+ fi
+ if [ -n "${vidnamesarray[1]}" ] && [ -n "${vidlinksarray[1]}" ]; then
+ sed -i "s#$(grep 'currentdp02=' "${conf}")#currentdp02=${vidnamesarray[1]} ${vidlinksarray[1]}#" "${conf}"
+ fi
+ if [ -n "${vidnamesarray[2]}" ] && [ -n "${vidlinksarray[2]}" ]; then
+ sed -i "s#$(grep 'currentdp04=' "${conf}")#currentdp04=${vidnamesarray[2]} ${vidlinksarray[2]}#" "${conf}"
+ fi
+ if [ -n "${vidnamesarray[3]}" ] && [ -n "${vidlinksarray[3]}" ]; then
+ sed -i "s#$(grep 'currentdp05=' "${conf}")#currentdp05=${vidnamesarray[3]} ${vidlinksarray[3]}#" "${conf}"
+ fi
+ if [ -n "${vidnamesarray[4]}" ] && [ -n "${vidlinksarray[4]}" ]; then
+ sed -i "s#$(grep 'currentdp11=' "${conf}")#currentdp11=${vidnamesarray[4]} ${vidlinksarray[4]}#" "${conf}"
+ fi
+ if [ -n "${vidnamesarray[5]}" ] && [ -n "${vidlinksarray[5]}" ]; then
+ sed -i "s#$(grep 'currentdp12=' "${conf}")#currentdp12=${vidnamesarray[5]} ${vidlinksarray[5]}#" "${conf}"
+ fi
+ fi
+ if [ "${when}" == "-w" ]; then
+ if [ -n "${vidnamesarray[0]}" ] && [ -n "${vidlinksarray[0]}" ]; then
+ sed -i "s#$(grep 'currentwp01=' "${conf}")#currentwp01=${vidnamesarray[0]} ${vidlinksarray[0]}#" "${conf}"
+ fi
+ if [ -n "${vidnamesarray[1]}" ] && [ -n "${vidlinksarray[1]}" ]; then
+ sed -i "s#$(grep 'currentwp02=' "${conf}")#currentwp02=${vidnamesarray[1]} ${vidlinksarray[1]}#" "${conf}"
+ fi
+ if [ -n "${vidnamesarray[2]}" ] && [ -n "${vidlinksarray[2]}" ]; then
+ sed -i "s#$(grep 'currentwp04=' "${conf}")#currentwp04=${vidnamesarray[2]} ${vidlinksarray[2]}#" "${conf}"
+ fi
+ if [ -n "${vidnamesarray[3]}" ] && [ -n "${vidlinksarray[3]}" ]; then
+ sed -i "s#$(grep 'currentwp05=' "${conf}")#currentwp05=${vidnamesarray[3]} ${vidlinksarray[3]}#" "${conf}"
+ fi
+ if [ -n "${vidnamesarray[4]}" ] && [ -n "${vidlinksarray[4]}" ]; then
+ sed -i "s#$(grep 'currentwp11=' "${conf}")#currentwp11=${vidnamesarray[4]} ${vidlinksarray[4]}#" "${conf}"
+ fi
+ if [ -n "${vidnamesarray[5]}" ] && [ -n "${vidlinksarray[5]}" ]; then
+ sed -i "s#$(grep 'currentwp12=' "${conf}")#currentwp12=${vidnamesarray[5]} ${vidlinksarray[5]}#" "${conf}"
+ fi
+ fi
+ if [ "${when}" == "-m" ]; then
+ if [ -n "${vidnamesarray[0]}" ] && [ -n "${vidlinksarray[0]}" ]; then
+ sed -i "s#$(grep 'currentmp01=' "${conf}")#currentmp01=${vidnamesarray[0]} ${vidlinksarray[0]}#" "${conf}"
+ fi
+ if [ -n "${vidnamesarray[1]}" ] && [ -n "${vidlinksarray[1]}" ]; then
+ sed -i "s#$(grep 'currentmp02=' "${conf}")#currentmp02=${vidnamesarray[1]} ${vidlinksarray[1]}#" "${conf}"
+ fi
+ if [ -n "${vidnamesarray[2]}" ] && [ -n "${vidlinksarray[2]}" ]; then
+ sed -i "s#$(grep 'currentmp04=' "${conf}")#currentmp04=${vidnamesarray[2]} ${vidlinksarray[2]}#" "${conf}"
+ fi
+ if [ -n "${vidnamesarray[3]}" ] && [ -n "${vidlinksarray[3]}" ]; then
+ sed -i "s#$(grep 'currentmp05=' "${conf}")#currentmp05=${vidnamesarray[3]} ${vidlinksarray[3]}#" "${conf}"
+ fi
+ if [ -n "${vidnamesarray[4]}" ] && [ -n "${vidlinksarray[4]}" ]; then
+ sed -i "s#$(grep 'currentmp11=' "${conf}")#currentmp11=${vidnamesarray[4]} ${vidlinksarray[4]}#" "${conf}"
+ fi
+ if [ -n "${vidnamesarray[5]}" ] && [ -n "${vidlinksarray[5]}" ]; then
+ sed -i "s#$(grep 'currentmp12=' "${conf}")#currentmp12=${vidnamesarray[5]} ${vidlinksarray[5]}#" "${conf}"
+ fi
+ fi
+ if [ "${when}" == "-y" ]; then
+ if [ -n "${vidnamesarray[0]}" ] && [ -n "${vidlinksarray[0]}" ]; then
+ sed -i "s#$(grep 'currentyp01=' "${conf}")#currentyp01=${vidnamesarray[0]} ${vidlinksarray[0]}#" "${conf}"
+ fi
+ if [ -n "${vidnamesarray[1]}" ] && [ -n "${vidlinksarray[1]}" ]; then
+ sed -i "s#$(grep 'currentyp02=' "${conf}")#currentyp02=${vidnamesarray[1]} ${vidlinksarray[1]}#" "${conf}"
+ fi
+ if [ -n "${vidnamesarray[2]}" ] && [ -n "${vidlinksarray[2]}" ]; then
+ sed -i "s#$(grep 'currentyp04=' "${conf}")#currentyp04=${vidnamesarray[2]} ${vidlinksarray[2]}#" "${conf}"
+ fi
+ if [ -n "${vidnamesarray[3]}" ] && [ -n "${vidlinksarray[3]}" ]; then
+ sed -i "s#$(grep 'currentyp05=' "${conf}")#currentyp05=${vidnamesarray[3]} ${vidlinksarray[3]}#" "${conf}"
+ fi
+ if [ -n "${vidnamesarray[4]}" ] && [ -n "${vidlinksarray[4]}" ]; then
+ sed -i "s#$(grep 'currentyp11=' "${conf}")#currentyp11=${vidnamesarray[4]} ${vidlinksarray[4]}#" "${conf}"
+ fi
+ if [ -n "${vidnamesarray[5]}" ] && [ -n "${vidlinksarray[5]}" ]; then
+ sed -i "s#$(grep 'currentyp12=' "${conf}")#currentyp12=${vidnamesarray[5]} ${vidlinksarray[5]}#" "${conf}"
+ fi
+ fi
+}
+
+#
+# VARIABLES:
+#
+
+show=$1
+conf=$2
+if [ -z "${conf}" ] || [ "${conf}" == "-" ]; then
+ conf="$(dirname "$(realpath "$0")")/$(basename -s .sh "$0").conf"
+fi
+ever=$4
+if [ -z "${ever}" ] || [ "${ever}" == "-" ]; then
+ ever=1
+fi
+if grep -q -o "^[0-9][0-9]*$" <<< "${ever}"; then
+ :
+else
+ execerror "${ever} - wrong argument"
+fi
+when=$3
+if [ -z "${when}" ]; then
+ viddate=$(date +"%Y").$(date +"%m").$(date +"%d")
+fi
+if [ "${when}" == "-d" ]; then
+ d=$(date -d "-${ever} day" +"%d")
+ m=$(date +"%m")
+ if [ "$(date -d "-${ever} day" +'%m')" != "$(date +'%m')" ]; then
+ m=$(date -d "-${ever} day" +'%m')
+ fi
+ y=$(date +"%Y")
+ if [ "$(date -d "-${ever} day" +'%Y')" != "$(date +'%Y')" ]; then
+ y=$(date -d "-${ever} day" +'%Y')
+ fi
+ viddate="${y}.${m}.${d}"
+fi
+if [ "${when}" == "-w" ]; then
+ w=$(date -d "-${ever} week" +"%V")
+ y=$(date +"%Y")
+ if [ "$(date -d "-${ever} week" +'%Y')" != "$(date +'%Y')" ]; then
+ y=$(date -d "-${ever} week" +'%Y')
+ fi
+ viddate="${y}-w${w}"
+fi
+if [ "${when}" == "-m" ]; then
+ m=$(date -d "-${ever} month" +"%m")
+ y=$(date +"%Y")
+ if [ "$(date -d "-${ever} month" +'%Y')" != "$(date +'%Y')" ]; then
+ y=$(date -d "-${ever} month" +'%Y')
+ fi
+ viddate="${y}.${m}"
+fi
+if [ "${when}" == "-y" ]; then
+ y=$(date -d "-${ever} year" +"%Y")
+ viddate="${y}"
+fi
+vidname="${viddate}.mp4"
+only=$5
+
+time=$(date +%s)
+cd "$(dirname "$(realpath "$0")")" || execerror
+if [ ! -e "${conf}" ]; then
+ execerror "Not found config file: ${conf}"
+else
+ getconfig
+fi
+if [ -z "${logs}" ]; then
+ logs=/dev/null
+elif [ ! -e "${logs}" ]; then
+ touch "${logs}"
+fi
+if ! command -v curl &> /dev/null || \
+ ! command -v ffmpeg &> /dev/null || \
+ ! command -v xmllint &> /dev/null || \
+ ! command -v jq &> /dev/null; then
+ execerror "Not found dependencies"
+fi
+
+#
+# MAIN:
+#
+
+vidpathsarray=()
+vidlinksarray=()
+vidtgidsarray=()
+vidtgcpsarray=()
+for name in "${vidnamesarray[@]}"; do
+ vidmatch="${name}_${vidname}"
+ while read -r FILE; do
+ vidpathsarray+=("${FILE}")
+ done < <(find "${pathroot}" -name "*${vidmatch}" | sort)
+done
+for item in "${!vidpathsarray[@]}"; do
+ # WORDPRESS UPLOAD VIDEO
+ if [ ! "$only" == "--onlytg" ]; then
+ # This realisation isn't optimal, but it fixes a few issues with large files:
+ # variable=$(base64 $file) -> "xrealloc: cannot allocate"
+ # response=$(curl -X POST -d @${file}.xml $url) -> "curl: option -d: out of memory"
+ filetype="video/mp4"
+ echo '
+ wp.uploadFile
+
+ 1
+ '"${wpxmlrpcuser}"'
+ '"${wpxmlrpcpass}"'
+
+ name'"$(basename "${vidpathsarray[$item]}")"'
+ type'"${filetype}"'
+ bits' > "${vidpathsarray[$item]}.xml"
+ base64 --wrap=0 "${vidpathsarray[$item]}" >> "${vidpathsarray[$item]}.xml"
+ echo '
+
+
+ ' >> "${vidpathsarray[$item]}.xml"
+ response="${vidpathsarray[$item]}-response.xml"
+ if curl -X POST -T "${vidpathsarray[$item]}.xml" "${wpxmlrpclink}" > "${response}"; then
+ vidlinksarray+=("$(xmllint --xpath '//member[contains(name,"link")]/value/string/text()' "${response}")")
+ vidlinkcodeis=$(curl --output /dev/null --silent --write-out "%{http_code}" "${vidlinksarray[$item]}")
+ else
+ execerror "${response}"
+ fi
+ if [[ ${vidlinkcodeis} -eq 200 ]]; then
+ rm "${vidpathsarray[$item]}.xml"
+ rm "${response}"
+ echo "$(date +'%Y.%m.%d-%H:%M:%S') sent ${vidpathsarray[$item]} to ${vidlinksarray[$item]}" | tee -a "${logs}"
+ echo -e "WORDPRESS UPLOAD VIDEO RESULT:"
+ echo -e "item: ${item}"
+ echo -e "type: ${filetype}"
+ echo -e "name: ${vidnamesarray[$item]}"
+ echo -e "base: $(basename "${vidpathsarray[$item]}")"
+ echo -e "path: ${vidpathsarray[$item]}"
+ echo -e "link: ${vidlinksarray[$item]}"
+ echo -e "code: ${vidlinkcodeis}"
+ else
+ vidlinksarray[item]=''
+ fi
+ fi
+
+ # TELEGRAM SEND/UPLOAD VIDEO
+ if [ ! "${only}" == "--onlywp" ]; then
+ videofullpath=${vidpathsarray[$item]}
+ vidcompressed=${videofullpath//".mp4"/"-compressed.mp4"}
+ if [ -n "$(find "${videofullpath}" -prune -size +51380224c)" ]; then
+ duration=$(ffprobe -i "${videofullpath}" \
+ -show_entries format=duration -v quiet -of csv="p=0" | cut -d'.' -f 1)
+ if ffmpeg -i "${videofullpath}" \
+ -c:v libx264 -b:v "$((49*8*1000/duration))k" \
+ -vf "scale=960:540,fps=25,format=yuv420p" \
+ -preset veryslow "${vidcompressed}" -y -loglevel quiet -stats; then
+ videofullpath=${vidcompressed}
+ else
+ execerror "ffmpeg convert ${videofullpath} to ${vidcompressed}"
+ fi
+ fi
+ videobasename=$(basename "${videofullpath}")
+ videobasename=${videobasename//".mp4"/""}
+ response=$(curl -s \
+ -F "chat_id=${telegramchatid}" \
+ -F "video=@${videofullpath}" \
+ -F "caption=${videobasename}" \
+ "${telegramapiurl}/sendVideo")
+ if curl -s -F "chat_id=${telegramchatid}" \
+ -F "message_id=$(echo "${response}" | jq -r '.result.message_id')" \
+ "${telegramapiurl}/deleteMessage"; then
+ vidtgidsarray+=("$(echo "${response}" | jq -r '.result.video.file_id')")
+ vidtgcpsarray+=("$(echo "${response}" | jq -r '.result.caption')")
+ addtologs "sent ${videofullpath} to ${vidtgidsarray[$item]} Telegram file ID"
+ else
+ execerror "sent ${videofullpath} to ${telegramchatid} Telegram Chat ID"
+ fi
+ if [ -e "${vidcompressed}" ]; then
+ rm "${vidcompressed}"
+ fi
+ fi
+done
+
+# TELEGRAM SEND MEDIAGROUP
+if [ ! "${only}" == "--onlywp" ]; then
+ response=$(curl -s -F "chat_id=${telegramchatid}" \
+ -F media='[
+ {"type":"photo","media":"'"${tgpreviewlink}"'",
+ "caption":"period: '"${viddate}"'\nsource: '"${tgpreviewtext}"'\nstream: '"${youtubelink}"'"},
+ {"type":"video","media":"'"${vidtgidsarray[0]}"'"},
+ {"type":"video","media":"'"${vidtgidsarray[1]}"'"},
+ {"type":"video","media":"'"${vidtgidsarray[2]}"'"},
+ {"type":"video","media":"'"${vidtgidsarray[3]}"'"},
+ {"type":"video","media":"'"${vidtgidsarray[4]}"'"},
+ {"type":"video","media":"'"${vidtgidsarray[5]}"'"}]' \
+ -H "Content-Type:multipart/form-data" \
+ "${telegramapiurl}/sendMediaGroup")
+fi
+
+# WORDPRESS UPDATE PAGE
+if [ ! "${only}" == "--onlytg" ]; then
+ setconfig
+ getconfig
+ xml=$(cat "./${wptemplateis}")
+
+ xml=${xml//wpeditpageid/${wpeditpageid}}
+ xml=${xml//wpxmlrpcuser/${wpxmlrpcuser}}
+ xml=${xml//wpxmlrpcpass/${wpxmlrpcpass}}
+
+ xml=${xml//youtubelink/${youtubelink}}
+
+ xml=${xml//"currentdp01[0]"/${currentdp01[0]}}
+ xml=${xml//"currentdp01[1]"/${currentdp01[1]}}
+ xml=${xml//"currentdp02[0]"/${currentdp02[0]}}
+ xml=${xml//"currentdp02[1]"/${currentdp02[1]}}
+ xml=${xml//"currentdp04[0]"/${currentdp04[0]}}
+ xml=${xml//"currentdp04[1]"/${currentdp04[1]}}
+ xml=${xml//"currentdp05[0]"/${currentdp05[0]}}
+ xml=${xml//"currentdp05[1]"/${currentdp05[1]}}
+ xml=${xml//"currentdp11[0]"/${currentdp11[0]}}
+ xml=${xml//"currentdp11[1]"/${currentdp11[1]}}
+ xml=${xml//"currentdp12[0]"/${currentdp12[0]}}
+ xml=${xml//"currentdp12[1]"/${currentdp12[1]}}
+
+ xml=${xml//"currentwp01[0]"/${currentwp01[0]}}
+ xml=${xml//"currentwp01[1]"/${currentwp01[1]}}
+ xml=${xml//"currentwp02[0]"/${currentwp02[0]}}
+ xml=${xml//"currentwp02[1]"/${currentwp02[1]}}
+ xml=${xml//"currentwp04[0]"/${currentwp04[0]}}
+ xml=${xml//"currentwp04[1]"/${currentwp04[1]}}
+ xml=${xml//"currentwp05[0]"/${currentwp05[0]}}
+ xml=${xml//"currentwp05[1]"/${currentwp05[1]}}
+ xml=${xml//"currentwp11[0]"/${currentwp11[0]}}
+ xml=${xml//"currentwp11[1]"/${currentwp11[1]}}
+ xml=${xml//"currentwp12[0]"/${currentwp12[0]}}
+ xml=${xml//"currentwp12[1]"/${currentwp12[1]}}
+
+ xml=${xml//"currentmp01[0]"/${currentmp01[0]}}
+ xml=${xml//"currentmp01[1]"/${currentmp01[1]}}
+ xml=${xml//"currentmp02[0]"/${currentmp02[0]}}
+ xml=${xml//"currentmp02[1]"/${currentmp02[1]}}
+ xml=${xml//"currentmp04[0]"/${currentmp04[0]}}
+ xml=${xml//"currentmp04[1]"/${currentmp04[1]}}
+ xml=${xml//"currentmp05[0]"/${currentmp05[0]}}
+ xml=${xml//"currentmp05[1]"/${currentmp05[1]}}
+ xml=${xml//"currentmp11[0]"/${currentmp11[0]}}
+ xml=${xml//"currentmp11[1]"/${currentmp11[1]}}
+ xml=${xml//"currentmp12[0]"/${currentmp12[0]}}
+ xml=${xml//"currentmp12[1]"/${currentmp12[1]}}
+
+ xml=${xml//"currentyp01[0]"/${currentyp01[0]}}
+ xml=${xml//"currentyp01[1]"/${currentyp01[1]}}
+ xml=${xml//"currentyp02[0]"/${currentyp02[0]}}
+ xml=${xml//"currentyp02[1]"/${currentyp02[1]}}
+ xml=${xml//"currentyp04[0]"/${currentyp04[0]}}
+ xml=${xml//"currentyp04[1]"/${currentyp04[1]}}
+ xml=${xml//"currentyp05[0]"/${currentyp05[0]}}
+ xml=${xml//"currentyp05[1]"/${currentyp05[1]}}
+ xml=${xml//"currentyp11[0]"/${currentyp11[0]}}
+ xml=${xml//"currentyp11[1]"/${currentyp11[1]}}
+ xml=${xml//"currentyp12[0]"/${currentyp12[0]}}
+ xml=${xml//"currentyp12[1]"/${currentyp12[1]}}
+
+ xml=${xml//wpedituserid/${wpedituserid}}
+ xml=${xml//wppageauthor/${wppageauthor}}
+ xml=${xml//wppagelinkis/${wppagelinkis}}
+ xml=${xml//wpeditdateis/${wpeditdateis}}
+
+ response=$(curl -d "${xml}" "${wpxmlrpclink}")
+ if echo "${response}" | grep -q '1'; then
+ addtologs "update $(grep 'Link' "${xml}" | cut -d'>' -f 6 | cut -d'<' -f 1))"
+ else
+ echo "${response}" | xmllint --format - > "${pathroot}/wp-page${wpeditpageid}-response.xml" 2>/dev/null
+ execerror "${pathroot}/wp-page${wpeditpageid}-response.xml"
+ fi
+fi
+execquite
diff --git a/cctv-scheduler.conf b/cctv-scheduler.conf
index 702868f..743d0d3 100644
--- a/cctv-scheduler.conf
+++ b/cctv-scheduler.conf
@@ -28,6 +28,11 @@ camera.test.local = true
sensor.test.local = true
+[enable-convert]
+# List the convert block names. Only blocks with the TRUE value will be used.
+camera.test.local = true
+
+
[broadcast-config:camera.test.local]
# Broadcast parameter description block always starts with "broadcast-config:".
src = rtsp://user:pass@192.168.254.253:554/Streaming/Channels/101,http://radio.fm:8000/stream.mp3
@@ -108,5 +113,24 @@ step051 = setposashome, -, -, -, -, -, -,
step061 = settextonosd, 0, 0, -, -, -, -, 3, hello, 'x|y: osd text position, text: text for osd without quotes or commas'
step062 = settextonosd, 0, 0, -, -, -, -, 3, sensor-config:sensor.test.local, 'if a sensor configuration is specified, the sensor value is used instead of text'
step063 = settextonosd, 0, 0, -, -, -, -, 3, , 'an empty text value is used to clear the osd'
-step071 = downloadjpeg, 1280, 720, -, -, -, -, 5, , 'name: filename prefix, x|y: camera width|height resolution'
+step071 = downloadjpeg, 1920, 1080, -, -, -, -, 5, , 'name: filename prefix, x|y: camera width|height resolution'
step999 = rebootcamera, -, -, -, -, -, -, 120, ,
+
+
+[convert-config:camera.test.local]
+# Converter parameter description block always starts with "convert-config:".
+image_find_names = step071, image-01, image-02
+# If a image root or destination video directories on a remote host is used, a username and password must be specified.
+# Supported protocols:
+# FTP.
+image_root_path = ftp://192.168.254.254/Records/camera.test.local
+image_root_user = user
+image_root_pass = pass
+
+video_dest_path = ftp://192.168.254.254/Downloads
+video_dest_user = user
+video_dest_pass = pass
+
+video_scale_x = 1920
+video_scale_y = 1080
+video_framerate = 25
\ No newline at end of file
diff --git a/cctv-scheduler.py b/cctv-scheduler.py
index 5b20558..cd5df83 100644
--- a/cctv-scheduler.py
+++ b/cctv-scheduler.py
@@ -4,15 +4,17 @@
import calendar
import base64
+import datetime
import json
import logging
+from random import choice
import re
import urllib.request
from argparse import ArgumentParser
-from datetime import datetime
from ftplib import FTP
from multiprocessing import Process, Queue
-from os import environ, makedirs, path, remove, replace, sep, stat, walk
+from os import environ, makedirs, path, remove, replace, rmdir, sep, stat, walk
+from string import ascii_letters, digits
from subprocess import Popen, PIPE, STDOUT
from sys import platform
from time import sleep
@@ -389,6 +391,75 @@ class Connect:
)
return 'ERROR'
'''
+
+ @staticmethod
+ # pylint: disable=W0718
+ def ftp_file_search(
+ root_path: (str),
+ search_name: (str, type(None)) = None,
+ ftp: FTP = None,
+ hostname: str = None,
+ username: str = None,
+ password: str = None
+ ) -> list:
+ """Search files over FTP.
+
+ Args:
+ root_path (str): where to search.
+ search_name (str, None, optional): full or partial filename for the filter.
+ Defaults to None.
+ ftp (FTP, optional): FTP object generated by recursive search. Defaults to None.
+ hostname (str, optional): ftp hostname. Defaults to None.
+ username (str, optional): ftp username. Defaults to None.
+ password (str, optional): ftp password. Defaults to None.
+
+ Returns:
+ list: list of found files.
+ """
+ parent = False
+ if not ftp:
+ try:
+ ftp = FTP(host=hostname)
+ ftp.login(user=username, passwd=password)
+ parent = True
+ except Exception:
+ pass
+
+ result = []
+ ftp.cwd(root_path)
+ for file in ftp.mlsd():
+ if file[1]['type'] == 'dir':
+ result = result + Connect.ftp_file_search(
+ root_path=root_path + "/" + file[0],
+ search_name=search_name,
+ ftp=ftp
+ )
+ elif file[1]['type'] == 'file':
+ if search_name:
+ if search_name in file[0]:
+ result.append(root_path + "/" + file[0])
+ else:
+ result.append(root_path + "/" + file[0])
+
+ if parent:
+ ftp.close()
+ result.sort()
+ return result
+
+ @staticmethod
+ # pylint: disable=W0718,C0116
+ def ftp_get_file(src_file: str, dst_file: str, hostname: str, username: str, password: str):
+ ftp = FTP(host=hostname)
+ try:
+ ftp.login(user=username, passwd=password)
+ with open(dst_file, "wb+") as file:
+ ftp.retrbinary(f"RETR {src_file}", file.write)
+ ftp.quit()
+ return True
+ except Exception as error:
+ logging.debug(msg='\n' + 'error: ' + str(error))
+ return False
+
@staticmethod
# pylint: disable=W0718,C0116
def ftp_put_file(
@@ -411,30 +482,13 @@ class Connect:
except Exception:
ftp.mkd(path_item)
ftp.cwd(path_item)
- except Exception:
- pass
-
- with open(src_file, "rb") as file:
- ftp.storbinary(f"STOR {dst_file}", file)
- ftp.quit()
- return True
- '''
- @staticmethod
- def ftp_get_file(src_file: str, dst_file: str, hostname: str, username: str, password: str):
- ftp = FTP(host=hostname)
- try:
- ftp.login(user=username, passwd=password)
- with open(dst_file, "wb") as file:
- ftp.retrbinary(f"RETR {src_file}", file.write)
+ with open(src_file, "rb") as file:
+ ftp.storbinary(f"STOR {dst_file}", file)
ftp.quit()
- except Exception:
- pass
- '''
- '''
- @staticmethod
- def xmlrpc():
- pass
- '''
+ return True
+ except Exception as error:
+ logging.debug(msg='\n' + 'error: ' + str(error))
+ return False
class HikISAPI(Connect):
@@ -1734,13 +1788,13 @@ class Sequence:
if records_root_temp != path.dirname(path.realpath(__file__)):
records_root_temp = path.dirname(path.realpath(__file__)) + sep + 'temp'
makedirs(records_root_temp, exist_ok=True)
- dy = datetime.now().strftime('%Y')
- dm = datetime.now().strftime('%m')
- dv = datetime.now().strftime('%V')
- dd = datetime.now().strftime('%d')
- th = datetime.now().strftime('%H')
- tm = datetime.now().strftime('%M')
- ts = datetime.now().strftime('%S')
+ dy = datetime.datetime.now().strftime('%Y')
+ dm = datetime.datetime.now().strftime('%m')
+ dv = datetime.datetime.now().strftime('%V')
+ dd = datetime.datetime.now().strftime('%d')
+ th = datetime.datetime.now().strftime('%H')
+ tm = datetime.datetime.now().strftime('%M')
+ ts = datetime.datetime.now().strftime('%S')
records_file_name = (
key + '_' + dy + '-' + dm + '-' + dd + '_' + th + '.' + tm + '.' + ts + '.jpeg'
)
@@ -1838,6 +1892,164 @@ class Sequence:
logging.warning(msg='result:' + key + ' = ERROR')
+class Convert:
+ """Convert handling.
+ """
+ @staticmethod
+ # pylint: disable=W0612
+ def run(
+ image_root_path: (str, list),
+ image_find_names: list,
+ video_dest_path: str,
+ video_dest_sufx: str,
+ video_scale_x: int,
+ video_scale_y: int,
+ video_framerate: int,
+ video_duration: int,
+ temp_path: str,
+ image_root_user: (str, type(None)) = None,
+ image_root_pass: (str, type(None)) = None,
+ video_dest_user: (str, type(None)) = None,
+ video_dest_pass: (str, type(None)) = None
+ ) -> None:
+ """Converting executor.
+
+ Args:
+ image_root_path (str, list): source images path.
+ image_find_names (list): image names to search.
+ video_dest_path (str): path to destination video.
+ video_dest_sufx (str): destination video name suffix.
+ video_scale_x (int): destination video width.
+ video_scale_y (int): destination video height.
+ video_framerate (int): destination video frame per second.
+ video_duration (int): destination video duration.
+ temp_path (str): path to directory for temp files.
+ image_root_user (str, type, optional): username to source images ftp,sftp,smb path.
+ Defaults to None.
+ image_root_pass (str, type, optional): password to source images ftp,sftp,smb path.
+ Defaults to None.
+ video_dest_user (str, type, optional): username to destination video ftp,sftp,smb path.
+ Defaults to None.
+ video_dest_pass (str, type, optional): password to destination video ftp,sftp,smb path.
+ Defaults to None.
+ """
+ if isinstance(image_root_path, str):
+ image_root_path = [image_root_path]
+
+ temp_path = temp_path + sep + Do.random_string(8)
+ temp_files = []
+ for name in image_find_names:
+ image_found = []
+ for image_root in image_root_path:
+ if '://' in image_root:
+ image_hostname = image_root.split('/')[2]
+ image_hosttype = image_root.split('://')[0]
+ if image_hosttype == 'ftp':
+ image_hostport = 21
+ if image_hosttype == 'sftp':
+ image_hostport = 22
+ if image_hosttype == 'smb':
+ image_hostport = 445
+ image_hostpath = image_root.replace(image_hosttype + '://' + image_hostname, '')
+ if '@' in image_hostname:
+ image_root_user = image_hostname.split('@')[0].split(':')[0]
+ image_root_pass = image_hostname.split('@')[0].split(':')[1]
+ image_hostname = image_hostname.split('@')[1]
+ if ':' in image_hostname:
+ image_hostport = int(image_hostname.split(':')[1])
+ image_hostname = image_hostname.split(':')[0]
+ if image_hosttype == 'ftp':
+ image_found = image_found + Connect.ftp_file_search(
+ root_path=image_hostpath,
+ search_name=name,
+ hostname=image_hostname,
+ username=image_root_user,
+ password=image_root_pass
+ )
+ makedirs(temp_path, exist_ok=True)
+ for image in image_found:
+ if Connect.ftp_get_file(
+ src_file=image,
+ dst_file=temp_path + sep + path.basename(image),
+ hostname=image_hostname,
+ username=image_root_user,
+ password=image_root_pass
+ ):
+ temp_files.append(temp_path + sep + path.basename(image))
+ elif image_hosttype == 'sftp':
+ pass
+ else:
+ pass
+
+ image_temp_list = temp_path + sep + 'convert.list'
+ image_amount = 0
+ with open(image_temp_list, mode='w+', encoding='UTF-8') as converter_list:
+ for file in Do.file_search(root_path=temp_path, search_name=name):
+ converter_list.write("\nfile '" + file + "'")
+ image_amount += 1
+ temp_files.append(image_temp_list)
+
+ video_converted = name + '_' + video_dest_sufx + '.mp4'
+ video_temp_path = temp_path + sep + video_converted
+ video_conv_conf = (''
+ + '-r '
+ + str(image_amount) + '/' + str(video_duration)
+ + ' -f concat -safe 0 -i '
+ + image_temp_list
+ + ' -c:v libx264 -vf scale=' + str(video_scale_x) + ':' + str(video_scale_y)
+ + ',fps=' + str(video_framerate) + ',format=yuv420p '
+ + video_temp_path
+ + ' -y'
+ )
+
+ if FFmpeg.run(raw=video_conv_conf) == 0:
+ temp_files.append(video_temp_path)
+
+ if '://' in video_dest_path:
+ video_hostname = video_dest_path.split('/')[2]
+ video_hosttype = video_dest_path.split('://')[0]
+ if video_hosttype == 'ftp':
+ video_hostport = 21
+ if video_hosttype == 'sftp':
+ video_hostport = 22
+ if video_hosttype == 'smb':
+ video_hostport = 445
+ video_hostpath = video_dest_path.replace(
+ video_hosttype + '://' + video_hostname,
+ ''
+ )
+ if '@' in image_hostname:
+ video_dest_user = video_hostname.split('@')[0].split(':')[0]
+ video_dest_pass = video_hostname.split('@')[0].split(':')[1]
+ video_hostname = video_hostname.split('@')[1]
+ if ':' in video_hostname:
+ video_hostport = int(video_hostname.split(':')[1])
+ video_hostname = video_hostname.split(':')[0]
+ if video_hosttype == 'ftp':
+ if Connect.ftp_put_file(
+ src_file=video_temp_path,
+ dst_file=video_hostpath + '/' + video_converted,
+ hostname=video_hostname,
+ username=video_dest_user,
+ password=video_dest_pass
+ ):
+ pass
+ elif image_hosttype == 'sftp':
+ pass
+ else:
+ pass
+
+ for temp_file in temp_files:
+ try:
+ remove(temp_file)
+ except OSError as error:
+ logging.debug(msg='\n' + 'error: ' + str(error))
+ try:
+ rmdir(temp_path)
+ except OSError as error:
+ logging.debug(msg='\n' + 'error: ' + str(error))
+
+
class Proc:
"""Find a running process from Python.
"""
@@ -2033,13 +2245,13 @@ class FFmpeg:
target=cls._watchdog,
args=(proc.pid, watchsec, que,),
daemon=True
- ).start()
+ ).start()
for line in proc.stdout:
if not que:
- print(line, flush=True)
+ logging.debug(msg=line)
else:
que.put(line)
- return proc.returncode
+ return proc.returncode
@classmethod
def _bin(cls, ffpath: str, tool: str = 'ffmpeg') -> str:
@@ -2346,6 +2558,18 @@ class Do():
}
return date
+ @staticmethod
+ def random_string(length: int) -> str:
+ """Generate string from lowercase letters, uppercase letters, digits.
+
+ Args:
+ length (int): string lenght.
+
+ Returns:
+ str: random string.
+ """
+ return ''.join(choice(ascii_letters + digits) for i in range(length))
+
@staticmethod
# pylint: disable=W0612
def file_search(root_path: str, search_name: (str, type(None)) = None) -> list:
@@ -2368,6 +2592,7 @@ class Do():
found_list.append(path.join(path.realpath(root), file))
else:
found_list.append(path.join(path.realpath(root), file))
+ found_list.sort()
return found_list
@staticmethod
@@ -2774,7 +2999,7 @@ class Do():
if __name__ == "__main__":
- time_start = datetime.now()
+ time_start = datetime.datetime.now()
args = ArgumentParser(
prog='cctv-scheduler',
@@ -2782,7 +3007,7 @@ if __name__ == "__main__":
epilog='Dependencies: '
'- Python 3 (tested version 3.9.5), '
'- Python 3 modules: paramiko '
- )
+ )
args.add_argument('--config', type=str, default=path.splitext(__file__)[0] + '.conf',
required=False,
help='custom configuration file path')
@@ -2794,10 +3019,27 @@ if __name__ == "__main__":
help='convert JPEG collection to MP4')
args.add_argument('-p', '--publisher', action='store_true', required=False,
help='publish content from templates')
+ args.add_argument('-d', '--day', type=int, default=0, required=False,
+ help=('day in amount of days from today, '
+ + 'for which the publication or conversion is made')
+ )
+ args.add_argument('-w', '--week', type=int, default=0, required=False,
+ help=('week in amount of weeks from today, '
+ + 'for which the publication or conversion is made')
+ )
+ args.add_argument('-m', '--month', type=int, default=0, required=False,
+ help=('month in amount of months from today, '
+ + 'for which the publication or conversion is made')
+ )
+ args.add_argument('-y', '--year', type=int, default=0, required=False,
+ help=('year in amount of years from today, '
+ + 'for which the publication or conversion is made')
+ )
args = vars(args.parse_args())
log_root = path.dirname(path.realpath(__file__))
log_level = 'INFO'
+ temp_path = path.dirname(path.realpath(__file__)) + sep + 'temp'
if path.exists(args['config']):
conf = Parse(parameters=args['config'], block='common')
if 'log_root' in conf.data:
@@ -2824,9 +3066,12 @@ if __name__ == "__main__":
logging.StreamHandler()
],
level=log_level
- )
+ )
logging.getLogger("paramiko").setLevel(logging.WARNING)
+ if 'temp_path' in conf.data:
+ temp_path = conf.data['temp_path']
+
if args['broadcast']:
logging.info(msg='Starting streaming media to destination')
broadcasts = {}
@@ -2924,11 +3169,140 @@ if __name__ == "__main__":
records_root_pass=records_root_pass
)
elif args['converter']:
- logging.info(msg='Starting convert JPEG collection to MP4')
+ if args['day'] or args['week'] or args['month'] or args['year']:
+ convert_period = None
+ convert_amount = None
+ video_duration = None
+ if args['day']:
+ convert_period = 'day'
+ convert_amount = args['day']
+ if args['week']:
+ convert_period = 'week'
+ convert_amount = args['week']
+ if args['month']:
+ convert_period = 'month'
+ convert_amount = args['month']
+ if args['year']:
+ convert_period = 'year'
+ convert_amount = args['year']
+ convert_date = Do.date_calc(period=convert_period, amount=convert_amount)
+
+ logging.info(msg='Starting convert JPEG collection to MP4')
+ conf = Parse(parameters=args['config'], block='enable-convert')
+ for key, value in conf.data.items():
+ if value == 'true':
+ convert_config = Parse(
+ parameters=args['config'],
+ block='convert-config:' + key
+ ).data
+ image_root_path = path.dirname(path.realpath(__file__))
+ image_root_user = None
+ image_root_pass = None
+ image_find_names = None
+ if 'image_root_path' in convert_config:
+ image_root_path = convert_config['image_root_path']
+ if 'image_root_user' in convert_config:
+ image_root_user = convert_config['image_root_user']
+ if 'image_root_pass' in convert_config:
+ image_root_pass = convert_config['image_root_pass']
+ image_find_names = []
+ for name in convert_config['image_find_names'].split(','):
+ image_find_names.append(name.strip())
+
+ if convert_date['period'] == 'day':
+ image_root_path = (''
+ + image_root_path + '/'
+ + str(convert_date['start']['y']) + '/'
+ + str(convert_date['start']['m']).zfill(2) + '/'
+ + str(convert_date['start']['w']).zfill(2) + '/'
+ + str(convert_date['start']['d']).zfill(2)
+ )
+ video_dest_sufx = (''
+ + str(convert_date['start']['y']) + '.'
+ + str(convert_date['start']['m']).zfill(2) + '.'
+ + str(convert_date['start']['d']).zfill(2)
+ )
+ video_duration = 1
+ if convert_date['period'] == 'week':
+ if convert_date['start']['m'] == convert_date['end']['m']:
+ image_root_path = (''
+ + image_root_path + '/'
+ + str(convert_date['start']['y']) + '/'
+ + str(convert_date['start']['m']).zfill(2) + '/'
+ + str(convert_date['start']['w']).zfill(2)
+ )
+ else:
+ image_root_path = [
+ (''
+ + image_root_path + '/'
+ + str(convert_date['start']['y']) + '/'
+ + str(convert_date['start']['m']).zfill(2) + '/'
+ + str(convert_date['start']['w']).zfill(2)
+ ),
+ (''
+ + image_root_path + '/'
+ + str(convert_date['end']['y']) + '/'
+ + str(convert_date['end']['m']).zfill(2) + '/'
+ + str(convert_date['end']['w']).zfill(2)
+ )
+ ]
+ video_dest_sufx = (''
+ + str(convert_date['start']['y']) + '-w'
+ + str(convert_date['start']['w']).zfill(2)
+ )
+ video_duration = 7
+ if convert_date['period'] == 'month':
+ image_root_path = (''
+ + image_root_path + '/'
+ + str(convert_date['start']['y']) + '/'
+ + str(convert_date['start']['m']).zfill(2)
+ )
+ video_dest_sufx = (''
+ + str(convert_date['start']['y']) + '.'
+ + str(convert_date['start']['m']).zfill(2)
+ )
+ video_duration = 30
+ if convert_date['period'] == 'year':
+ image_root_path = (''
+ + image_root_path + '/'
+ + str(convert_date['start']['y'])
+ )
+ video_dest_sufx = (''
+ + str(convert_date['start']['y'])
+ )
+ video_duration = 360
+
+ video_dest_path = path.dirname(path.realpath(__file__))
+ video_dest_user = None
+ video_dest_pass = None
+ if 'video_dest_path' in convert_config:
+ video_dest_path = convert_config['video_dest_path']
+ if 'video_dest_user' in convert_config:
+ video_dest_user = convert_config['video_dest_user']
+ if 'video_dest_pass' in convert_config:
+ video_dest_pass = convert_config['video_dest_pass']
+
+ Convert.run(
+ image_root_path=image_root_path,
+ image_root_user=image_root_user,
+ image_root_pass=image_root_pass,
+ image_find_names=image_find_names,
+ video_dest_path=video_dest_path,
+ video_dest_sufx=video_dest_sufx,
+ video_dest_user=video_dest_user,
+ video_dest_pass=video_dest_pass,
+ video_scale_x=int(convert_config['video_scale_x']),
+ video_scale_y=int(convert_config['video_scale_y']),
+ video_framerate=int(convert_config['video_framerate']),
+ video_duration=video_duration,
+ temp_path=temp_path
+ )
+ else:
+ logging.info(msg='Period was not selected. Exit.')
elif args['publisher']:
logging.info(msg='Starting publish content from templates')
else:
logging.info(msg='Start arguments was not selected. Exit.')
- time_execute = datetime.now() - time_start
+ time_execute = datetime.datetime.now() - time_start
logging.info(msg='execution time is ' + str(time_execute) + '. Exit.')
diff --git a/info/images/cctv-scheduler-0.4.png b/info/images/cctv-scheduler-0.4.png
new file mode 100755
index 0000000..341f89d
Binary files /dev/null and b/info/images/cctv-scheduler-0.4.png differ