Merge branch 'main' into feature/add-confidencepct-variable-to-apprise-notification
This commit is contained in:
+26
@@ -1,3 +1,29 @@
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
.idea
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
*.pyc
|
||||
*.flac
|
||||
.vscode
|
||||
|
||||
@@ -14,23 +14,10 @@ Icon made by <a href="https://www.freepik.com" title="Freepik">Freepik</a> from
|
||||
</p>
|
||||
|
||||
## Introduction
|
||||
BirdNET-Pi is built on the (deprecated) [TFLite version of BirdNET](https://github.com/kahst/BirdNET-Lite) by [**@kahst**](https://github.com/kahst) <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/"><img src="https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg"></a> using [pre-built TFLite binaries](https://github.com/PINTO0309/TensorflowLite-bin) by [**@PINTO0309**](https://github.com/PINTO0309) . It is able to recognize bird sounds from a USB microphone or sound card in realtime and share its data with the rest of the world.
|
||||
BirdNET-Pi is built on the [BirdNET framework](https://github.com/kahst/BirdNET-Analyzer) by [**@kahst**](https://github.com/kahst) <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/"><img src="https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg"></a> using [pre-built TFLite binaries](https://github.com/PINTO0309/TensorflowLite-bin) by [**@PINTO0309**](https://github.com/PINTO0309) . It is able to recognize bird sounds from a USB microphone or sound card in realtime and share its data with the rest of the world.
|
||||
|
||||
Check out birds from around the world
|
||||
- [BirdWeather](https://app.birdweather.com)<br>
|
||||
- [Johannesburg, South Africa](https://joburg.birdnetpi.com)<br>
|
||||
- [Öringe, Tyresö, Sweden](https://tyreso.birdnetpi.com)<br>
|
||||
- [Berowra, New South Wales, Australia](https://berowra.birdnetpi.com)
|
||||
- [Dundas, Ontario, Canada](https://dundasontario.birdnetpi.com)
|
||||
- [Bungendore, New South Wales, Australia](https://bungendorensw.birdnetpi.com)
|
||||
- [Rivers Bend, Ohio, United States](https://riversbendoh.birdnetpi.com)
|
||||
- [Vienna, Virginia, United States](https://viennava.birdnetpi.com)
|
||||
- [Grevenbroich, Elsen, Germany](https://grevenbroich-elsen.birdnetpi.com)
|
||||
- [Occoquan, Virginia, United States](https://occoquanva.birdnetpi.com)
|
||||
- [Cambridge, Massachusetts, United States](https://cambridgema.birdnetpi.com)
|
||||
|
||||
[Share your installation!!](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Sharing-Your-BirdNET-Pi)
|
||||
Have a public installation not in the list above? Let me know!! I'd be happy to add it.
|
||||
|
||||
Currently listening in these countries . . . that I know of . . .
|
||||
- The United States
|
||||
@@ -62,13 +49,19 @@ Currently listening in these countries . . . that I know of . . .
|
||||
- South Sudan
|
||||
- Argentina
|
||||
- Brazil
|
||||
- Thailand
|
||||
- Colombia
|
||||
- Estonia
|
||||
- Tasmania
|
||||
- Luxembourgh
|
||||
- Crete
|
||||
|
||||
## Features
|
||||
* 24/7 recording and automatic identification of bird songs using BirdNET machine learning
|
||||
* Bird sounds are automatically extracted and catalogued
|
||||
* Visualize your recorded bird data and analyze trends
|
||||
* Live audio stream & spectrogram
|
||||
* Automatic disk space management, where old audio files are periodically purged
|
||||
* **24/7 recording and automatic identification** of bird songs, chirps, and peeps using BirdNET machine learning
|
||||
* **Automatic extraction and cataloguing** of bird clips from full-length recordings
|
||||
* **Tools to visualize your recorded bird data** and analyze trends
|
||||
* **Live audio stream and spectrogram**
|
||||
* **Automatic disk space management** that periodically purges old audio files
|
||||
* [BirdWeather](https://app.birdweather.com) integration -- you can request a BirdWeather ID from BirdNET-Pi's "Tools" > "Settings" page
|
||||
* Web interface access to all data and logs provided by [Caddy](https://caddyserver.com)
|
||||
* [GoTTY](https://github.com/yudai/gotty) Web Terminal
|
||||
@@ -101,7 +94,7 @@ The installer takes care of any and all necessary updates, so you can run that a
|
||||
The installation creates a log in `$HOME/installation-$(date "+%F").txt`.
|
||||
## Access
|
||||
The BirdNET-Pi can be accessed from any web browser on the same network:
|
||||
- http://birdnetpi.local
|
||||
- http://birdnetpi.local OR your Pi's IP address
|
||||
- Default Basic Authentication Username: birdnet
|
||||
- Password is empty by default. Set this in "Tools" > "Settings" > "Advanced Settings"
|
||||
|
||||
|
||||
+15
-1
@@ -71,12 +71,14 @@ RTSP_STREAM=
|
||||
#----------------------- Apprise Miscellanous Configuration -------------------#
|
||||
|
||||
APPRISE_NOTIFICATION_TITLE="New BirdNET-Pi Detection"
|
||||
APPRISE_NOTIFICATION_BODY="A \$sciname \$comname was just detected with a confidence of \$confidence"
|
||||
APPRISE_NOTIFICATION_BODY="A \$comname (\$sciname) was just detected with a confidence of \$confidence"
|
||||
APPRISE_NOTIFY_EACH_DETECTION=0
|
||||
APPRISE_NOTIFY_NEW_SPECIES=0
|
||||
APPRISE_WEEKLY_REPORT=1
|
||||
APPRISE_NOTIFY_NEW_SPECIES_EACH_DAY=0
|
||||
APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES=0
|
||||
APPRISE_ONLY_NOTIFY_SPECIES_NAMES=""
|
||||
APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2=""
|
||||
|
||||
#---------------------- Flickr Images API Configuration -----------------------#
|
||||
## If FLICKR_API_KEY is set, the web interface will try and display bird images
|
||||
@@ -195,12 +197,24 @@ DATABASE_LANG=en
|
||||
|
||||
SILENCE_UPDATE_INDICATOR=0
|
||||
|
||||
## RAW_SPECTROGRAM is for removing the axes and labels of the spectrograms
|
||||
## that are generated by Sox for each detection for a cleaner appearance.
|
||||
|
||||
RAW_SPECTROGRAM=0
|
||||
|
||||
## HEARTBEAT_URL is a location to ping every time some analysis is done
|
||||
## no information is sent to the the URL, it is a heart beat to show that the
|
||||
## analysis is continuing
|
||||
|
||||
HEARTBEAT_URL=
|
||||
|
||||
## CUSTOM_IMAGE and CUSTOM_IMAGE_TITLE allow you to show a custom image on the
|
||||
## Overview page of your BirdNET-Pi. This can be used to show a dynamically
|
||||
## updating picture of your garden, for example.
|
||||
|
||||
CUSTOM_IMAGE=
|
||||
CUSTOM_IMAGE_TITLE=""
|
||||
|
||||
## These are just for debugging
|
||||
LAST_RUN=
|
||||
THIS_RUN=
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg class="img-fluid" id="outputsvg" xmlns="http://www.w3.org/2000/svg" style="transform: matrix(1.3, 0, 0, 1.3, 0, 0); transform-origin: 50% 50%; cursor: move; transition: transform 200ms ease-in-out 0s;" width="120" height="120" viewBox="0 0 1200 1200"><g id="led6HDXxLpKB3rJBInqSAJ" fill="rgb(253,174,179)" style="transform: none; transform-origin: 50% 50%;"><g style="transform: none;"><path id="ppWkGCoaW" d="M143 1036 l-28 -24 -6 -169 -7 -168 52 -49 c28 -26 100 -90 159 -142 100 -87 111 -94 151 -94 39 0 50 6 127 74 72 64 86 72 101 62 9 -7 91 -78 182 -159 129 -115 171 -147 193 -147 l28 0 -5 396 -5 396 -28 24 -28 24 -429 0 -429 0 -28 -24z"></path></g></g><g id="l1PReg5IXY9KUxbReAvzebZ" fill="rgb(239,68,82)" style="transform: none; transform-origin: 50% 50%;"><g style="transform: none;"><path id="pRauFKI19" d="M100 649 l0 -73 48 -45 c137 -132 293 -261 314 -261 13 0 37 12 54 28 117 102 143 122 161 122 19 0 37 -15 241 -193 109 -94 139 -106 167 -64 8 13 13 35 11 48 -3 13 -5 27 -5 32 -1 4 -6 7 -11 7 -6 0 -84 65 -173 144 -210 187 -213 189 -240 182 -12 -3 -60 -40 -106 -81 -46 -41 -90 -75 -97 -75 -12 0 -87 62 -255 210 -119 106 -109 104 -109 19z"></path></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
+27
-2
@@ -1,4 +1,29 @@
|
||||
<?php
|
||||
|
||||
/* Prevent XSS input */
|
||||
$_GET = filter_input_array(INPUT_GET, FILTER_SANITIZE_STRING);
|
||||
$_POST = filter_input_array(INPUT_POST, FILTER_SANITIZE_STRING);
|
||||
|
||||
$sys_timezone = "";
|
||||
// If we can get the timezome from the systems timezone file ust that
|
||||
if (file_exists('/etc/timezone')) {
|
||||
$tz_data = file_get_contents('/etc/timezone');
|
||||
if ($tz_data !== false) {
|
||||
$sys_timezone = trim($tz_data);
|
||||
}
|
||||
} else {
|
||||
// Else get timezone from the timedatectl command
|
||||
$tz_data = shell_exec('timedatectl show');
|
||||
$tz_data_array = parse_ini_string($tz_data);
|
||||
if (is_array($tz_data_array) && array_key_exists('Timezone', $tz_data_array)) {
|
||||
$sys_timezone = $tz_data_array['Timezone'];
|
||||
}
|
||||
}
|
||||
//Finally if we have a valod timezone, set it as the one PHP uses
|
||||
if ($sys_timezone !== "") {
|
||||
date_default_timezone_set($sys_timezone);
|
||||
}
|
||||
|
||||
if (file_exists('./scripts/thisrun.txt')) {
|
||||
$config = parse_ini_file('./scripts/thisrun.txt');
|
||||
} elseif (file_exists('./scripts/firstrun.ini')) {
|
||||
@@ -17,7 +42,7 @@ body::-webkit-scrollbar {
|
||||
display:none
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="style.css?v=1.20.23">
|
||||
<link rel="stylesheet" href="style.css?v=<?php echo date ('n.d.y', filemtime('style.css')); ?>">
|
||||
<link rel="stylesheet" type="text/css" href="static/dialog-polyfill.css" />
|
||||
<body>
|
||||
<div class="banner">
|
||||
@@ -79,4 +104,4 @@ echo "
|
||||
<iframe src=\"/views.php\"></iframe>
|
||||
</div>";
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
var pluginTrendlineLinear={id:"trendlineLinear",afterDraw:function(a){var t,s;for(var e in a.scales)if("x"==e[0]?s=a.scales[e]:t=a.scales[e],s&&t)break;var n=a.chart.ctx;a.data.datasets.forEach(function(t,e){if(t.trendlineLinear&&a.isDatasetVisible(e)&&0!=t.data.length){var i=a.getDatasetMeta(e);addFitter(i,n,t,s,a.scales[i.yAxisID])}}),n.setLineDash([])}};function addFitter(t,e,i,a,s){var n=i.trendlineLinear.style||i.borderColor,r=i.trendlineLinear.width||i.borderWidth,o=i.trendlineLinear.lineStyle||"solid";n=void 0!==n?n:"rgba(169,169,169, .6)",r=void 0!==r?r:3;var l=new LineFitter,d=i.data.length-1,h=t.data[0]._model.x,u=t.data[d]._model.x,m=!1;i.data&&"object"==typeof i.data[0]&&(m=!0),i.data.forEach(function(t,e){if(null!=t)if("time"===a.options.type){var i=null!=t.x?t.x:t.t;l.add(new Date(i).getTime(),t.y)}else m?l.add(t.x,t.y):l.add(e,t)});var x=a.getPixelForValue(l.minx),c=a.getPixelForValue(l.maxx),f=s.getPixelForValue(l.f(l.minx)),g=s.getPixelForValue(l.f(l.maxx));m||(x=h,c=u);var p=t.controller.chart.chartArea.bottom,v=t.controller.chart.width;if(p<f){var X=f-p,L=f-g;f=p,x+=v*(X/L)}else if(p<g){X=g-p,L=g-f;g=p,c=v-(c-(v-v*(X/L)))}e.lineWidth=r,"dotted"===o&&e.setLineDash([2,3]),e.beginPath(),e.moveTo(x,f),e.lineTo(c,g),e.strokeStyle=n,e.stroke()}function LineFitter(){this.count=0,this.sumX=0,this.sumX2=0,this.sumXY=0,this.sumY=0,this.minx=1e100,this.maxx=-1e100}LineFitter.prototype={add:function(t,e){t=parseFloat(t),e=parseFloat(e),this.count++,this.sumX+=t,this.sumX2+=t*t,this.sumXY+=t*e,this.sumY+=e,t<this.minx&&(this.minx=t),t>this.maxx&&(this.maxx=t)},f:function(t){t=parseFloat(t);var e=this.count*this.sumX2-this.sumX*this.sumX;return(this.sumX2*this.sumY-this.sumX*this.sumXY)/e+t*((this.count*this.sumXY-this.sumX*this.sumY)/e)}},"undefined"!=typeof window&&window.Chart&&window.Chart.plugins.register(pluginTrendlineLinear);try{module.exports=exports=pluginTrendlineLinear}catch(t){}
|
||||
+23
-3
@@ -26,7 +26,7 @@ iframe {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
height: 93%;
|
||||
height: 85%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -409,8 +409,8 @@ button:hover {
|
||||
}
|
||||
|
||||
#body::-webkit-scrollbar {
|
||||
# display:none
|
||||
#}
|
||||
/*display:none*/
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1290px) {
|
||||
.column1,.column2,.column3,.column4 {
|
||||
@@ -563,6 +563,11 @@ button:hover {
|
||||
height:25px !important;
|
||||
}
|
||||
|
||||
.copyimage-mobile {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position:relative;
|
||||
}
|
||||
@@ -814,6 +819,21 @@ pre#timer.bash {
|
||||
background-color:#9fe29b
|
||||
}
|
||||
|
||||
#newrtspstream{
|
||||
cursor: pointer;
|
||||
margin-left: 2px;
|
||||
height: 5px;
|
||||
line-height: 5px;
|
||||
padding: 3px;
|
||||
background-color: #9fe29b;
|
||||
}
|
||||
|
||||
.exclude_species_list_option_highlight {
|
||||
color: black;
|
||||
background-color: rgb(119, 196, 135);
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
#ddnewline::before {
|
||||
content: none;
|
||||
}
|
||||
+115
-12
@@ -1,4 +1,29 @@
|
||||
<?php
|
||||
|
||||
/* Prevent XSS input */
|
||||
$_GET = filter_input_array(INPUT_GET, FILTER_SANITIZE_STRING);
|
||||
$_POST = filter_input_array(INPUT_POST, FILTER_SANITIZE_STRING);
|
||||
|
||||
$sys_timezone = "";
|
||||
// If we can get the timezome from the systems timezone file ust that
|
||||
if (file_exists('/etc/timezone')) {
|
||||
$tz_data = file_get_contents('/etc/timezone');
|
||||
if ($tz_data !== false) {
|
||||
$sys_timezone = trim($tz_data);
|
||||
}
|
||||
} else {
|
||||
// else get timezone from the timedatectl command
|
||||
$tz_data = shell_exec('timedatectl show');
|
||||
$tz_data_array = parse_ini_string($tz_data);
|
||||
if (is_array($tz_data_array) && array_key_exists('Timezone', $tz_data_array)) {
|
||||
$sys_timezone = $tz_data_array['Timezone'];
|
||||
}
|
||||
}
|
||||
// finally if we have a valod timezone, set it as the one PHP uses
|
||||
if ($sys_timezone !== "") {
|
||||
date_default_timezone_set($sys_timezone);
|
||||
}
|
||||
|
||||
session_start();
|
||||
$user = shell_exec("awk -F: '/1000/{print $1}' /etc/passwd");
|
||||
$user = trim($user);
|
||||
@@ -11,21 +36,38 @@ if(!isset($_SESSION['behind'])) {
|
||||
$num_commits_behind = $matches[1];
|
||||
$_SESSION['behind'] = $num_commits_behind;
|
||||
}
|
||||
if(isset($_SESSION['behind'])&&intval($_SESSION['behind']) >= 99) {?>
|
||||
if (preg_match('/\b(\d+)\b and \b(\d+)\b different commits each/', $str, $matches)) {
|
||||
$num1 = (int) $matches[1];
|
||||
$num2 = (int) $matches[2];
|
||||
$sum = $num1 + $num2;
|
||||
$_SESSION['behind'] = $sum;
|
||||
}
|
||||
}
|
||||
if(isset($_SESSION['behind'])&&intval($_SESSION['behind']) >= 99) {?>
|
||||
<style>
|
||||
.updatenumber {
|
||||
width:30px !important;
|
||||
}
|
||||
</style>
|
||||
<?php }}
|
||||
<?php }
|
||||
|
||||
if (file_exists('./scripts/thisrun.txt')) {
|
||||
$config = parse_ini_file('./scripts/thisrun.txt');
|
||||
} elseif (file_exists('./scripts/firstrun.ini')) {
|
||||
$config = parse_ini_file('./scripts/firstrun.ini');
|
||||
}
|
||||
|
||||
if ($config["LATITUDE"] == "0.000" && $config["LONGITUDE"] == "0.000") {
|
||||
echo "<center style='color:red'><b>WARNING: Your latitude and longitude are not set properly. Please do so now in Tools -> Settings.</center></b>";
|
||||
}
|
||||
elseif ($config["LATITUDE"] == "0.000") {
|
||||
echo "<center style='color:red'><b>WARNING: Your latitude is not set properly. Please do so now in Tools -> Settings.</center></b>";
|
||||
}
|
||||
elseif ($config["LONGITUDE"] == "0.000") {
|
||||
echo "<center style='color:red'><b>WARNING: Your longitude is not set properly. Please do so now in Tools -> Settings.</center></b>";
|
||||
}
|
||||
?>
|
||||
<link rel="stylesheet" href="style.css?v=1.20.23">
|
||||
<link rel="stylesheet" href="style.css?v=<?php echo date ('n.d.y', filemtime('style.css')); ?>">
|
||||
<style>
|
||||
body::-webkit-scrollbar {
|
||||
display:none
|
||||
@@ -37,7 +79,7 @@ body::-webkit-scrollbar {
|
||||
<button type="submit" name="view" value="Overview" form="views">Overview</button>
|
||||
</form>
|
||||
<form action="" method="GET" id="views">
|
||||
<button type="submit" name="view" value="Today's Detections" form="views">Today's Detections</button>
|
||||
<button type="submit" name="view" value="Todays Detections" form="views">Today's Detections</button>
|
||||
</form>
|
||||
<form action="" method="GET" id="views">
|
||||
<button type="submit" name="view" value="Spectrogram" form="views">Spectrogram</button>
|
||||
@@ -107,7 +149,7 @@ if(isset($_GET['view'])){
|
||||
if($_GET['view'] == "Spectrogram"){include('spectrogram.php');}
|
||||
if($_GET['view'] == "View Log"){echo "<body style=\"scroll:no;overflow-x:hidden;\"><iframe style=\"width:calc( 100% + 1em);\" src=\"/log\"></iframe></body>";}
|
||||
if($_GET['view'] == "Overview"){include('overview.php');}
|
||||
if($_GET['view'] == "Today's Detections"){include('todays_detections.php');}
|
||||
if($_GET['view'] == "Todays Detections"){include('todays_detections.php');}
|
||||
if($_GET['view'] == "Kiosk"){$kiosk = true;include('todays_detections.php');}
|
||||
if($_GET['view'] == "Species Stats"){include('stats.php');}
|
||||
if($_GET['view'] == "Weekly Report"){include('weekly_report.php');}
|
||||
@@ -245,10 +287,10 @@ if(isset($_GET['view'])){
|
||||
} else {
|
||||
$submittedpwd = $_SERVER['PHP_AUTH_PW'];
|
||||
$submitteduser = $_SERVER['PHP_AUTH_USER'];
|
||||
$allowedCommands = array('sudo systemctl stop livestream.service && sudo /etc/init.d/icecast2 stop',
|
||||
'sudo systemctl restart livestream.service && sudo /etc/init.d/icecast2 restart',
|
||||
'sudo systemctl disable --now livestream.service && sudo systemctl disable icecast2 && sudo /etc/init.d/icecast2 stop',
|
||||
'sudo systemctl enable icecast2 && sudo /etc/init.d/icecast2 start && sudo systemctl enable --now livestream.service',
|
||||
$allowedCommands = array('sudo systemctl stop livestream.service && sudo systemctl stop icecast2.service',
|
||||
'sudo systemctl restart livestream.service && sudo systemctl restart icecast2.service',
|
||||
'sudo systemctl disable --now livestream.service && sudo systemctl disable icecast2 && sudo systemctl stop icecast2.service',
|
||||
'sudo systemctl enable icecast2 && sudo systemctl start icecast2.service && sudo systemctl enable --now livestream.service',
|
||||
'sudo systemctl stop web_terminal.service',
|
||||
'sudo systemctl restart web_terminal.service',
|
||||
'sudo systemctl disable --now web_terminal.service',
|
||||
@@ -296,8 +338,27 @@ if(isset($_GET['view'])){
|
||||
if(isset($command)){
|
||||
$initcommand = $command;
|
||||
if (strpos($command, "systemctl") !== false) {
|
||||
$tmp = explode(" ",trim($command));
|
||||
$command .= "& sleep 3;sudo systemctl status ".end($tmp);
|
||||
//If there more than one command to execute, processes then separately
|
||||
//currently only livestream service uses multiple commands to interact with the required services
|
||||
if (strpos($command, " && ") !== false) {
|
||||
$separate_commands = explode("&&", trim($command));
|
||||
$new_multiservice_status_command = "";
|
||||
foreach ($separate_commands as $indiv_service_command) {
|
||||
//explode the string by " " space so we can get each individual component of the command
|
||||
//and eventually the service name at the end
|
||||
$separate_command_tmp = explode(" ", trim($indiv_service_command));
|
||||
//get the service names
|
||||
$new_multiservice_status_command .= " " . trim(end($separate_command_tmp));
|
||||
}
|
||||
|
||||
$service_names = $new_multiservice_status_command;
|
||||
} else {
|
||||
//only one service needs restarting so we only need to query the status of one service
|
||||
$tmp = explode(" ", trim($command));
|
||||
$service_names = end($tmp);
|
||||
}
|
||||
|
||||
$command .= " & sleep 3;sudo systemctl status " . $service_names;
|
||||
}
|
||||
if($initcommand == "update_birdnet.sh") {
|
||||
unset($_SESSION['behind']);
|
||||
@@ -307,10 +368,52 @@ if(isset($_GET['view'])){
|
||||
$results = str_replace("failed", "<span style='color:red'>failed</span>",$results);
|
||||
$results = str_replace("active (running)", "<span style='color:green'><b>active (running)</b></span>",$results);
|
||||
$results = str_replace("Your branch is up to date", "<span style='color:limegreen'><b>Your branch is up to date</b></span>",$results);
|
||||
|
||||
$results = str_replace("(+)", "(<span style='color:lime;font-weight:bold'>+</span>)",$results);
|
||||
$results = str_replace("(-)", "(<span style='color:red;font-weight:bold'>-</span>)",$results);
|
||||
|
||||
// split the input string into lines
|
||||
$lines = explode("\n", $results);
|
||||
|
||||
// iterate over each line
|
||||
foreach ($lines as &$line) {
|
||||
// check if the line matches the pattern
|
||||
if (preg_match('/^(.+?)\s*\|\s*(\d+)\s*([\+\- ]+)(\d+)?$/', $line, $matches)) {
|
||||
// extract the filename, count, and indicator letters
|
||||
$filename = $matches[1];
|
||||
$count = $matches[2];
|
||||
$diff = $matches[3];
|
||||
$delta = $matches[4] ?? '';
|
||||
// determine the indicator letters
|
||||
$diff_array = str_split($diff);
|
||||
$indicators = array_map(function ($d) use ($delta) {
|
||||
if ($d === '+') {
|
||||
return "<span style='color:lime;'><b>+</b></span>";
|
||||
} elseif ($d === '-') {
|
||||
return "<span style='color:red;'><b>-</b></span>";
|
||||
} elseif ($d === ' ') {
|
||||
if ($delta !== '') {
|
||||
return 'A';
|
||||
} else {
|
||||
return ' ';
|
||||
}
|
||||
}
|
||||
}, $diff_array);
|
||||
// modify the line with the new indicator letters
|
||||
$line = sprintf('%-35s|%3d %s%s', $filename, $count, implode('', $indicators), $delta);
|
||||
}
|
||||
}
|
||||
|
||||
// rejoin the modified lines into a string
|
||||
$output = implode("\n", $lines);
|
||||
$results = $output;
|
||||
|
||||
// remove script tags (xss)
|
||||
$results = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $results);
|
||||
if(strlen($results) == 0) {
|
||||
$results = "This command has no output.";
|
||||
}
|
||||
echo "<table style='min-width:70%;'><tr class='relative'><th>Output of command:`".$initcommand."`<button class='copyimage' style='right:40px' onclick='copyOutput(this);'>Copy</button></th></tr><tr><td><pre style='text-align:left'>$results</pre></td></tr></table>";
|
||||
echo "<table style='min-width:70%;'><tr class='relative'><th>Output of command:`".$initcommand."`<button class='copyimage' style='right:40px' onclick='copyOutput(this);'>Copy</button></th></tr><tr><td style='padding-left: 0px;padding-right: 0px;padding-bottom: 0px;padding-top: 0px;'><pre class='bash' style='text-align:left;margin:0px'>$results</pre></td></tr></table>";
|
||||
} else {
|
||||
header('WWW-Authenticate: Basic realm="My Realm"');
|
||||
header('HTTP/1.0 401 Unauthorized');
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -5,6 +5,15 @@ if [ "$EUID" == 0 ]
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ "$(uname -m)" != "aarch64" ];then
|
||||
echo "BirdNET-Pi requires a 64-bit OS.
|
||||
It looks like your operating system is using $(uname -m),
|
||||
but would need to be aarch64.
|
||||
Please take a look at https://birdnetwiki.pmcgui.xyz for more
|
||||
information"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Simple new installer
|
||||
HOME=$HOME
|
||||
USER=$USER
|
||||
|
||||
+4
-2
@@ -8,9 +8,11 @@ pytz
|
||||
tzlocal
|
||||
pandas
|
||||
seaborn
|
||||
streamlit
|
||||
streamlit==1.19.0
|
||||
plotly
|
||||
apprise
|
||||
apprise==1.2.1
|
||||
paho-mqtt
|
||||
pytest==7.1.2
|
||||
pytest-mock==3.7.0
|
||||
suntime
|
||||
altair<5
|
||||
+392
-23
@@ -52,6 +52,8 @@ if(isset($_GET['submit'])) {
|
||||
|
||||
if(isset($_GET["birdnetpi_url"])) {
|
||||
$birdnetpi_url = $_GET["birdnetpi_url"];
|
||||
// remove trailing slash to prevent conf from becoming broken
|
||||
$birdnetpi_url = rtrim($birdnetpi_url, '/');
|
||||
if(strcmp($birdnetpi_url,$config['BIRDNETPI_URL']) !== 0) {
|
||||
$contents = preg_replace("/BIRDNETPI_URL=.*/", "BIRDNETPI_URL=$birdnetpi_url", $contents);
|
||||
$contents2 = preg_replace("/BIRDNETPI_URL=.*/", "BIRDNETPI_URL=$birdnetpi_url", $contents2);
|
||||
@@ -77,6 +79,39 @@ if(isset($_GET['submit'])) {
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_GET["rtsp_stream_to_livestream"])) {
|
||||
$rtsp_stream_selected = trim($_GET["rtsp_stream_to_livestream"]);
|
||||
|
||||
//Setting exists already, see if the value changed
|
||||
if (strcmp($rtsp_stream_selected, $config['RTSP_STREAM_TO_LIVESTREAM']) !== 0) {
|
||||
$contents = preg_replace("/RTSP_STREAM_TO_LIVESTREAM=.*/", "RTSP_STREAM_TO_LIVESTREAM=\"$rtsp_stream_selected\"", $contents);
|
||||
$contents2 = preg_replace("/RTSP_STREAM_TO_LIVESTREAM=.*/", "RTSP_STREAM_TO_LIVESTREAM=\"$rtsp_stream_selected\"", $contents2);
|
||||
$fh = fopen("/etc/birdnet/birdnet.conf", "w");
|
||||
$fh2 = fopen("./scripts/thisrun.txt", "w");
|
||||
fwrite($fh, $contents);
|
||||
fwrite($fh2, $contents2);
|
||||
sleep(1);
|
||||
exec("sudo systemctl restart livestream.service");
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_GET["activate_freqshift_in_livestream"])) {
|
||||
$activate_freqshift_in_livestream = trim($_GET["activate_freqshift_in_livestream"]);
|
||||
|
||||
//Setting exists already, see if the value changed
|
||||
if (strcmp($activate_freqshift_in_livestream, $config['ACTIVATE_FREQSHIFT_IN_LIVESTREAM']) !== 0) {
|
||||
$contents = preg_replace("/ACTIVATE_FREQSHIFT_IN_LIVESTREAM=.*/", "ACTIVATE_FREQSHIFT_IN_LIVESTREAM=\"$activate_freqshift_in_livestream\"", $contents);
|
||||
$contents2 = preg_replace("/ACTIVATE_FREQSHIFT_IN_LIVESTREAM=.*/", "ACTIVATE_FREQSHIFT_IN_LIVESTREAM=\"$activate_freqshift_in_livestream\"", $contents2);
|
||||
$fh = fopen("/etc/birdnet/birdnet.conf", "w");
|
||||
$fh2 = fopen("./scripts/thisrun.txt", "w");
|
||||
fwrite($fh, $contents);
|
||||
fwrite($fh2, $contents2);
|
||||
sleep(1);
|
||||
exec("sudo systemctl restart livestream.service");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(isset($_GET["overlap"])) {
|
||||
$overlap = $_GET["overlap"];
|
||||
if(strcmp($overlap,$config['OVERLAP']) !== 0) {
|
||||
@@ -101,7 +136,7 @@ if(isset($_GET['submit'])) {
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($_GET["freqshift_hi"])) {
|
||||
if(isset($_GET["freqshift_hi"]) && is_numeric($_GET['freqshift_hi'])) {
|
||||
$freqshift_hi = $_GET["freqshift_hi"];
|
||||
if(strcmp($freqshift_hi,$config['FREQSHIFT_HI']) !== 0) {
|
||||
$contents = preg_replace("/FREQSHIFT_HI=.*/", "FREQSHIFT_HI=$freqshift_hi", $contents);
|
||||
@@ -109,7 +144,7 @@ if(isset($_GET['submit'])) {
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($_GET["freqshift_lo"])) {
|
||||
if(isset($_GET["freqshift_lo"]) && is_numeric($_GET['freqshift_lo'])) {
|
||||
$freqshift_lo = $_GET["freqshift_lo"];
|
||||
if(strcmp($freqshift_lo,$config['FREQSHIFT_LO']) !== 0) {
|
||||
$contents = preg_replace("/FREQSHIFT_LO=.*/", "FREQSHIFT_LO=$freqshift_lo", $contents);
|
||||
@@ -117,7 +152,7 @@ if(isset($_GET['submit'])) {
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($_GET["freqshift_pitch"])) {
|
||||
if(isset($_GET["freqshift_pitch"]) && is_numeric($_GET['freqshift_pitch'])) {
|
||||
$freqshift_pitch = $_GET["freqshift_pitch"];
|
||||
if(strcmp($freqshift_pitch,$config['FREQSHIFT_PITCH']) !== 0) {
|
||||
$contents = preg_replace("/FREQSHIFT_PITCH=.*/", "FREQSHIFT_PITCH=$freqshift_pitch", $contents);
|
||||
@@ -133,6 +168,14 @@ if(isset($_GET['submit'])) {
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($_GET["freqshift_reconnect_delay"]) && is_numeric($_GET['freqshift_reconnect_delay'])) {
|
||||
$freqshift_reconnect_delay = $_GET["freqshift_reconnect_delay"];
|
||||
if(strcmp($freqshift_hi,$config['FREQSHIFT_RECONNECT_DELAY']) !== 0) {
|
||||
$contents = preg_replace("/FREQSHIFT_RECONNECT_DELAY=.*/", "FREQSHIFT_RECONNECT_DELAY=$freqshift_reconnect_delay", $contents);
|
||||
$contents2 = preg_replace("/FREQSHIFT_RECONNECT_DELAY=.*/", "FREQSHIFT_RECONNECT_DELAY=$freqshift_reconnect_delay", $contents2);
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($_GET["full_disk"])) {
|
||||
$full_disk = $_GET["full_disk"];
|
||||
if(strcmp($full_disk,$config['FULL_DISK']) !== 0) {
|
||||
@@ -200,6 +243,129 @@ if(isset($_GET['submit'])) {
|
||||
$contents2 = preg_replace("/SILENCE_UPDATE_INDICATOR=.*/", "SILENCE_UPDATE_INDICATOR=0", $contents2);
|
||||
}
|
||||
|
||||
if(isset($_GET["raw_spectrogram"])) {
|
||||
$raw_spectrogram = 1;
|
||||
if(strcmp($RAW_SPECTROGRAM,$config['RAW_SPECTROGRAM']) !== 0) {
|
||||
$contents = preg_replace("/RAW_SPECTROGRAM=.*/", "RAW_SPECTROGRAM=$raw_spectrogram", $contents);
|
||||
$contents2 = preg_replace("/RAW_SPECTROGRAM=.*/", "RAW_SPECTROGRAM=$raw_spectrogram", $contents2);
|
||||
}
|
||||
} else {
|
||||
$contents = preg_replace("/RAW_SPECTROGRAM=.*/", "RAW_SPECTROGRAM=0", $contents);
|
||||
$contents2 = preg_replace("/RAW_SPECTROGRAM=.*/", "RAW_SPECTROGRAM=0", $contents2);
|
||||
}
|
||||
|
||||
if(isset($_GET["custom_image"])) {
|
||||
$custom_image = $_GET["custom_image"];
|
||||
if(strcmp($custom_image,$config['CUSTOM_IMAGE']) !== 0) {
|
||||
$contents = preg_replace("/CUSTOM_IMAGE=.*/", "CUSTOM_IMAGE=$custom_image", $contents);
|
||||
$contents2 = preg_replace("/CUSTOM_IMAGE=.*/", "CUSTOM_IMAGE=$custom_image", $contents2);
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($_GET["custom_image_label"])) {
|
||||
$custom_image_label = $_GET["custom_image_label"];
|
||||
if(strcmp($custom_image_label,$config['CUSTOM_IMAGE_TITLE']) !== 0) {
|
||||
$contents = preg_replace("/CUSTOM_IMAGE_TITLE=.*/", "CUSTOM_IMAGE_TITLE=\"$custom_image_label\"", $contents);
|
||||
$contents2 = preg_replace("/CUSTOM_IMAGE_TITLE=.*/", "CUSTOM_IMAGE_TITLE=\"$custom_image_label\"", $contents2);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_GET["LogLevel_BirdnetRecordingService"])) {
|
||||
$birdnet_recording_service_log_level = trim($_GET["LogLevel_BirdnetRecordingService"]);
|
||||
|
||||
//If setting exists change it's value
|
||||
if (array_key_exists('LogLevel_BirdnetRecordingService', $config)) {
|
||||
//Setting exists already, see if the value changed
|
||||
if (strcmp($birdnet_recording_service_log_level, $config['LogLevel_BirdnetRecordingService']) !== 0) {
|
||||
$contents = preg_replace("/LogLevel_BirdnetRecordingService=.*/", "LogLevel_BirdnetRecordingService=\"$birdnet_recording_service_log_level\"", $contents);
|
||||
$contents2 = preg_replace("/LogLevel_BirdnetRecordingService=.*/", "LogLevel_BirdnetRecordingService=\"$birdnet_recording_service_log_level\"", $contents2);
|
||||
$fh = fopen("/etc/birdnet/birdnet.conf", "w");
|
||||
$fh2 = fopen("./scripts/thisrun.txt", "w");
|
||||
fwrite($fh, $contents);
|
||||
fwrite($fh2, $contents2);
|
||||
sleep(1);
|
||||
exec("sudo systemctl restart birdnet_recording.service");
|
||||
}
|
||||
} else {
|
||||
//Create the setting in the setting file - same as what update_birdnet_snippets.sh does but will take the users selected log level as the value
|
||||
shell_exec('sudo echo "LogLevel_BirdnetRecordingService=\"' . $birdnet_recording_service_log_level . '\"" >> /etc/birdnet/birdnet.conf');
|
||||
//also update this run txt file
|
||||
shell_exec('sudo echo "LogLevel_BirdnetRecordingService=\"' . $birdnet_recording_service_log_level . '\"" >> ./scripts/thisrun.txt');
|
||||
|
||||
//Reload the config files as we've changed the contents, we need to make sure the contents of the existing variables reflects contents of the config file
|
||||
sleep(1);
|
||||
$contents = file_get_contents('/etc/birdnet/birdnet.conf');
|
||||
$contents2 = file_get_contents('./scripts/thisrun.txt');
|
||||
|
||||
exec("sudo systemctl restart birdnet_recording.service");
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_GET["LogLevel_SpectrogramViewerService"])) {
|
||||
$spectrogram_viewer_service_log_level = trim($_GET["LogLevel_SpectrogramViewerService"]);
|
||||
|
||||
//If setting exists change it's value
|
||||
if (array_key_exists('LogLevel_SpectrogramViewerService', $config)) {
|
||||
//Setting exists already, see if the value changed
|
||||
if (strcmp($spectrogram_viewer_service_log_level, $config['LogLevel_SpectrogramViewerService']) !== 0) {
|
||||
$contents = preg_replace("/LogLevel_SpectrogramViewerService=.*/", "LogLevel_SpectrogramViewerService=\"$spectrogram_viewer_service_log_level\"", $contents);
|
||||
$contents2 = preg_replace("/LogLevel_SpectrogramViewerService=.*/", "LogLevel_SpectrogramViewerService=\"$spectrogram_viewer_service_log_level\"", $contents2);
|
||||
$fh = fopen("/etc/birdnet/birdnet.conf", "w");
|
||||
$fh2 = fopen("./scripts/thisrun.txt", "w");
|
||||
fwrite($fh, $contents);
|
||||
fwrite($fh2, $contents2);
|
||||
sleep(1);
|
||||
exec("sudo systemctl restart spectrogram_viewer.service");
|
||||
}
|
||||
} else {
|
||||
//Create the setting in the setting file - same as what update_birdnet_snippets.sh does but will take the users selected log level as the value
|
||||
shell_exec('sudo echo "LogLevel_SpectrogramViewerService=\"' . $spectrogram_viewer_service_log_level . '\"" >> /etc/birdnet/birdnet.conf');
|
||||
//also update this run txt file
|
||||
shell_exec('sudo echo "LogLevel_SpectrogramViewerService=\"' . $spectrogram_viewer_service_log_level . '\"" >> ./scripts/thisrun.txt');
|
||||
|
||||
//Reload the config files as we've changed the contents, we need to make sure the contents of the existing variables reflects contents of the config file
|
||||
sleep(1);
|
||||
$contents = file_get_contents('/etc/birdnet/birdnet.conf');
|
||||
$contents2 = file_get_contents('./scripts/thisrun.txt');
|
||||
|
||||
exec("sudo systemctl restart spectrogram_viewer.service");
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_GET["LogLevel_LiveAudioStreamService"])) {
|
||||
$livestream_audio_service_log_level = trim($_GET["LogLevel_LiveAudioStreamService"]);
|
||||
|
||||
//If setting exists change it's value
|
||||
if (array_key_exists('LogLevel_LiveAudioStreamService', $config)) {
|
||||
//Setting exists already, see if the value changed
|
||||
if (strcmp($livestream_audio_service_log_level, $config['LogLevel_LiveAudioStreamService']) !== 0) {
|
||||
$contents = preg_replace("/LogLevel_LiveAudioStreamService=.*/", "LogLevel_LiveAudioStreamService=\"$livestream_audio_service_log_level\"", $contents);
|
||||
$contents2 = preg_replace("/LogLevel_LiveAudioStreamService=.*/", "LogLevel_LiveAudioStreamService=\"$livestream_audio_service_log_level\"", $contents2);
|
||||
//Write the settings to the config files, so we can restart the relevant services
|
||||
$fh = fopen("/etc/birdnet/birdnet.conf", "w");
|
||||
$fh2 = fopen("./scripts/thisrun.txt", "w");
|
||||
fwrite($fh, $contents);
|
||||
fwrite($fh2, $contents2);
|
||||
sleep(1);
|
||||
exec("sudo systemctl restart livestream.service && sudo systemctl restart icecast2.service");
|
||||
}
|
||||
} else {
|
||||
//Create the setting in the setting file - same as what update_birdnet_snippets.sh does but will take the users selected log level as the value
|
||||
shell_exec('sudo echo "LogLevel_LiveAudioStreamService=\"' . $livestream_audio_service_log_level . '\"" >> /etc/birdnet/birdnet.conf');
|
||||
//also update this run txt file
|
||||
shell_exec('sudo echo "LogLevel_LiveAudioStreamService=\"' . $livestream_audio_service_log_level . '\"" >> ./scripts/thisrun.txt');
|
||||
|
||||
//Reload the config files as we've changed the contents, we need to make sure the contents of the existing variables reflects contents of the config file
|
||||
sleep(1);
|
||||
$contents = file_get_contents('/etc/birdnet/birdnet.conf');
|
||||
$contents2 = file_get_contents('./scripts/thisrun.txt');
|
||||
|
||||
exec("sudo systemctl restart livestream.service && sudo systemctl restart icecast2.service");
|
||||
}
|
||||
}
|
||||
|
||||
//Finally write the data out. some sections do this themselves in order to have the new settings ready for the services that will be restarted
|
||||
//but will doubly ensure the settings are saved after any modification
|
||||
$fh = fopen('/etc/birdnet/birdnet.conf', "w");
|
||||
$fh2 = fopen("./scripts/thisrun.txt", "w");
|
||||
fwrite($fh, $contents);
|
||||
@@ -225,9 +391,10 @@ if (file_exists('./scripts/thisrun.txt')) {
|
||||
$newconfig = parse_ini_file('./scripts/firstrun.ini');
|
||||
}
|
||||
?>
|
||||
<h2>Advanced Settings</h2>
|
||||
<div class="brbanner"><h1>Advanced Settings</h1></div><br>
|
||||
<form action="" method="GET">
|
||||
<label>Privacy Threshold: </label><br>
|
||||
<table class="settingstable"><tr><td>
|
||||
<h2>Privacy Threshold</h2>
|
||||
<div class="slidecontainer">
|
||||
<input name="privacy_threshold" type="range" min="0" max="3" value="<?php print($newconfig['PRIVACY_THRESHOLD']);?>" class="slider" id="privacy_threshold">
|
||||
<p>Value: <span id="threshold_value"></span>%</p>
|
||||
@@ -244,21 +411,25 @@ if (file_exists('./scripts/thisrun.txt')) {
|
||||
}
|
||||
</script>
|
||||
<p>If a Human is predicted anywhere among the top <span id="predictionCount"><?php echo $newconfig['PRIVACY_THRESHOLD'] == 0 ? "threshold % of" : intval(($newconfig['PRIVACY_THRESHOLD'] * $count)/100); ?></span> predictions, the sample will be considered of human origin and no data will be collected. Start with 1% and move up as needed.</p>
|
||||
<label>Full Disk Behavior: </label>
|
||||
</td></tr></table><br>
|
||||
|
||||
<table class="settingstable"><tr><td>
|
||||
<h2>Full Disk Behaviour</h2>
|
||||
<label for="purge">
|
||||
<input name="full_disk" type="radio" id="purge" value="purge" <?php if (strcmp($newconfig['FULL_DISK'], "purge") == 0) { echo "checked"; }?>>Purge</label>
|
||||
<label for="keep">
|
||||
<input name="full_disk" type="radio" id="keep" value="keep" <?php if (strcmp($newconfig['FULL_DISK'], "keep") == 0) { echo "checked"; }?>>Keep</label>
|
||||
<p>When the disk becomes full, you can choose to 'purge' old files to make room for new ones or 'keep' your data and stop all services instead.<br>Note: you can exclude specific files from 'purge' on the Recordings page.</p>
|
||||
</td></tr></table><br>
|
||||
<table class="settingstable"><tr><td>
|
||||
|
||||
<h2>Audio Settings</h2>
|
||||
<label for="rec_card">Audio Card: </label>
|
||||
<input name="rec_card" type="text" value="<?php print($newconfig['REC_CARD']);?>" required/><br>
|
||||
<p>Set Audio Card to 'default' to use PulseAudio (always recommended), or an ALSA recognized sound card device from the output of `arecord -L`. Choose the `dsnoop` device if it is available</p>
|
||||
<label for="channels">Audio Channels: </label>
|
||||
<input name="channels" type="number" min="1" max="32" step="1" value="<?php print($newconfig['CHANNELS']);?>" required/><br>
|
||||
<p>Set Channels to the number of channels supported by your sound card. 32 max.</p>
|
||||
<label for="rtsp_stream">RTSP Stream: </label>
|
||||
<input name="rtsp_stream" type="url" value="<?php echo $newconfig['RTSP_STREAM'];?>"</input><br>
|
||||
<p>If you place an RTSP stream URL here, BirdNET-Pi will use that as its audio source.</p>
|
||||
<label for="recording_length">Recording Length: </label>
|
||||
<input name="recording_length" oninput="document.getElementsByName('extraction_length')[0].setAttribute('max', this.value);" type="number" min="3" max="60" step="1" value="<?php print($newconfig['RECORDING_LENGTH']);?>" required/><br>
|
||||
<p>Set Recording Length in seconds between 6 and 60. Multiples of 3 are recommended, as BirdNET analyzes in 3-second chunks.</p>
|
||||
@@ -275,19 +446,133 @@ foreach($formats as $format){
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<h3>BirdNET-Pi Password</h3>
|
||||
<br><br>
|
||||
|
||||
<label id="rtsp_stream_input_label" for="rtsp_stream">RTSP Stream: </label>
|
||||
<br>
|
||||
<input style="display: none;" name="rtsp_stream" type="url" value="">
|
||||
<input style="display: none;" id="rtsp_stream_url_placeholder" name="rtsp_stream_placeholder" type="url" size="60" value="">
|
||||
<?php
|
||||
//Print out the rtsp urls in their own input fields
|
||||
//Explode the stream into an array at the comma
|
||||
$rtsp_streams = explode(",", $newconfig['RTSP_STREAM']);
|
||||
//Print out existing streams
|
||||
foreach ($rtsp_streams as $stream_idx => $stream_url) {
|
||||
//For the first input keep the element mostly the same as the original but without styling to align it
|
||||
if ($stream_idx === 0) {
|
||||
?>
|
||||
<input id="rtsp_stream_url_0" name="rtsp_stream_0" type="url" size="60" value="<?php echo $stream_url; ?>">
|
||||
<br>
|
||||
<?php
|
||||
} else {
|
||||
//For every other input field, change the id to reflect the URL's index in the array
|
||||
?>
|
||||
<input id="rtsp_stream_url_<?php echo $stream_idx; ?>" name="rtsp_stream_<?php echo $stream_idx; ?>" type="url" size="60"
|
||||
value="<?php echo $stream_url; ?>">
|
||||
<br>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div id="newrtspstream_button_container">
|
||||
<br>
|
||||
<span id="newrtspstream" onclick="addNewrtspInput();">Add</span><br>
|
||||
</div>
|
||||
<script>
|
||||
//Keep track of how many new input fields were added
|
||||
var number_of_new_rtsp_urls_added = 0;
|
||||
|
||||
//Function to insert new input fields
|
||||
function addNewrtspInput() {
|
||||
//Find the placeholder input field
|
||||
var url_template_element = document.getElementById('rtsp_stream_url_placeholder');
|
||||
var new_url_input_template = url_template_element.cloneNode();
|
||||
var br_seperator = document.createElement("BR");
|
||||
|
||||
//Fix up the new element so it's visible, set the style so it's sligned correctly
|
||||
new_url_input_template.setAttribute("id", "rtsp_stream_url_new_" + number_of_new_rtsp_urls_added);
|
||||
new_url_input_template.setAttribute("name", "rtsp_stream_new_" + number_of_new_rtsp_urls_added);
|
||||
new_url_input_template.removeAttribute("style");
|
||||
|
||||
//Insert the new input field before the button to add new urls
|
||||
var newrtspstream_button = document.getElementById('newrtspstream_button_container');
|
||||
//Insert the new input element before the newrtspstream button
|
||||
newrtspstream_button.parentNode.insertBefore(new_url_input_template, newrtspstream_button);
|
||||
//Add a separator before the button
|
||||
newrtspstream_button.parentNode.insertBefore(br_seperator, newrtspstream_button);
|
||||
|
||||
//Increment the counter
|
||||
number_of_new_rtsp_urls_added++;
|
||||
}
|
||||
|
||||
var rtsp_stream_string = "";
|
||||
var rtsp_stream_string_array = [];
|
||||
|
||||
//Collect all the rtsp urls that have been set, concat them into a single string and set it into the rtsp_stream input field so it gets saved
|
||||
function collectrtspUrls() {
|
||||
//Reset the array and string so we don't get duplicates
|
||||
rtsp_stream_string = "";
|
||||
rtsp_stream_string_array = [];
|
||||
|
||||
//Get the inputs by name (which is similar across
|
||||
var existing_rtsp_stream_urls = document.querySelectorAll('[name^="rtsp_stream_"]');
|
||||
//Loop over the result and get the values
|
||||
for (let i = 0; i < existing_rtsp_stream_urls.length; i++) {
|
||||
//Only collect results that re not empty and add them to the array
|
||||
if (existing_rtsp_stream_urls[i].value !== 'undefined' && existing_rtsp_stream_urls[i].value !== "") {
|
||||
rtsp_stream_string_array.push(existing_rtsp_stream_urls[i].value.trim());
|
||||
}
|
||||
}
|
||||
|
||||
//if the array is not empty, then implode the array joining all the values by a comma
|
||||
if (rtsp_stream_string_array.length !== 0) {
|
||||
rtsp_stream_string = rtsp_stream_string_array.join(',');
|
||||
//Locate the hidden rtsp_stream input field that we'll populate with the full string which will get saved to the config file
|
||||
var rtsp_stream_input = document.querySelector('[name=rtsp_stream]');
|
||||
rtsp_stream_input.setAttribute('value', rtsp_stream_string);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<p>If you place an RTSP stream URL here, BirdNET-Pi will use that as its audio source.<br>Multiple streams are allowed but may have a impact on rPi performance.<br>Analyze ffmpeg CPU/Memory usage with <b>top</b> or <b>htop</b> if necessary.<br>To remove all and use the soundcard again, just delete the RTSP entries and click Save at the bottom.</p>
|
||||
</td></tr></table><br>
|
||||
<table class="settingstable"><tr><td>
|
||||
<h2>BirdNET-Pi Password</h2>
|
||||
<p>This password will protect your "Tools" page and "Live Audio" stream.</p>
|
||||
<p>Do NOT use special characters. Accepted characters: [A-Z0-9a-z]</p>
|
||||
<label for="caddy_pwd">Password: </label>
|
||||
<input style="width:40ch" name="caddy_pwd" id="caddy_pwd" type="password" pattern="[A-Za-z0-9]+" title="Password must be alphanumeric (A-Z, 0-9)" value="<?php print($newconfig['CADDY_PWD']);?>" /><span id="showpassword" onmouseover="document.getElementById('caddy_pwd').type='text';" onmouseout="document.getElementById('caddy_pwd').type='password';">show</span><br>
|
||||
<h3>Custom URL</h3>
|
||||
</td></tr></table><br>
|
||||
<table class="settingstable"><tr><td>
|
||||
<h2>Custom URL</h2>
|
||||
<p>When you update the URL below, the web server will reload, so be sure to wait at least 30 seconds and then go to your new URL.</p>
|
||||
<label for="birdnetpi_url">BirdNET-Pi URL: </label>
|
||||
<input style="width:40ch;" name="birdnetpi_url" type="url" value="<?php print($newconfig['BIRDNETPI_URL']);?>" /><br>
|
||||
<p>The BirdNET-Pi URL is how the main page will be reached. If you want your installation to respond to an IP address, place that here, but be sure to indicate "<i>http://</i>".<br>Example for IP: <i>http://192.168.0.109</i><br>Example if you own your own domain: <i>https://virginia.birdnetpi.com</i></p>
|
||||
</td></tr></table><br>
|
||||
<table class="settingstable"><tr><td>
|
||||
<h2>Options</h2>
|
||||
<label for="silence_update_indicator">Silence Update Indicator: </label>
|
||||
<input type="checkbox" name="silence_update_indicator" <?php if($newconfig['SILENCE_UPDATE_INDICATOR'] == 1) { echo "checked"; };?> ><br>
|
||||
<p>This allows you to quiet the display of how many commits your installation is behind by relative to the Github repo. This number appears next to "Tools" when you're 50 or more commits behind.</p>
|
||||
|
||||
<h3>BirdNET-Lite Settings</h3>
|
||||
<label for="raw_spectrogram">Minimalist Spectrograms: </label>
|
||||
<input type="checkbox" name="raw_spectrogram" <?php if($newconfig['RAW_SPECTROGRAM'] == 1) { echo "checked"; };?> ><br>
|
||||
<p>This allows you to remove the axes and labels of the spectrograms that are generated by Sox for each detection for a cleaner appearance.</p>
|
||||
</td></tr></table><br>
|
||||
|
||||
<table class="settingstable"><tr><td>
|
||||
<h2>Custom Image</h2>
|
||||
<label for="custom_image">Custom Image Absolute Path: </label>
|
||||
<input name="custom_image" type="text" value="<?php print($newconfig['CUSTOM_IMAGE']);?>"/><br>
|
||||
|
||||
<label for="custom_image_label">Custom Image Label: </label>
|
||||
<input name="custom_image_label" type="text" value="<?php print($newconfig['CUSTOM_IMAGE_TITLE']);?>"/><br>
|
||||
|
||||
<p>These allow you to show a custom image on the Overview page of your BirdNET-Pi. This can be used to show a dynamically updating picture of your garden, for example.</p>
|
||||
</td></tr></table><br>
|
||||
|
||||
<table class="settingstable"><tr><td>
|
||||
<h2>BirdNET-Lite Settings</h2>
|
||||
|
||||
<p>
|
||||
<label for="overlap">Overlap: </label>
|
||||
@@ -304,15 +589,17 @@ foreach($formats as $format){
|
||||
<input name="sensitivity" type="number" min="0.5" max="1.5" step="0.01" value="<?php print($newconfig['SENSITIVITY']);?>" required/><br>
|
||||
Min=0.5, Max=1.5
|
||||
</p>
|
||||
</td></tr></table><br>
|
||||
|
||||
<h3>Accessibility Settings</h3>
|
||||
<table class="settingstable"><tr><td>
|
||||
<h2>Accessibility Settings</h2>
|
||||
|
||||
<p>Birdsongs Frequency shifting configuration:<br>
|
||||
this can be useful for hearing impaired people.<br>
|
||||
This can be useful for hearing impaired people. <br>Note: audio files will only be pitch shifted when the "FREQ SHIFT" button is manually clicked for a detection on the "Recordings" page. <br>The frequency shifting can also be activated for the realtime audio livestream, accessible in the SPECTROGRAM tab of BirdNET-Pi. Once it has been activated, it will be made available for the Live Audio feature as well.<br>Livestream is using ffmpeg for streaming its audio data, so the pitch shifter in that case will use this tool too. If you choose sox as the tool for freq shifting recorded audio files, then you must configure both sox and ffmpeg parameters: sox for recordings, and ffmpeg for livestream.<br>
|
||||
|
||||
<p style="margin-left: 40px">
|
||||
|
||||
<label for="freqshift_tool">shifting tool: </label>
|
||||
<label for="freqshift_tool">Shifting tool: </label>
|
||||
<select name="freqshift_tool">
|
||||
<option selected="<?php print($newconfig['FREQSHIFT_TOOL']);?>"><?php print($newconfig['FREQSHIFT_TOOL']);?></option>
|
||||
<?php
|
||||
@@ -325,29 +612,111 @@ foreach($formats as $format){
|
||||
?>
|
||||
</select>
|
||||
|
||||
Choose here the shifting tool.<br>
|
||||
Choose the shifting tool here.<br>
|
||||
</p>
|
||||
|
||||
<p style="margin-left: 40px">
|
||||
using ffmpeg:
|
||||
Using ffmpeg:
|
||||
e.g. origin=6000, target=4000, performs a shift of 2000 Hz down.<br>
|
||||
<label for="freqshift_hi">origin [Hz]: </label>
|
||||
<label for="freqshift_hi">Origin [Hz]: </label>
|
||||
<input name="freqshift_hi" type="number" min="0" max="20000" step="1" value="<?php print($newconfig['FREQSHIFT_HI']);?>" required/><br>
|
||||
<label for="freqshift_lo">target [Hz]: </label>
|
||||
<label for="freqshift_lo">Target [Hz]: </label>
|
||||
<input name="freqshift_lo" type="number" min="0" max="20000" step="1" value="<?php print($newconfig['FREQSHIFT_LO']);?>" required/>
|
||||
</p>
|
||||
<p style="margin-left: 40px">
|
||||
<label for="freqshift_reconnect_delay">Livestream reconnection delay (in ms): </label>
|
||||
<input name="freqshift_reconnect_delay" type="number" min="1000" max="10000" step="100" value="<?php print($newconfig['FREQSHIFT_RECONNECT_DELAY']);?>" required/>
|
||||
</p>
|
||||
|
||||
<p style="margin-left: 40px">
|
||||
using sox:
|
||||
Using sox:
|
||||
e.g. shiftPitch=-1200 performs a shift of 1 octave down. This value is in 100ths of a semitone.<br>
|
||||
<label for="freqshift_pitch">pitch shift: </label>
|
||||
<label for="freqshift_pitch">Pitch shift: </label>
|
||||
<input name="freqshift_pitch" type="number" min="-4000" max="4000" step="1" value="<?php print($newconfig['FREQSHIFT_PITCH']);?>" required/><br>
|
||||
</p>
|
||||
</td></tr></table><br>
|
||||
|
||||
</p>
|
||||
<table class="settingstable">
|
||||
<tr>
|
||||
<td>
|
||||
<h2>Logging</h2>
|
||||
<div class="callout callout-warning">
|
||||
<b>Note:</b>
|
||||
It is recommended that the Log Level be set to <b>Error</b> on production systems to keep output
|
||||
manageable, by only reporting errors.
|
||||
<br>
|
||||
Not all components support the log level option at this time.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Birdnet Recording:
|
||||
<select id="LogLevel_BirdnetRecordingService" name="LogLevel_BirdnetRecordingService">
|
||||
<option value="error" <?php echo $newconfig['LogLevel_BirdnetRecordingService'] == "error" || !array_key_exists('LogLevel_BirdnetRecordingService', $newconfig) ? "selected=''" : "" ?>>
|
||||
Errors Only
|
||||
</option>
|
||||
<option value="warning" <?php echo $newconfig['LogLevel_BirdnetRecordingService'] == "warning" ? "selected=''" : "" ?>>
|
||||
Warning
|
||||
</option>
|
||||
<option value="info" <?php echo $newconfig['LogLevel_BirdnetRecordingService'] == "info" ? "selected=''" : "" ?>>
|
||||
Info
|
||||
</option>
|
||||
<option value="debug" <?php echo $newconfig['LogLevel_BirdnetRecordingService'] == "debug" ? "selected=''" : "" ?>>
|
||||
Debug
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Live Audio Stream:
|
||||
<select id="LogLevel_LiveAudioStreamService" name="LogLevel_LiveAudioStreamService">
|
||||
<option value="error" <?php echo $newconfig['LogLevel_LiveAudioStreamService'] == "error" || !array_key_exists('LogLevel_LiveAudioStreamService', $newconfig) ? "selected=''" : "" ?>>
|
||||
Errors Only
|
||||
</option>
|
||||
<option value="warning" <?php echo $newconfig['LogLevel_LiveAudioStreamService'] == "warning" ? "selected=''" : "" ?>>
|
||||
Warning
|
||||
</option>
|
||||
<option value="info" <?php echo $newconfig['LogLevel_LiveAudioStreamService'] == "info" ? "selected=''" : "" ?>>
|
||||
Info
|
||||
</option>
|
||||
<option value="debug" <?php echo $newconfig['LogLevel_LiveAudioStreamService'] == "debug" ? "selected=''" : "" ?>>
|
||||
Debug
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Spectrogram Service:
|
||||
<select id="LogLevel_SpectrogramViewerService" name="LogLevel_SpectrogramViewerService">
|
||||
<option value="error" <?php echo $newconfig['LogLevel_SpectrogramViewerService'] == "error" || !array_key_exists('LogLevel_SpectrogramViewerService', $newconfig) ? "selected=''" : "" ?>>
|
||||
Errors Only
|
||||
</option>
|
||||
<option value="warning" <?php echo $newconfig['LogLevel_SpectrogramViewerService'] == "warning" ? "selected=''" : "" ?>>
|
||||
Warning
|
||||
</option>
|
||||
<option value="info" <?php echo $newconfig['LogLevel_SpectrogramViewerService'] == "info" ? "selected=''" : "" ?>>
|
||||
Info
|
||||
</option>
|
||||
<option value="debug" <?php echo $newconfig['LogLevel_SpectrogramViewerService'] == "debug" ? "selected=''" : "" ?>>
|
||||
Debug
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<small>
|
||||
<b>'error'</b> - Show all errors, including ones which can be recovered from. <b>This is the default value.</b><br>
|
||||
<b>'warning'</b> - Show all warnings and errors. Any message related to possibly incorrect or unexpected events will be shown.<br>
|
||||
<b>'info'</b> - Show informative messages and output during processing. This is in addition to warnings and errors. This will produce more output, use this for initial debugging.<br>
|
||||
<b>'debug'</b> - Show everything, including debugging information. Produces a lot of output.<br>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br><br>
|
||||
<input type="hidden" name="view" value="Advanced">
|
||||
<button onclick="if(<?php print($newconfig['PRIVACY_THRESHOLD']);?> != document.getElementById('privacy_threshold').value){return confirm('This will take about 90 seconds.')}" type="submit" name="submit" value="advanced">
|
||||
<button onclick="if(<?php print($newconfig['PRIVACY_THRESHOLD']);?> != document.getElementById('privacy_threshold').value){return confirm('This will take about 90 seconds.')} collectrtspUrls();" type="submit" name="submit" value="advanced">
|
||||
<?php
|
||||
if(isset($_GET['submit'])){
|
||||
echo "Success!";
|
||||
|
||||
@@ -189,7 +189,7 @@ TODAY="$RECS_DIR/$(date "+%B-%Y/%d-%A")"
|
||||
if [ $(find ${YESTERDAY} -name '*wav' 2>/dev/null | wc -l) -gt 0 ];then
|
||||
find $YESTERDAY -name '*wav' -type f -size 0 -delete
|
||||
run_birdnet "${YESTERDAY}"
|
||||
elif [ $(find ${TODAY} -name '*wav' | wc -l) -gt 0 ];then
|
||||
elif [ $(find ${TODAY} -name '*wav' 2>/dev/null | wc -l) -gt 0 ];then
|
||||
find $TODAY -name '*wav' -type f -size 0 -delete
|
||||
run_birdnet "${TODAY}"
|
||||
fi
|
||||
|
||||
@@ -1,15 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
set -x
|
||||
# Performs the recording from the specified RTSP stream or soundcard
|
||||
source /etc/birdnet/birdnet.conf
|
||||
|
||||
# Read the logging level from the configuration option
|
||||
LOGGING_LEVEL="${LogLevel_BirdnetRecordingService}"
|
||||
# If empty for some reason default to log level of error
|
||||
[ -z $LOGGING_LEVEL ] && LOGGING_LEVEL='error'
|
||||
# Additionally if we're at debug or info level then allow printing of script commands and variables
|
||||
if [ "$LOGGING_LEVEL" == "info" ] || [ "$LOGGING_LEVEL" == "debug" ];then
|
||||
# Enable printing of commands/variables etc to terminal for debugging
|
||||
set -x
|
||||
fi
|
||||
|
||||
[ -z $RECORDING_LENGTH ] && RECORDING_LENGTH=15
|
||||
|
||||
if [ ! -z $RTSP_STREAM ];then
|
||||
[ -d $RECS_DIR/StreamData ] || mkdir -p $RECS_DIR/StreamData
|
||||
# Explode the RSPT steam setting into an array so we can count the number we have
|
||||
RTSP_STREAMS_EXPLODED_ARRAY=(${RTSP_STREAM//,/ })
|
||||
|
||||
while true;do
|
||||
for i in ${RTSP_STREAM//,/ };do
|
||||
ffmpeg -nostdin -i ${i} -t ${RECORDING_LENGTH} -vn -acodec pcm_s16le -ac 2 -ar 48000 file:${RECS_DIR}/StreamData/$(date "+%F")-birdnet-$(date "+%H:%M:%S").wav
|
||||
# Original loop
|
||||
# for i in ${RTSP_STREAM//,/ };do
|
||||
# ffmpeg -nostdin -i ${i} -t ${RECORDING_LENGTH} -vn -acodec pcm_s16le -ac 2 -ar 48000 file:${RECS_DIR}/StreamData/$(date "+%F")-birdnet-$(date "+%H:%M:%S").wav
|
||||
# done
|
||||
|
||||
# Initially start the count off at 1 - our very first stream
|
||||
RTSP_STREAMS_STARTED_COUNT=1
|
||||
FFMPEG_PARAMS=""
|
||||
|
||||
# Loop over the streams
|
||||
for i in "${RTSP_STREAMS_EXPLODED_ARRAY[@]}"
|
||||
do
|
||||
# Map id used to map input to output (first stream being 0), this is 0 based in ffmpeg so decrement our counter (which is more human readable) by 1
|
||||
MAP_ID=$((RTSP_STREAMS_STARTED_COUNT-1))
|
||||
# Build up the parameters to process the RSTP stream, including mapping for the output
|
||||
FFMPEG_PARAMS+="-vn -thread_queue_size 512 -i ${i} -map ${MAP_ID}:a:0 -t ${RECORDING_LENGTH} -acodec pcm_s16le -ac 2 -ar 48000 file:${RECS_DIR}/StreamData/$(date "+%F")-birdnet-RTSP_${RTSP_STREAMS_STARTED_COUNT}-$(date "+%H:%M:%S").wav "
|
||||
# Increment counter
|
||||
((RTSP_STREAMS_STARTED_COUNT += 1))
|
||||
done
|
||||
|
||||
# Make sure were passing something valid to ffmpeg, ffmpeg will run interactive and control our loop by waiting ${RECORDING_LENGTH} between loops because it will stop once that much has been recorded
|
||||
if [ -n "$FFMPEG_PARAMS" ];then
|
||||
ffmpeg -hide_banner -loglevel $LOGGING_LEVEL -nostdin $FFMPEG_PARAMS
|
||||
fi
|
||||
|
||||
done
|
||||
else
|
||||
if ! pulseaudio --check;then pulseaudio --start;fi
|
||||
|
||||
+92
-77
@@ -2,6 +2,36 @@
|
||||
error_reporting(E_ERROR);
|
||||
ini_set('display_errors',1);
|
||||
|
||||
$user = shell_exec("awk -F: '/1000/{print $1}' /etc/passwd");
|
||||
$home = shell_exec("awk -F: '/1000/{print $6}' /etc/passwd");
|
||||
$home = trim($home);
|
||||
if (file_exists($home.'/BirdNET-Pi/scripts/thisrun.txt')) {
|
||||
$config = parse_ini_file($home.'/BirdNET-Pi/scripts/thisrun.txt');
|
||||
} elseif (file_exists($home.'/BirdNET-Pi/scripts/thisrun.ini')) {
|
||||
$config = parse_ini_file($home.'/BirdNET-Pi/scripts/thisrun.ini');
|
||||
}
|
||||
if (file_exists($home."/BirdNET-Pi/apprise.txt")) {
|
||||
$apprise_config = file_get_contents($home."/BirdNET-Pi/apprise.txt");
|
||||
} else {
|
||||
$apprise_config = "";
|
||||
}
|
||||
$caddypwd = $config['CADDY_PWD'];
|
||||
if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
||||
header('WWW-Authenticate: Basic realm="My Realm"');
|
||||
header('HTTP/1.0 401 Unauthorized');
|
||||
echo '<table><tr><td>You cannot edit the settings for this installation</td></tr></table>';
|
||||
exit;
|
||||
} else {
|
||||
$submittedpwd = $_SERVER['PHP_AUTH_PW'];
|
||||
$submitteduser = $_SERVER['PHP_AUTH_USER'];
|
||||
if($submittedpwd !== $caddypwd || $submitteduser !== 'birdnet'){
|
||||
header('WWW-Authenticate: Basic realm="My Realm"');
|
||||
header('HTTP/1.0 401 Unauthorized');
|
||||
echo '<table><tr><td>You cannot edit the settings for this installation</td></tr></table>';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
function syslog_shell_exec($cmd, $sudo_user = null) {
|
||||
if ($sudo_user) {
|
||||
$cmd = "sudo -u $sudo_user $cmd";
|
||||
@@ -53,6 +83,8 @@ if(isset($_GET["latitude"])){
|
||||
$timezone = $_GET["timezone"];
|
||||
$model = $_GET["model"];
|
||||
$sf_thresh = $_GET["sf_thresh"];
|
||||
$only_notify_species_names = $_GET['only_notify_species_names'];
|
||||
$only_notify_species_names_2 = $_GET['only_notify_species_names_2'];
|
||||
|
||||
if(isset($_GET['apprise_notify_each_detection'])) {
|
||||
$apprise_notify_each_detection = 1;
|
||||
@@ -75,13 +107,13 @@ if(isset($_GET["latitude"])){
|
||||
$apprise_weekly_report = 0;
|
||||
}
|
||||
|
||||
if(isset($timezone)) {
|
||||
if(isset($timezone) && in_array($timezone, DateTimeZone::listIdentifiers())) {
|
||||
shell_exec("sudo timedatectl set-timezone ".$timezone);
|
||||
date_default_timezone_set($timezone);
|
||||
echo "<script>setTimeout(
|
||||
function() {
|
||||
const xhttp = new XMLHttpRequest();
|
||||
xhttp.open(\"GET\", \"scripts/config.php?restart_php=true\", true);
|
||||
xhttp.open(\"GET\", \"./config.php?restart_php=true\", true);
|
||||
xhttp.send();
|
||||
}, 1000);</script>";
|
||||
}
|
||||
@@ -91,7 +123,11 @@ if(isset($_GET["latitude"])){
|
||||
// can't set the date manually if it's getting it from the internet, disable ntp
|
||||
exec("sudo timedatectl set-ntp false");
|
||||
|
||||
// check if valid date and time
|
||||
$datetime = DateTime::createFromFormat('Y-m-d H:i', $_GET['date'] . ' ' . $_GET['time']);
|
||||
if ($datetime && $datetime->format('Y-m-d H:i') === $_GET['date'] . ' ' . $_GET['time']) {
|
||||
exec("sudo date -s '".$_GET['date']." ".$_GET['time']."'");
|
||||
}
|
||||
} else {
|
||||
// user checked 'use time from internet if available,' so make sure that's on
|
||||
if(strlen(trim(exec("sudo timedatectl | grep \"NTP service: active\""))) == 0){
|
||||
@@ -100,40 +136,29 @@ if(isset($_GET["latitude"])){
|
||||
}
|
||||
}
|
||||
|
||||
// Update Language settings only if a change is requested
|
||||
if (file_exists('./scripts/thisrun.txt')) {
|
||||
$lang_config = parse_ini_file('./scripts/thisrun.txt');
|
||||
} elseif (file_exists('./scripts/firstrun.ini')) {
|
||||
$lang_config = parse_ini_file('./scripts/firstrun.ini');
|
||||
if (file_exists($home.'/BirdNET-Pi/scripts/thisrun.txt')) {
|
||||
$lang_config = parse_ini_file($home.'/BirdNET-Pi/scripts/thisrun.txt');
|
||||
} elseif (file_exists($home.'/BirdNET-Pi/scripts/thisrun.ini')) {
|
||||
$lang_config = parse_ini_file($home.'/BirdNET-Pi/scripts/thisrun.ini');
|
||||
}
|
||||
if ($language != $lang_config['DATABASE_LANG']){
|
||||
|
||||
if ($model != $lang_config['MODEL'] || $language != $lang_config['DATABASE_LANG']){
|
||||
if(strlen($language) == 2){
|
||||
$user = trim(shell_exec("awk -F: '/1000/{print $1}' /etc/passwd"));
|
||||
$home = trim(shell_exec("awk -F: '/1000/{print $6}' /etc/passwd"));
|
||||
|
||||
// Archive old language file
|
||||
syslog_shell_exec("cp -f $home/BirdNET-Pi/model/labels.txt $home/BirdNET-Pi/model/labels.txt.old", $user);
|
||||
|
||||
if($model == "BirdNET_GLOBAL_3K_V2.3_Model_FP16"){
|
||||
// Install new language label file
|
||||
syslog_shell_exec("$home/BirdNET-Pi/scripts/install_language_label.sh -l $language", $user);
|
||||
|
||||
syslog(LOG_INFO, "Successfully changed language to '$language'");
|
||||
}
|
||||
|
||||
if ($model != $lang_config['MODEL']){
|
||||
$user = trim(shell_exec("awk -F: '/1000/{print $1}' /etc/passwd"));
|
||||
$home = trim(shell_exec("awk -F: '/1000/{print $6}' /etc/passwd"));
|
||||
|
||||
// Archive old language file
|
||||
syslog_shell_exec("cp -f $home/BirdNET-Pi/model/labels.txt $home/BirdNET-Pi/model/labels.txt.old", $user);
|
||||
|
||||
if($model == "BirdNET_GLOBAL_3K_V2.2_Model_FP16"){
|
||||
// Install new language label file
|
||||
syslog_shell_exec("sudo chmod +x $home/BirdNET-Pi/scripts/install_language_label_nm.sh && $home/BirdNET-Pi/scripts/install_language_label_nm.sh -l $language", $user);
|
||||
syslog_shell_exec("chmod +x $home/BirdNET-Pi/scripts/install_language_label_nm.sh && $home/BirdNET-Pi/scripts/install_language_label_nm.sh -l $language", $user);
|
||||
} else {
|
||||
syslog_shell_exec("$home/BirdNET-Pi/scripts/install_language_label.sh -l $language", $user);
|
||||
}
|
||||
|
||||
syslog(LOG_INFO, "Successfully changed language to '$language'");
|
||||
syslog(LOG_INFO, "Successfully changed language to '$language' and model to '$model'");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -154,8 +179,10 @@ if(isset($_GET["latitude"])){
|
||||
$contents = preg_replace("/APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES=.*/", "APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES=$minimum_time_limit", $contents);
|
||||
$contents = preg_replace("/MODEL=.*/", "MODEL=$model", $contents);
|
||||
$contents = preg_replace("/SF_THRESH=.*/", "SF_THRESH=$sf_thresh", $contents);
|
||||
$contents = preg_replace("/APPRISE_ONLY_NOTIFY_SPECIES_NAMES=.*/", "APPRISE_ONLY_NOTIFY_SPECIES_NAMES=\"$only_notify_species_names\"", $contents);
|
||||
$contents = preg_replace("/APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2=.*/", "APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2=\"$only_notify_species_names_2\"", $contents);
|
||||
|
||||
$contents2 = file_get_contents("./scripts/thisrun.txt");
|
||||
$contents2 = file_get_contents($home."/BirdNET-Pi/scripts/thisrun.txt");
|
||||
$contents2 = preg_replace("/SITE_NAME=.*/", "SITE_NAME=\"$site_name\"", $contents2);
|
||||
$contents2 = preg_replace("/LATITUDE=.*/", "LATITUDE=$latitude", $contents2);
|
||||
$contents2 = preg_replace("/LONGITUDE=.*/", "LONGITUDE=$longitude", $contents2);
|
||||
@@ -172,6 +199,8 @@ if(isset($_GET["latitude"])){
|
||||
$contents2 = preg_replace("/APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES=.*/", "APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES=$minimum_time_limit", $contents2);
|
||||
$contents2 = preg_replace("/MODEL=.*/", "MODEL=$model", $contents2);
|
||||
$contents2 = preg_replace("/SF_THRESH=.*/", "SF_THRESH=$sf_thresh", $contents2);
|
||||
$contents2 = preg_replace("/APPRISE_ONLY_NOTIFY_SPECIES_NAMES=.*/", "APPRISE_ONLY_NOTIFY_SPECIES_NAMES=\"$only_notify_species_names\"", $contents2);
|
||||
$contents2 = preg_replace("/APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2=.*/", "APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2=\"$only_notify_species_names_2\"", $contents2);
|
||||
|
||||
|
||||
|
||||
@@ -183,7 +212,7 @@ if(isset($_GET["latitude"])){
|
||||
}
|
||||
|
||||
$fh = fopen("/etc/birdnet/birdnet.conf", "w");
|
||||
$fh2 = fopen("./scripts/thisrun.txt", "w");
|
||||
$fh2 = fopen($home."/BirdNET-Pi/scripts/thisrun.txt", "w");
|
||||
fwrite($fh, $contents);
|
||||
fwrite($fh2, $contents2);
|
||||
|
||||
@@ -201,18 +230,17 @@ if(isset($_GET["latitude"])){
|
||||
}
|
||||
|
||||
if(isset($_GET['sendtest']) && $_GET['sendtest'] == "true") {
|
||||
$db = new SQLite3('./birds.db', SQLITE3_OPEN_CREATE | SQLITE3_OPEN_READWRITE);
|
||||
|
||||
$user = shell_exec("awk -F: '/1000/{print $1}' /etc/passwd");
|
||||
$home = shell_exec("awk -F: '/1000/{print $6}' /etc/passwd");
|
||||
$home = trim($home);
|
||||
|
||||
if (file_exists('./thisrun.txt')) {
|
||||
$config = parse_ini_file('./thisrun.txt');
|
||||
} elseif (file_exists('./firstrun.ini')) {
|
||||
$config = parse_ini_file('./firstrun.ini');
|
||||
if (file_exists($home.'/BirdNET-Pi/scripts/thisrun.txt')) {
|
||||
$config = parse_ini_file($home.'/BirdNET-Pi/scripts/thisrun.txt');
|
||||
} elseif (file_exists($home.'/BirdNET-Pi/scripts/thisrun.ini')) {
|
||||
$config = parse_ini_file($home.'/BirdNET-Pi/scripts/thisrun.ini');
|
||||
}
|
||||
|
||||
$db = new SQLite3($home."/BirdNET-Pi/scripts/birds.db", SQLITE3_OPEN_CREATE | SQLITE3_OPEN_READWRITE);
|
||||
|
||||
$cf = explode("\n",$_GET['apprise_config']);
|
||||
$cf = "'".implode("' '", $cf)."'";
|
||||
|
||||
@@ -280,11 +308,17 @@ if(isset($_GET['sendtest']) && $_GET['sendtest'] == "true") {
|
||||
$body = str_replace("\$overlap", $overlap, $body);
|
||||
$body = str_replace("\$flickrimage", $exampleimage, $body);
|
||||
|
||||
echo "<pre class=\"bash\">".shell_exec($home."/BirdNET-Pi/birdnet/bin/apprise -vv --plugin-path ".$home."/.apprise/plugins "." -t '".$title."' -b '".$body."' ".$attach." ".$cf." ")."</pre>";
|
||||
echo "<pre class=\"bash\">".shell_exec($home."/BirdNET-Pi/birdnet/bin/apprise -vv --plugin-path ".$home."/.apprise/plugins "." -t '".escapeshellcmd($title)."' -b '".escapeshellcmd($body)."' ".$attach." ".$cf." ")."</pre>";
|
||||
|
||||
die();
|
||||
}
|
||||
|
||||
// have to get the config again after we change the variables, so the UI reflects the changes too
|
||||
if (file_exists($home.'/BirdNET-Pi/scripts/thisrun.txt')) {
|
||||
$config = parse_ini_file($home.'/BirdNET-Pi/scripts/thisrun.txt');
|
||||
} elseif (file_exists($home.'/BirdNET-Pi/scripts/thisrun.ini')) {
|
||||
$config = parse_ini_file($home.'/BirdNET-Pi/scripts/thisrun.ini');
|
||||
}
|
||||
?>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
@@ -293,42 +327,12 @@ if(isset($_GET['sendtest']) && $_GET['sendtest'] == "true") {
|
||||
<div class="settings">
|
||||
<div class="brbanner"><h1>Basic Settings</h1></div><br>
|
||||
<form id="basicform" action="" method="GET">
|
||||
<?php
|
||||
if (file_exists('./scripts/thisrun.txt')) {
|
||||
$config = parse_ini_file('./scripts/thisrun.txt');
|
||||
} elseif (file_exists('./scripts/firstrun.ini')) {
|
||||
$config = parse_ini_file('./scripts/firstrun.ini');
|
||||
}
|
||||
$user = shell_exec("awk -F: '/1000/{print $1}' /etc/passwd");
|
||||
$home = shell_exec("awk -F: '/1000/{print $6}' /etc/passwd");
|
||||
$home = trim($home);
|
||||
if (file_exists($home."/BirdNET-Pi/apprise.txt")) {
|
||||
$apprise_config = file_get_contents($home."/BirdNET-Pi/apprise.txt");
|
||||
} else {
|
||||
$apprise_config = "";
|
||||
}
|
||||
$caddypwd = $config['CADDY_PWD'];
|
||||
if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
||||
header('WWW-Authenticate: Basic realm="My Realm"');
|
||||
header('HTTP/1.0 401 Unauthorized');
|
||||
echo '<table><tr><td>You cannot edit the settings for this installation</td></tr></table>';
|
||||
exit;
|
||||
} else {
|
||||
$submittedpwd = $_SERVER['PHP_AUTH_PW'];
|
||||
$submitteduser = $_SERVER['PHP_AUTH_USER'];
|
||||
if($submittedpwd !== $caddypwd || $submitteduser !== 'birdnet'){
|
||||
header('WWW-Authenticate: Basic realm="My Realm"');
|
||||
header('HTTP/1.0 401 Unauthorized');
|
||||
echo '<table><tr><td>You cannot edit the settings for this installation</td></tr></table>';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('modelsel').addEventListener('change', function() {
|
||||
if(this.value == "BirdNET_GLOBAL_3K_V2.2_Model_FP16"){
|
||||
if(this.value == "BirdNET_GLOBAL_3K_V2.3_Model_FP16"){
|
||||
document.getElementById("soft").style.display="unset";
|
||||
} else {
|
||||
document.getElementById("soft").style.display="none";
|
||||
@@ -360,7 +364,7 @@ function sendTestNotification(e) {
|
||||
<label for="model">Select a Model: </label>
|
||||
<select id="modelsel" name="model">
|
||||
<?php
|
||||
$models = array("BirdNET_6K_GLOBAL_MODEL", "BirdNET_GLOBAL_3K_V2.2_Model_FP16");
|
||||
$models = array("BirdNET_6K_GLOBAL_MODEL", "BirdNET_GLOBAL_3K_V2.3_Model_FP16");
|
||||
foreach($models as $modelName){
|
||||
$isSelected = "";
|
||||
if($config['MODEL'] == $modelName){
|
||||
@@ -375,7 +379,8 @@ function sendTestNotification(e) {
|
||||
<span <?php if($config['MODEL'] == "BirdNET_6K_GLOBAL_MODEL") { ?>style="display: none"<?php } ?> id="soft">
|
||||
<label for="sf_thresh">Species Occurence Frequency Threshold [0.0005, 0.99]: </label>
|
||||
<input name="sf_thresh" type="number" max="0.99" min="0.0005" step="any" value="<?php print($config['SF_THRESH']);?>"/> <span onclick="document.getElementById('sfhelp').style.display='unset'" style="text-decoration:underline;cursor:pointer">[more info]</span><br>
|
||||
<p id="sfhelp" style='display:none'>This value is used by the model to constrain the list of possible species that it will try to detect, given the minimum occurence frequency. A 0.03 threshold means that for a species to be included in this list, it needs to, on average, be seen on at least 3% of historically submitted eBird checklists for your given lat/lon/current week of year. So, the lower the threshold, the rarer the species it will include.<br><br>If you'd like to tinker with this threshold value and see which species make it onto the list, <?php if($config['MODEL'] == "BirdNET_6K_GLOBAL_MODEL"){ ?>please click "Update Settings" at the very bottom of this page to install the appropriate label file, then come back here and you'll be able to use the Species List Tester.<?php } else { ?>you can use this tool: <button type="button" class="testbtn" id="openModal">Species List Tester</button><?php } ?></p>
|
||||
<p id="sfhelp" style='display:none'>This value is used by the model to constrain the list of possible species that it will try to detect, given the minimum occurence frequency. A 0.03 threshold means that for a species to be included in this list, it needs to, on average, be seen on at least 3% of historically submitted eBird checklists for your given lat/lon/current week of year. So, the lower the threshold, the rarer the species it will include.<br><img style='width:75%;padding-top:5px;padding-bottom:5px' alt="BirdNET-Pi new model detection flowchart" title="BirdNET-Pi new model detection flowchart" src="https://i.imgur.com/8YEAuSA.jpeg">
|
||||
<br>If you'd like to tinker with this threshold value and see which species make it onto the list, <?php if($config['MODEL'] == "BirdNET_6K_GLOBAL_MODEL"){ ?>please click "Update Settings" at the very bottom of this page to install the appropriate label file, then come back here and you'll be able to use the Species List Tester.<?php } else { ?>you can use this tool: <button type="button" class="testbtn" id="openModal">Species List Tester</button><?php } ?></p>
|
||||
</span>
|
||||
|
||||
<script src="static/dialog-polyfill.js"></script>
|
||||
@@ -474,9 +479,11 @@ function runProcess() {
|
||||
|
||||
<dl>
|
||||
<dt>BirdNET_6K_GLOBAL_MODEL (2020)</dt><br>
|
||||
<dd id="ddnewline">This model comes from BirdNET-Lite, with bird sound recognition for more than 6,000 species worldwide. This is the default option and will generally work very well for most use cases.</dd>
|
||||
<dt>BirdNET_GLOBAL_3K_V2.2_Model_FP16 (2022)</dt><br>
|
||||
<dd id="ddnewline">This model comes from BirdNET-Analyzer, a newer work-in-progress project with aims to improve on the old model. Currently it only supports about 3,500 species worldwide, so for users in North America, this model is generally much more accurate than the above model, but elsewhere it will be less accurate and possibly useless.</dd>
|
||||
<dd id="ddnewline">This is the BirdNET-Lite model, with bird sound recognition for more than 6,000 species worldwide. This is the default option and will generally work very for people in most of the world.</dd>
|
||||
<br>
|
||||
<dt>BirdNET_GLOBAL_3K_V2.3_Model_FP16 (2023)</dt><br>
|
||||
<dd id="ddnewline">This is the BirdNET-Analyzer model, a newer work-in-progress project with aims to improve on the BirdNET-Lite model. Currently it only supports about 3,500 species worldwide, meaning for some regions (North America, Europe, Australia) it will usually outperform the BirdNET-Lite model, but for other regions it will be worse.</dd><br>
|
||||
<dt>[ In-depth technical write-up on the models <a target="_blank" href="https://github.com/mcguirepr89/BirdNET-Pi/wiki/BirdNET-Pi:-some-theory-on-classification-&-some-practical-hints">here</a> ]</dt>
|
||||
</dl>
|
||||
</td></tr></table><br>
|
||||
|
||||
@@ -484,7 +491,7 @@ function runProcess() {
|
||||
<h2>Location</h2>
|
||||
<label for="site_name">Site Name: </label>
|
||||
<input name="site_name" type="text" value="<?php print($config['SITE_NAME']);?>"/> (Optional)<br>
|
||||
<label for="latitude">Latitude: </label>
|
||||
<label for="latitude">Latitude: </label>
|
||||
<input name="latitude" type="number" max="90" min="-90" step="0.0001" value="<?php print($config['LATITUDE']);?>" required/><br>
|
||||
<label for="longitude">Longitude: </label>
|
||||
<input name="longitude" type="number" max="180" min="-180" step="0.0001" value="<?php print($config['LONGITUDE']);?>" required/><br>
|
||||
@@ -494,7 +501,7 @@ function runProcess() {
|
||||
<h2>BirdWeather</h2>
|
||||
<label for="birdweather_id">BirdWeather ID: </label>
|
||||
<input name="birdweather_id" type="text" value="<?php print($config['BIRDWEATHER_ID']);?>" /><br>
|
||||
<p><a href="https://app.birdweather.com" target="_blank">BirdWeather.com</a> is a weather map for bird sounds. Stations around the world supply audio and video streams to BirdWeather where they are then analyzed by BirdNET and compared to eBird Grid data. BirdWeather catalogues the bird audio and spectrogram visualizations so that you can listen to, view, and read about birds throughout the world. <a href="mailto:tim@birdweather.com?subject=Request%20BirdWeather%20ID&body=<?php include('./scripts/birdweather_request.php'); ?>" target="_blank">Email Tim</a> to request a BirdWeather ID</p>
|
||||
<p><a href="https://app.birdweather.com" target="_blank">BirdWeather.com</a> is a weather map for bird sounds. Stations around the world supply audio and video streams to BirdWeather where they are then analyzed by BirdNET and compared to eBird Grid data. BirdWeather catalogues the bird audio and spectrogram visualizations so that you can listen to, view, and read about birds throughout the world. <a href="mailto:tim@birdweather.com?subject=Request%20BirdWeather%20ID&body=<?php include($home.'/BirdNET-Pi/scripts/birdweather_request.php'); ?>" target="_blank">Email Tim</a> to request a BirdWeather ID</p>
|
||||
</td></tr></table><br>
|
||||
<table class="settingstable" style="width:100%"><tr><td>
|
||||
<h2>Notifications</h2>
|
||||
@@ -547,11 +554,15 @@ https://discordapp.com/api/webhooks/{WebhookID}/{WebhookToken}
|
||||
<input type="checkbox" name="apprise_notify_each_detection" <?php if($config['APPRISE_NOTIFY_EACH_DETECTION'] == 1 && filesize($home."/BirdNET-Pi/apprise.txt") != 0) { echo "checked"; };?> >
|
||||
<label for="apprise_weekly_report">Notify each new detection</label><br>
|
||||
<input type="checkbox" name="apprise_weekly_report" <?php if($config['APPRISE_WEEKLY_REPORT'] == 1 && filesize($home."/BirdNET-Pi/apprise.txt") != 0) { echo "checked"; };?> >
|
||||
<label for="apprise_weekly_report">Send weekly report</label><br>
|
||||
<label for="apprise_weekly_report">Send <a href="views.php?view=Weekly%20Report"> weekly report</a></label><br>
|
||||
|
||||
<hr>
|
||||
<label for="quantity">Minimum time between notifications of the same species (sec):</label>
|
||||
<label for="minimum_time_limit">Minimum time between notifications of the same species (sec):</label>
|
||||
<input type="number" id="minimum_time_limit" name="minimum_time_limit" value="<?php echo $config['APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES'];?>" min="0"><br>
|
||||
<label for="only_notify_species_names">Exclude these species (comma separated common names):</label>
|
||||
<input type="text" id="only_notify_species_names" placeholder="Mourning Dove,American Crow" name="only_notify_species_names" value="<?php echo $config['APPRISE_ONLY_NOTIFY_SPECIES_NAMES'];?>" size=96><br>
|
||||
<label for="only_notify_species_names_2">ONLY notify for these species (comma separated common names):</label>
|
||||
<input type="text" id="only_notify_species_names_2" placeholder="Northern Cardinal,Carolina Chickadee,Eastern Bluebird" name="only_notify_species_names_2" value="<?php echo $config['APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2'];?>" size=96><br>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -647,8 +658,12 @@ https://discordapp.com/api/webhooks/{WebhookID}/{WebhookToken}
|
||||
<h2>Time and Date</h2>
|
||||
<span>If connected to the internet, retrieve time automatically?</span>
|
||||
<input type="checkbox" onchange='handleChange(this)' <?php echo $checkedvalue; ?> ><br>
|
||||
<input onclick="this.showPicker()" type="date" id="date" name="date" value="<?php echo date('Y-m-d') ?>" <?php echo $disabledvalue; ?>>
|
||||
<input onclick="this.showPicker()" type="time" id="time" name="time" value="<?php echo date('H:i') ?>" <?php echo $disabledvalue; ?>><br>
|
||||
<?php
|
||||
$date = new DateTime('now');
|
||||
?>
|
||||
<input onclick="this.showPicker()" type="date" id="date" name="date" value="<?php echo $date->format('Y-m-d') ?>" <?php echo $disabledvalue; ?>>
|
||||
<input onclick="this.showPicker()" type="time" id="time" name="time" value="<?php echo $date->format('H:i'); ?>" <?php echo $disabledvalue; ?>><br>
|
||||
<br>
|
||||
<label for="timezone">Select a Timezone: </label>
|
||||
<select name="timezone">
|
||||
<option disabled selected>
|
||||
|
||||
+16
-6
@@ -82,24 +82,25 @@ plot.set(xlabel="Detections")
|
||||
|
||||
|
||||
# Generate crosstab matrix for heatmap plot
|
||||
|
||||
heat = pd.crosstab(df_plt_top10_today['Com_Name'], df_plt_top10_today['Hour of Day'])
|
||||
|
||||
# Order heatmap Birds by frequency of occurrance
|
||||
heat.index = pd.CategoricalIndex(heat.index, categories=freq_order)
|
||||
heat.sort_index(level=0, inplace=True)
|
||||
|
||||
|
||||
hours_in_day = pd.Series(data=range(0, 24))
|
||||
heat_frame = pd.DataFrame(data=0, index=heat.index, columns=hours_in_day)
|
||||
heat = (heat + heat_frame).fillna(0)
|
||||
|
||||
# Generatie heatmap plot
|
||||
# Get current hour
|
||||
current_hour = now.hour
|
||||
|
||||
# Generate heatmap plot
|
||||
plot = sns.heatmap(
|
||||
heat,
|
||||
norm=LogNorm(),
|
||||
annot=True,
|
||||
annot_kws={
|
||||
"fontsize": 7},
|
||||
annot_kws={"fontsize": 7},
|
||||
fmt="g",
|
||||
cmap=pal,
|
||||
square=False,
|
||||
@@ -107,9 +108,18 @@ plot = sns.heatmap(
|
||||
linewidths=0.5,
|
||||
linecolor="Grey",
|
||||
ax=axs[1],
|
||||
yticklabels=False)
|
||||
yticklabels=False
|
||||
)
|
||||
|
||||
# Set color and weight of tick label for current hour
|
||||
for label in plot.get_xticklabels():
|
||||
if int(label.get_text()) == current_hour:
|
||||
label.set_color('yellow')
|
||||
|
||||
plot.set_xticklabels(plot.get_xticklabels(), rotation=0, size=7)
|
||||
|
||||
|
||||
|
||||
# Set heatmap border
|
||||
for _, spine in plot.spines.items():
|
||||
spine.set_visible(True)
|
||||
|
||||
@@ -4,7 +4,9 @@ Below is the information I would like to use to request a BirdWeather ID
|
||||
%0A%0A
|
||||
Latitude={{ LATITUDE }}%0A
|
||||
Longitude={{ LONGITUDE }}%0A
|
||||
Location= [ Create a location name ]%0A
|
||||
City= [ City ]%0A
|
||||
State= [ State ]%0A
|
||||
Country= [ Country ]%0A
|
||||
%0A%0A
|
||||
Thank you so much!%0A
|
||||
[ Your Name Here ]
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
<div class="customlabels column1">
|
||||
<form action="" method="GET" id="add">
|
||||
<h3>All Species Labels</h3>
|
||||
<input autocomplete="off" size="18" type="text" placeholder="Search Species..." id="exclude_species_searchterm" name="exclude_species_searchterm">
|
||||
<br>
|
||||
<span>Once the desired species has been highlighted, click it and then click ADD to have it excluded.</span>
|
||||
<select name="species[]" id="species" multiple size="auto">
|
||||
<option>Choose a species below to add to the Excluded Species List</option>
|
||||
<?php
|
||||
@@ -37,6 +40,7 @@
|
||||
<div class="customlabels column3">
|
||||
<form action="" method="GET" id="del">
|
||||
<h3>Excluded Species List</h3>
|
||||
<br><br>
|
||||
<select name="species[]" id="value2" multiple size="auto">
|
||||
<?php
|
||||
$filename = './scripts/exclude_species_list.txt';
|
||||
@@ -53,5 +57,85 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById("add").addEventListener("submit", function(event) {
|
||||
var speciesSelect = document.getElementById("species");
|
||||
if (speciesSelect.selectedIndex < 1) {
|
||||
alert("Please click the species you want to add.");
|
||||
document.querySelector('.views').style.opacity = 1;
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
var search_term = document.querySelector("input#exclude_species_searchterm");
|
||||
search_term.addEventListener("keydown", doSearch);
|
||||
//Index where we found a match
|
||||
var search_match_idx = 1;
|
||||
var last_search_term = "";
|
||||
|
||||
function doSearch(eventObj) {
|
||||
//Don't do anything if the user is till composing
|
||||
if (eventObj.isComposing || eventObj.keyCode === 229) {
|
||||
return;
|
||||
}
|
||||
|
||||
//If the key pressed is the enter key capture it, stop the form submitting and do the search
|
||||
if (eventObj.key === 'Enter' || eventObj.keyCode === 13) {
|
||||
eventObj.preventDefault();
|
||||
|
||||
//User wants to submit the text as a search
|
||||
var search_text = search_term.value.toLowerCase();
|
||||
|
||||
//Now look at the select list, loop over the options and try find part of the text in the option's name/text
|
||||
var species_select_list = document.querySelector('select#species');
|
||||
|
||||
//if the search text differs from last time start the search from the beginning of te list
|
||||
if (search_text !== last_search_term) {
|
||||
//Also unselect the last match
|
||||
species_select_list[search_match_idx].removeAttribute('class');
|
||||
search_match_idx = 1;
|
||||
}
|
||||
|
||||
//Start the loop at 1 so we skip the very initial value asking the user to select a option
|
||||
for (let i = search_match_idx; i < species_select_list.length; i++) {
|
||||
// if (species_select_list[i] !== 'undefined') {
|
||||
option_text = species_select_list[i].value;
|
||||
search_match_text = option_text.toLowerCase().includes(search_text)
|
||||
|
||||
//Check if the item is already selected, that could mean that user may be searching the same phrase
|
||||
if (species_select_list[search_match_idx].getAttribute('class') === "exclude_species_list_option_highlight") {
|
||||
species_select_list[search_match_idx].removeAttribute('class');
|
||||
// species_select_list[search_match_idx].removeAttribute('style');
|
||||
//Go to the next item
|
||||
i++
|
||||
continue;
|
||||
}
|
||||
|
||||
//There was a match,
|
||||
if (search_match_text === true) {
|
||||
//already on this item so skip it and continue with list
|
||||
if (search_match_idx === i) {
|
||||
i++
|
||||
continue;
|
||||
}
|
||||
//Finally we havent found this item before
|
||||
search_match_idx = i;
|
||||
|
||||
//Scroll into view and select it
|
||||
species_select_list[search_match_idx].scrollIntoView({behavior: 'smooth', block: 'start'});
|
||||
//Apply a style to the option since setting the selected value to true breaks scrolling on chrome :(
|
||||
//the style is nicer anyway :)
|
||||
species_select_list[search_match_idx].setAttribute('class', "exclude_species_list_option_highlight");
|
||||
// species_select_list[search_match_idx].setAttribute('selected', 'true');
|
||||
|
||||
//break the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
//Track what search term was used so we know when to start over
|
||||
last_search_term = search_text
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
+25
-22
@@ -4,63 +4,66 @@
|
||||
echo "........................................IPs....................................."
|
||||
echo "LAN IP: $(hostname -I|cut -d' ' -f1)"
|
||||
echo "Public IP: $(curl -s4 ifconfig.co)"
|
||||
echo "..................................\`vcgencmd stats\`.............................."
|
||||
sudo -u$USER vcgencmd get_throttled
|
||||
hex=$(sudo -u$USER vcgencmd get_throttled|cut -d'x' -f2)
|
||||
binary=$(echo "ibase=16;obase=2;$hex"|bc)
|
||||
echo "Binary: $binary";
|
||||
revbinary=$(echo $binary|rev)
|
||||
if echo $binary | grep 1 ;then
|
||||
if ! dpkg -l | grep -q vcgencmd ; then
|
||||
echo "..................................\`vcgencmd stats\`.............................."
|
||||
sudo -u$USER vcgencmd get_throttled
|
||||
hex=$(sudo -u$USER vcgencmd get_throttled | cut -d'x' -f2)
|
||||
binary=$(echo "ibase=16;obase=2;$hex" | bc)
|
||||
echo "Binary: $binary"
|
||||
revbinary=$(echo $binary | rev)
|
||||
if echo $binary | grep 1; then
|
||||
echo "ISSUES DETECTED"
|
||||
if [ ${revbinary:0:1} -eq 1 &> /dev/null ];then
|
||||
if [ ${revbinary:0:1} -eq 1 ] &>/dev/null; then
|
||||
message="Under-voltage detected"
|
||||
echo "$message"
|
||||
dmesg -H | grep -i voltage
|
||||
fi
|
||||
if [ ${revbinary:1:1} -eq 1 &> /dev/null ];then
|
||||
if [ ${revbinary:1:1} -eq 1 ] &>/dev/null; then
|
||||
message="Arm frequency capped"
|
||||
echo "$message"
|
||||
dmesg -H | grep -i frequen
|
||||
fi
|
||||
if [ ${revbinary:2:1} -eq 1 &> /dev/null ];then
|
||||
if [ ${revbinary:2:1} -eq 1 ] &>/dev/null; then
|
||||
message="Currently Throttled"
|
||||
echo "$message"
|
||||
dmesg -H | grep -i throttl
|
||||
fi
|
||||
if [ ${revbinary:3:1} -eq 1 &> /dev/null ];then
|
||||
if [ ${revbinary:3:1} -eq 1 ] &>/dev/null; then
|
||||
message="Soft temperatue limit active"
|
||||
echo "$message"
|
||||
dmesg -H | grep -i temperature
|
||||
fi
|
||||
if [ ${revbinary:16:1} -eq 1 &> /dev/null ];then
|
||||
if [ ${revbinary:16:1} -eq 1 ] &>/dev/null; then
|
||||
message="Under-voltage has occurred"
|
||||
echo "$message"
|
||||
dmesg -H | grep -i voltage
|
||||
fi
|
||||
if [ ${revbinary:17:1} -eq 1 &> /dev/null ];then
|
||||
if [ ${revbinary:17:1} -eq 1 ] &>/dev/null; then
|
||||
message="Arm frequency capping has occurred"
|
||||
echo "$message"
|
||||
dmesg -H | grep -i frequen
|
||||
fi
|
||||
if [ ${revbinary:18:1} -eq 1 &> /dev/null ];then
|
||||
if [ ${revbinary:18:1} -eq 1 ] &>/dev/null; then
|
||||
message="Throttling has occurred"
|
||||
echo "$message"
|
||||
dmesg -H | grep -i throttl
|
||||
fi
|
||||
if [ ${revbinary:19:1} -eq 1 &> /dev/null ];then
|
||||
if [ ${revbinary:19:1} -eq 1 ] &>/dev/null; then
|
||||
message="Soft temperature limit has occurred"
|
||||
echo "$message"
|
||||
dmesg -H | grep -i temperature
|
||||
fi
|
||||
fi
|
||||
echo "....................................Clock Speeds................................"
|
||||
for i in arm core h264 isp v3d uart pwm emmc pixel vec hdmi dpi;do
|
||||
fi
|
||||
|
||||
echo "....................................Clock Speeds................................"
|
||||
for i in arm core h264 isp v3d uart pwm emmc pixel vec hdmi dpi; do
|
||||
echo -e "${i}:\t$(sudo -u$USER vcgencmd measure_clock ${i})"
|
||||
done
|
||||
echo "........................................Volts..................................."
|
||||
for i in core sdram_c sdram_i sdram_p;do
|
||||
done
|
||||
echo "........................................Volts..................................."
|
||||
for i in core sdram_c sdram_i sdram_p; do
|
||||
echo -e "${i}:\t$(sudo -u$USER vcgencmd measure_volts ${i})"
|
||||
done
|
||||
done
|
||||
fi
|
||||
echo ".....................................Caddyfile.................................."
|
||||
cat /etc/caddy/Caddyfile
|
||||
echo ".................................... Crontab...................................."
|
||||
|
||||
@@ -118,11 +118,22 @@ for h in "${SCAN_DIRS[@]}";do
|
||||
sox -V1 "${h}/${OLDFILE}" "${NEWSPECIES_BYDATE}/${NEWFILE}" \
|
||||
trim ="${START}" ="${END}"
|
||||
|
||||
# Create spectrogram for extraction
|
||||
RAW_SPECTROGRAM=${RAW_SPECTROGRAM}
|
||||
# Check if RAW_SPECTROGRAM is 1
|
||||
if [ "$RAW_SPECTROGRAM" == "1" ]; then
|
||||
# If it is, add "-r" as an argument to the SOX command
|
||||
sox -V1 "${NEWSPECIES_BYDATE}/${NEWFILE}" -n remix 1 rate 24k spectrogram \
|
||||
-t "${COMMON_NAME}" \
|
||||
-c "${NEWSPECIES_BYDATE//$HOME\/}/${NEWFILE}" \
|
||||
-o "${NEWSPECIES_BYDATE}/${NEWFILE}.png" \
|
||||
-r
|
||||
else
|
||||
# If it's not, run the SOX command without the "-r" argument
|
||||
sox -V1 "${NEWSPECIES_BYDATE}/${NEWFILE}" -n remix 1 rate 24k spectrogram \
|
||||
-t "$(echo "${COMMON_NAME}" | iconv -f utf8 -t ascii//TRANSLIT)" \
|
||||
-c "${NEWSPECIES_BYDATE//$HOME\/}/${NEWFILE}" \
|
||||
-o "${NEWSPECIES_BYDATE}/${NEWFILE}.png"
|
||||
fi
|
||||
|
||||
done < "${TMPFILE}"
|
||||
|
||||
|
||||
+9
-2
@@ -1,4 +1,9 @@
|
||||
<?php
|
||||
|
||||
/* Prevent XSS input */
|
||||
$_GET = filter_input_array(INPUT_GET, FILTER_SANITIZE_STRING);
|
||||
$_POST = filter_input_array(INPUT_POST, FILTER_SANITIZE_STRING);
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors',1);
|
||||
ini_set('display_startup_errors',1);
|
||||
@@ -148,14 +153,16 @@ function submitID() {
|
||||
</table>
|
||||
<?php // <br><button type="button" onclick="showDialog()">Export as CSV for eBird</button><br><br> ?>
|
||||
<?php
|
||||
$time = time();
|
||||
|
||||
if (file_exists('./Charts/'.$chart)) {
|
||||
echo "<img src=\"/Charts/$chart?nocache=time()\" >";
|
||||
echo "<img src=\"/Charts/$chart?nocache=$time\" >";
|
||||
} else {
|
||||
echo "<p>No Charts for $theDate</p>";
|
||||
}
|
||||
echo "<hr>";
|
||||
if (file_exists('./Charts/'.$chart2)) {
|
||||
echo "<img src=\"/Charts/$chart2?nocache=time()\">";
|
||||
echo "<img src=\"/Charts/$chart2?nocache=$time\">";
|
||||
} else {
|
||||
echo "<p>No Charts For $theDate</p>";
|
||||
}?>
|
||||
|
||||
@@ -8,8 +8,8 @@ echo "Beginning $0"
|
||||
birdnet_conf=$my_dir/birdnet.conf
|
||||
|
||||
# Retrieve latitude and longitude from web
|
||||
LATITUDE=$(curl -s4 ifconfig.co/json | jq .latitude)
|
||||
LONGITUDE=$(curl -s4 ifconfig.co/json | jq .longitude)
|
||||
LATITUDE=$(curl -s4 http://ip-api.com/json?fields=lat,lon | jq .lat)
|
||||
LONGITUDE=$(curl -s4 http://ip-api.com/json?fields=lat,lon | jq .lon)
|
||||
|
||||
# Define regular expression pattern
|
||||
pattern='^[+-]?[0-9]{2}\.[0-9]{4}$'
|
||||
@@ -99,12 +99,14 @@ RTSP_STREAM=
|
||||
#----------------------- Apprise Miscellanous Configuration -------------------#
|
||||
|
||||
APPRISE_NOTIFICATION_TITLE="New BirdNET-Pi Detection"
|
||||
APPRISE_NOTIFICATION_BODY="A \$sciname \$comname was just detected with a confidence of \$confidence"
|
||||
APPRISE_NOTIFICATION_BODY="A \$comname (\$sciname) was just detected with a confidence of \$confidence"
|
||||
APPRISE_NOTIFY_EACH_DETECTION=0
|
||||
APPRISE_NOTIFY_NEW_SPECIES=0
|
||||
APPRISE_WEEKLY_REPORT=1
|
||||
APPRISE_NOTIFY_NEW_SPECIES_EACH_DAY=0
|
||||
APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES=0
|
||||
APPRISE_ONLY_NOTIFY_SPECIES_NAMES=""
|
||||
APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2=""
|
||||
|
||||
#---------------------- Flickr Images API Configuration -----------------------#
|
||||
## If FLICKR_API_KEY is set, the web interface will try and display bird images
|
||||
@@ -167,6 +169,9 @@ FREQSHIFT_HI=6000
|
||||
## FREQSHIFT_LO
|
||||
FREQSHIFT_LO=3000
|
||||
|
||||
## FREQSHIFT_RECONNECT_DELAY
|
||||
FREQSHIFT_RECONNECT_DELAY=4000
|
||||
|
||||
## If the tool is sox, you have to define the pitch shift (amount of 100ths of semintone)
|
||||
## FREQSHIFT_PITCH
|
||||
FREQSHIFT_PITCH=-1500
|
||||
@@ -228,6 +233,18 @@ HEARTBEAT_URL=
|
||||
|
||||
SILENCE_UPDATE_INDICATOR=0
|
||||
|
||||
## RAW_SPECTROGRAM is for removing the axes and labels of the spectrograms
|
||||
## that are generated by Sox for each detection for a cleaner appearance.
|
||||
|
||||
RAW_SPECTROGRAM=0
|
||||
|
||||
## CUSTOM_IMAGE and CUSTOM_IMAGE_TITLE allow you to show a custom image on the
|
||||
## Overview page of your BirdNET-Pi. This can be used to show a dynamically
|
||||
## updating picture of your garden, for example.
|
||||
|
||||
CUSTOM_IMAGE=
|
||||
CUSTOM_IMAGE_TITLE=""
|
||||
|
||||
## These are just for debugging
|
||||
LAST_RUN=
|
||||
THIS_RUN=
|
||||
|
||||
+35
-2
@@ -2,14 +2,47 @@
|
||||
# Live Audio Stream Service Script
|
||||
source /etc/birdnet/birdnet.conf
|
||||
|
||||
# Read the logging level from the configuration option
|
||||
LOGGING_LEVEL="${LogLevel_LiveAudioStreamService}"
|
||||
# If empty for some reason default to log level of error
|
||||
[ -z $LOGGING_LEVEL ] && LOGGING_LEVEL='error'
|
||||
# Additionally if we're at debug or info level then allow printing of script commands and variables
|
||||
if [ "$LOGGING_LEVEL" == "info" ] || [ "$LOGGING_LEVEL" == "debug" ];then
|
||||
# Enable printing of commands/variables etc to terminal for debugging
|
||||
set -x
|
||||
fi
|
||||
|
||||
if [ "$ACTIVATE_FREQSHIFT_IN_LIVESTREAM" == "true" ]; then
|
||||
FREQSHIFT_OPT='-af rubberband=pitch='${FREQSHIFT_LO}'/'${FREQSHIFT_HI}
|
||||
fi
|
||||
|
||||
if [ -z ${REC_CARD} ];then
|
||||
echo "Stream not supported"
|
||||
elif [[ ! -z ${RTSP_STREAM} ]];then
|
||||
ffmpeg -nostdin -loglevel 32 -ac ${CHANNELS} -i ${RTSP_STREAM} -acodec libmp3lame \
|
||||
# Explode the RSPT steam setting into an array so we can count the number we have
|
||||
RSTP_STREAMS_EXPLODED_ARRAY=(${RTSP_STREAM//,/ })
|
||||
|
||||
# If for some reason the RTSP_STREAM_TO_LIVESTREAM is not set, then init it to 0 to use the first stream
|
||||
if [[ -z ${RTSP_STREAM_TO_LIVESTREAM} ]];then
|
||||
RTSP_STREAM_TO_LIVESTREAM=0
|
||||
fi
|
||||
|
||||
# Get the RSTP stream at the specified array index
|
||||
SELECTED_RSTP_STREAM=${RSTP_STREAMS_EXPLODED_ARRAY[RTSP_STREAM_TO_LIVESTREAM]}
|
||||
|
||||
# If for some reason the RTSP stream url is null
|
||||
if [[ -z ${SELECTED_RSTP_STREAM} ]];then
|
||||
# Try select the first stream
|
||||
SELECTED_RSTP_STREAM=${RSTP_STREAMS_EXPLODED_ARRAY[0]}
|
||||
fi
|
||||
|
||||
ffmpeg -nostdin -loglevel $LOGGING_LEVEL -ac ${CHANNELS} -i ${SELECTED_RSTP_STREAM} -acodec libmp3lame \
|
||||
-b:a 320k -ac ${CHANNELS} -content_type 'audio/mpeg' \
|
||||
${FREQSHIFT_OPT} \
|
||||
-f mp3 icecast://source:${ICE_PWD}@localhost:8000/stream -re
|
||||
else
|
||||
ffmpeg -nostdin -loglevel 32 -ac ${CHANNELS} -f alsa -i ${REC_CARD} -acodec libmp3lame \
|
||||
ffmpeg -nostdin -loglevel $LOGGING_LEVEL -ac ${CHANNELS} -f alsa -i ${REC_CARD} -acodec libmp3lame \
|
||||
-b:a 320k -ac ${CHANNELS} -content_type 'audio/mpeg' \
|
||||
${FREQSHIFT_OPT} \
|
||||
-f mp3 icecast://source:${ICE_PWD}@localhost:8000/stream -re
|
||||
fi
|
||||
|
||||
+296
-20
@@ -2,6 +2,7 @@
|
||||
error_reporting(E_ERROR);
|
||||
ini_set('display_errors',1);
|
||||
ini_set('session.gc_maxlifetime', 7200);
|
||||
ini_set('user_agent', 'PHP_Flickr/1.0');
|
||||
session_set_cookie_params(7200);
|
||||
session_start();
|
||||
$myDate = date('Y-m-d');
|
||||
@@ -23,6 +24,55 @@ $user = shell_exec("awk -F: '/1000/{print $1}' /etc/passwd");
|
||||
$home = shell_exec("awk -F: '/1000/{print $6}' /etc/passwd");
|
||||
$home = trim($home);
|
||||
|
||||
$statement2 = $db->prepare('SELECT COUNT(*) FROM detections WHERE Date == DATE(\'now\', \'localtime\')');
|
||||
if($statement2 == False) {
|
||||
echo "Database is busy";
|
||||
header("refresh: 0;");
|
||||
}
|
||||
$result2 = $statement2->execute();
|
||||
$todaycount = $result2->fetchArray(SQLITE3_ASSOC);
|
||||
|
||||
if(isset($_GET['custom_image'])){
|
||||
if(isset($config["CUSTOM_IMAGE"])) {
|
||||
?>
|
||||
<br>
|
||||
<h3><?php echo $config["CUSTOM_IMAGE_TITLE"]; ?></h3>
|
||||
<?php
|
||||
$image_data = file_get_contents($config["CUSTOM_IMAGE"]);
|
||||
$image_base64 = base64_encode($image_data);
|
||||
$img_tag = "<img src='data:image/png;base64," . $image_base64 . "'>";
|
||||
echo $img_tag;
|
||||
}
|
||||
die();
|
||||
}
|
||||
|
||||
if(isset($_GET['blacklistimage'])) {
|
||||
if(isset($_SERVER['PHP_AUTH_USER'])) {
|
||||
$submittedpwd = $_SERVER['PHP_AUTH_PW'];
|
||||
$submitteduser = $_SERVER['PHP_AUTH_USER'];
|
||||
if($submittedpwd == $config['CADDY_PWD'] && $submitteduser == 'birdnet'){
|
||||
|
||||
$imageid = $_GET['blacklistimage'];
|
||||
$file_handle = fopen($home."/BirdNET-Pi/scripts/blacklisted_images.txt", 'a+');
|
||||
fwrite($file_handle, $imageid . "\n");
|
||||
fclose($file_handle);
|
||||
unset($_SESSION['images']);
|
||||
die("OK");
|
||||
} else {
|
||||
header('WWW-Authenticate: Basic realm="My Realm"');
|
||||
header('HTTP/1.0 401 Unauthorized');
|
||||
echo 'You must be authenticated.';
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
header('WWW-Authenticate: Basic realm="My Realm"');
|
||||
header('HTTP/1.0 401 Unauthorized');
|
||||
echo 'You must be authenticated.';
|
||||
exit;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if(isset($_GET['fetch_chart_string']) && $_GET['fetch_chart_string'] == "true") {
|
||||
$myDate = date('Y-m-d');
|
||||
$chart = "Combo-$myDate.png";
|
||||
@@ -32,7 +82,7 @@ if(isset($_GET['fetch_chart_string']) && $_GET['fetch_chart_string'] == "true")
|
||||
|
||||
if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" && isset($_GET['previous_detection_identifier'])) {
|
||||
|
||||
$statement4 = $db->prepare('SELECT Com_Name, Sci_Name, Date, Time, Confidence, File_Name FROM detections ORDER BY Date DESC, Time DESC LIMIT 5');
|
||||
$statement4 = $db->prepare('SELECT Com_Name, Sci_Name, Date, Time, Confidence, File_Name FROM detections ORDER BY Date DESC, Time DESC LIMIT 15');
|
||||
if($statement4 == False) {
|
||||
echo "Database is busy";
|
||||
header("refresh: 0;");
|
||||
@@ -43,6 +93,7 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" && isse
|
||||
}
|
||||
$iterations = 0;
|
||||
$lines;
|
||||
$licenses_urls = array();
|
||||
// hopefully one of the 5 most recent detections has an image that is valid, we'll use that one as the most recent detection until the newer ones get their images created
|
||||
while($mostrecent = $result4->fetchArray(SQLITE3_ASSOC)) {
|
||||
$comname = preg_replace('/ /', '_', $mostrecent['Com_Name']);
|
||||
@@ -81,6 +132,19 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" && isse
|
||||
if($key !== false) {
|
||||
$image = $_SESSION['images'][$key];
|
||||
} else {
|
||||
// Get license information if we haven't already
|
||||
if (empty($licenses_urls)) {
|
||||
$licenses_url = "https://api.flickr.com/services/rest/?method=flickr.photos.licenses.getInfo&api_key=".$config["FLICKR_API_KEY"]."&format=json&nojsoncallback=1";
|
||||
$licenses_response = file_get_contents($licenses_url);
|
||||
$licenses_data = json_decode($licenses_response, true)["licenses"]["license"];
|
||||
foreach ($licenses_data as $license) {
|
||||
$license_id = $license["id"];
|
||||
$license_name = $license["name"];
|
||||
$license_url = $license["url"];
|
||||
$licenses_urls[$license_id] = $license_url;
|
||||
}
|
||||
}
|
||||
|
||||
// only open the file once per script execution
|
||||
if(!isset($lines)) {
|
||||
$lines = file($home."/BirdNET-Pi/model/labels_flickr.txt");
|
||||
@@ -93,11 +157,30 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" && isse
|
||||
}
|
||||
}
|
||||
|
||||
$flickrjson = json_decode(file_get_contents("https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key=".$config["FLICKR_API_KEY"]."&text=".str_replace(" ", "%20", $engname).$comnameprefix."&sort=relevance".$args."&per_page=5&media=photos&format=json&nojsoncallback=1"), true)["photos"]["photo"][0];
|
||||
$modaltext = "https://flickr.com/photos/".$flickrjson["owner"]."/".$flickrjson["id"];
|
||||
$authorlink = "https://flickr.com/people/".$flickrjson["owner"];
|
||||
$imageurl = 'https://farm' .$flickrjson["farm"]. '.static.flickr.com/' .$flickrjson["server"]. '/' .$flickrjson["id"]. '_' .$flickrjson["secret"]. '.jpg';
|
||||
array_push($_SESSION['images'], array($comname,$imageurl,$flickrjson["title"], $modaltext, $authorlink));
|
||||
// Read the blacklisted image ids from the file into an array
|
||||
$blacklisted_ids = array_map('trim', file($home."/BirdNET-Pi/scripts/blacklisted_images.txt"));
|
||||
|
||||
// Make the API call
|
||||
$flickrjson = json_decode(file_get_contents("https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key=".$config["FLICKR_API_KEY"]."&text=".str_replace(" ", "%20", $engname).$comnameprefix."&sort=relevance".$args."&per_page=5&media=photos&format=json&nojsoncallback=1"), true)["photos"]["photo"];
|
||||
|
||||
// Find the first photo that is not blacklisted or is not the specific blacklisted id
|
||||
$photo = null;
|
||||
foreach ($flickrjson as $flickrphoto) {
|
||||
if ($flickrphoto["id"] !== "4892923285" && !in_array($flickrphoto["id"], $blacklisted_ids)) {
|
||||
$photo = $flickrphoto;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$license_url = "https://api.flickr.com/services/rest/?method=flickr.photos.getInfo&api_key=".$config["FLICKR_API_KEY"]."&photo_id=".$photo["id"]."&format=json&nojsoncallback=1";
|
||||
$license_response = file_get_contents($license_url);
|
||||
$license_info = json_decode($license_response, true)["photo"]["license"];
|
||||
$license_url = $licenses_urls[$license_info];
|
||||
|
||||
$modaltext = "https://flickr.com/photos/".$photo["owner"]."/".$photo["id"];
|
||||
$authorlink = "https://flickr.com/people/".$photo["owner"];
|
||||
$imageurl = 'https://farm' .$photo["farm"]. '.static.flickr.com/' .$photo["server"]. '/' .$photo["id"]. '_' .$photo["secret"]. '.jpg';
|
||||
array_push($_SESSION['images'], array($comname,$imageurl,$photo["title"], $modaltext, $authorlink, $license_url));
|
||||
$image = $_SESSION['images'][count($_SESSION['images'])-1];
|
||||
}
|
||||
}
|
||||
@@ -127,12 +210,12 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" && isse
|
||||
<td class="relative"><a target="_blank" href="index.php?filename=<?php echo $mostrecent['File_Name']; ?>"><img class="copyimage" title="Open in new tab" width="25" height="25" src="images/copy.png"></a>
|
||||
<div class="centered_image_container" style="margin-bottom: 0px !important;">
|
||||
<?php if(!empty($config["FLICKR_API_KEY"]) && strlen($image[2]) > 0) { ?>
|
||||
<img onclick='setModalText(<?php echo $iterations; ?>,"<?php echo urlencode($image[2]); ?>", "<?php echo $image[3]; ?>", "<?php echo $image[4]; ?>", "<?php echo $image[1]; ?>")' src="<?php echo $image[1]; ?>" class="img1">
|
||||
<img onclick='setModalText(<?php echo $iterations; ?>,"<?php echo urlencode($image[2]); ?>", "<?php echo $image[3]; ?>", "<?php echo $image[4]; ?>", "<?php echo $image[1]; ?>", "<?php echo $image[5]; ?>")' src="<?php echo $image[1]; ?>" class="img1">
|
||||
<?php } ?>
|
||||
<form action="" method="GET">
|
||||
<input type="hidden" name="view" value="Species Stats">
|
||||
<button type="submit" name="species" value="<?php echo $mostrecent['Com_Name'];?>"><?php echo $mostrecent['Com_Name'];?></button></br>
|
||||
<a href="https://wikipedia.org/wiki/<?php echo $sciname;?>" target="_blank"/><i><?php echo $mostrecent['Sci_Name'];?></i></a>
|
||||
<button type="submit" name="species" value="<?php echo $mostrecent['Com_Name'];?>"><?php echo $mostrecent['Com_Name'];?></button><img style="width: unset !important;display: inline;height: 1em;cursor:pointer" title="View species stats" onclick="generateMiniGraph(this, '<?php echo $comname; ?>')" width=25 src="images/chart.svg"><br>
|
||||
<a href="https://wikipedia.org/wiki/<?php echo $sciname;?>" target="_blank"><i><?php echo $mostrecent['Sci_Name'];?></i></a>
|
||||
<br>Confidence: <?php echo $percent = round((float)round($mostrecent['Confidence'],2) * 100 ) . '%';?><br></div><br>
|
||||
<video style="margin-top:10px" onplay='setLiveStreamVolume(0)' onended='setLiveStreamVolume(1)' onpause='setLiveStreamVolume(1)' controls poster="<?php echo $filename.".png";?>" preload="none" title="<?php echo $filename;?>"><source src="<?php echo $filename;?>"></video></td>
|
||||
</form>
|
||||
@@ -141,8 +224,12 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" && isse
|
||||
}
|
||||
}
|
||||
if($iterations == 0) {
|
||||
if($todaycount['COUNT(*)'] > 0) {
|
||||
echo "<h3>Your system is currently processing a backlog of audio. This can take several hours before normal functionality of your BirdNET-Pi resumes.</h3>";
|
||||
} else {
|
||||
echo "<h3>No Detections For Today.</h3>";
|
||||
}
|
||||
}
|
||||
die();
|
||||
}
|
||||
|
||||
@@ -156,14 +243,6 @@ if($statement == False) {
|
||||
$result = $statement->execute();
|
||||
$totalcount = $result->fetchArray(SQLITE3_ASSOC);
|
||||
|
||||
$statement2 = $db->prepare('SELECT COUNT(*) FROM detections WHERE Date == DATE(\'now\', \'localtime\')');
|
||||
if($statement2 == False) {
|
||||
echo "Database is busy";
|
||||
header("refresh: 0;");
|
||||
}
|
||||
$result2 = $statement2->execute();
|
||||
$todaycount = $result2->fetchArray(SQLITE3_ASSOC);
|
||||
|
||||
$statement3 = $db->prepare('SELECT COUNT(*) FROM detections WHERE Date == Date(\'now\', \'localtime\') AND TIME >= TIME(\'now\', \'localtime\', \'-1 hour\')');
|
||||
if($statement3 == False) {
|
||||
echo "Database is busy";
|
||||
@@ -230,13 +309,18 @@ body::-webkit-scrollbar {
|
||||
</style>
|
||||
</head>
|
||||
<div class="overview">
|
||||
<dialog id="attribution-dialog">
|
||||
<dialog style="margin-top: 5px;max-height: 95vh;
|
||||
overflow-y: auto;overscroll-behavior:contain" id="attribution-dialog">
|
||||
<h1 id="modalHeading"></h1>
|
||||
<p id="modalText"></p>
|
||||
<button onclick="hideDialog()">Close</button>
|
||||
<button style="font-weight:bold;color:blue" onclick="if(confirm('Are you sure you want to blacklist this image?')) { blacklistImage(); }">Blacklist this image</button>
|
||||
</dialog>
|
||||
<script src="static/dialog-polyfill.js"></script>
|
||||
<script src="static/Chart.bundle.js"></script>
|
||||
<script src="static/chartjs-plugin-trendline.min.js"></script>
|
||||
<script>
|
||||
var last_photo_link;
|
||||
var dialog = document.querySelector('dialog');
|
||||
dialogPolyfill.registerDialog(dialog);
|
||||
|
||||
@@ -248,9 +332,25 @@ body::-webkit-scrollbar {
|
||||
document.getElementById('attribution-dialog').close();
|
||||
}
|
||||
|
||||
function setModalText(iter, title, text, authorlink, photolink) {
|
||||
function blacklistImage() {
|
||||
const match = last_photo_link.match(/\d+$/); // match one or more digits
|
||||
const result = match ? match[0] : null; // extract the first match or return null if no match is found
|
||||
console.log(last_photo_link)
|
||||
const xhttp = new XMLHttpRequest();
|
||||
xhttp.onload = function() {
|
||||
if(this.responseText.length > 0) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
xhttp.open("GET", "overview.php?blacklistimage="+result, true);
|
||||
xhttp.send();
|
||||
|
||||
}
|
||||
|
||||
function setModalText(iter, title, text, authorlink, photolink, licenseurl) {
|
||||
document.getElementById('modalHeading').innerHTML = "Photo: \""+decodeURIComponent(title.replaceAll("+"," "))+"\" Attribution";
|
||||
document.getElementById('modalText').innerHTML = "<div><img style='border-radius:5px' src='"+photolink+"'></div><br><div>Image link: <a target='_blank' href="+text+">"+text+"</a><br>Author link: <a target='_blank' href="+authorlink+">"+authorlink+"</a></div>";
|
||||
document.getElementById('modalText').innerHTML = "<div><img style='border-radius:5px;max-height: calc(100vh - 15rem);display: block;margin: 0 auto;' src='"+photolink+"'></div><br><div style='white-space:nowrap'>Image link: <a target='_blank' href="+text+">"+text+"</a><br>Author link: <a target='_blank' href="+authorlink+">"+authorlink+"</a><br>License URL: <a href="+licenseurl+" target='_blank'>"+licenseurl+"</a></div>";
|
||||
last_photo_link = text;
|
||||
showDialog();
|
||||
}
|
||||
</script>
|
||||
@@ -282,8 +382,12 @@ if (file_exists('./Charts/'.$chart)) {
|
||||
$refresh = $config['RECORDING_LENGTH'];
|
||||
$time = time();
|
||||
echo "<img id=\"spectrogramimage\" src=\"/spectrogram.png?nocache=$time\">";
|
||||
|
||||
?>
|
||||
|
||||
<div id="customimage"></div>
|
||||
<br>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -360,5 +464,177 @@ window.addEventListener("load", function(){
|
||||
window.setInterval(function(){
|
||||
document.getElementById("spectrogramimage").src = "/spectrogram.png?nocache="+Date.now();
|
||||
}, <?php echo $refresh; ?>*1000);
|
||||
|
||||
<?php if(isset($config["CUSTOM_IMAGE"]) && strlen($config["CUSTOM_IMAGE"]) > 2){?>
|
||||
// every 1 second, this loop will run and refresh the custom image
|
||||
window.setInterval(function(){
|
||||
// Find the customimage element
|
||||
var customimage = document.getElementById("customimage");
|
||||
|
||||
function updateCustomImage() {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", "overview.php?custom_image=true", true);
|
||||
xhr.onload = function() {
|
||||
customimage.innerHTML = xhr.responseText;
|
||||
}
|
||||
xhr.send();
|
||||
}
|
||||
updateCustomImage();
|
||||
}, 1000);
|
||||
<?php } ?>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tooltip {
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
padding: 10px;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function generateMiniGraph(elem, comname) {
|
||||
|
||||
// Make an AJAX call to fetch the number of detections for the bird species
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '/todays_detections.php?comname=' + comname);
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
var detections = JSON.parse(xhr.responseText);
|
||||
|
||||
// Create a div element for the chart window
|
||||
if (typeof(window.chartWindow) != 'undefined') {
|
||||
document.body.removeChild(window.chartWindow);
|
||||
window.chartWindow = undefined;
|
||||
}
|
||||
var chartWindow = document.createElement('div');
|
||||
chartWindow.className = "chartdiv"
|
||||
chartWindow.style.position = 'fixed';
|
||||
chartWindow.style.top = '0%';
|
||||
chartWindow.style.left = '50%';
|
||||
chartWindow.style.width = window.innerWidth < 700 ? '40%' : '20%';
|
||||
chartWindow.style.height = window.innerWidth < 700 ? '25%' : '16%';
|
||||
chartWindow.style.backgroundColor = '#fff';
|
||||
chartWindow.style.zIndex = '9999';
|
||||
chartWindow.style.overflow = 'auto';
|
||||
chartWindow.style.borderRadius = '5px';
|
||||
chartWindow.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.2)';
|
||||
|
||||
document.body.appendChild(chartWindow);
|
||||
|
||||
|
||||
// Create a canvas element for the chart
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = chartWindow.offsetWidth;
|
||||
canvas.height = chartWindow.offsetHeight;
|
||||
chartWindow.appendChild(canvas);
|
||||
|
||||
// Create a new Chart.js chart
|
||||
var ctx = canvas.getContext('2d');
|
||||
var chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: detections.map(item => item.date),
|
||||
datasets: [{
|
||||
label: 'Detections',
|
||||
data: detections.map(item => item.count),
|
||||
backgroundColor: '#9fe29b',
|
||||
borderColor: '#77c487',
|
||||
borderWidth: 1,
|
||||
lineTension: 0.3, // Add smoothing to the line
|
||||
pointRadius: 1, // Make the data points smaller
|
||||
pointHitRadius: 10, // Increase the area around data points for mouse events
|
||||
|
||||
trendlineLinear: {
|
||||
style: "rgba(55, 99, 64, 0.5)",
|
||||
lineStyle: "solid",
|
||||
width: 1.5
|
||||
}
|
||||
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
layout: {
|
||||
padding: {
|
||||
right: 10
|
||||
}
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Detections Over 30d'
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
display: false,
|
||||
gridLines: {
|
||||
display: false // Hide the gridlines on the x-axis
|
||||
},
|
||||
ticks: {
|
||||
autoSkip: true,
|
||||
maxTicksLimit: 2
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
gridLines: {
|
||||
display: false // Hide the gridlines on the y-axis
|
||||
},
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
precision: 0,
|
||||
stepSize: 1
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var buttonRect = elem.getBoundingClientRect();
|
||||
var chartRect = chartWindow.getBoundingClientRect();
|
||||
if (window.innerWidth < 700) {
|
||||
chartWindow.style.left = 'calc(75% - ' + (chartRect.width / 2) + 'px)';
|
||||
} else {
|
||||
chartWindow.style.left = (buttonRect.right + 10) + 'px';
|
||||
}
|
||||
|
||||
|
||||
// Calculate the top position of the chart to center it with the button
|
||||
var buttonCenter = buttonRect.top + (buttonRect.height / 2);
|
||||
var chartHeight = chartWindow.offsetHeight;
|
||||
var chartTop = buttonCenter - (chartHeight / 2);
|
||||
chartWindow.style.top = chartTop + 'px';
|
||||
|
||||
// Add a close button to the chart window
|
||||
var closeButton = document.createElement('button');
|
||||
closeButton.id = "chartcb";
|
||||
closeButton.innerText = 'X';
|
||||
closeButton.style.position = 'absolute';
|
||||
closeButton.style.top = '5px';
|
||||
closeButton.style.right = '5px';
|
||||
closeButton.addEventListener('click', function() {
|
||||
document.body.removeChild(chartWindow);
|
||||
window.chartWindow = undefined;
|
||||
});
|
||||
chartWindow.appendChild(closeButton);
|
||||
window.chartWindow = chartWindow;
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
// Listen for the scroll event on the window object
|
||||
window.addEventListener('scroll', function() {
|
||||
// Get all chart elements
|
||||
var charts = document.querySelectorAll('.chartdiv');
|
||||
|
||||
// Loop through all chart elements and remove them
|
||||
charts.forEach(function(chart) {
|
||||
chart.parentNode.removeChild(chart);
|
||||
window.chartWindow = undefined;
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
+127
-29
@@ -1,4 +1,9 @@
|
||||
<?php
|
||||
|
||||
/* Prevent XSS input */
|
||||
$_GET = filter_input_array(INPUT_GET, FILTER_SANITIZE_STRING);
|
||||
$_POST = filter_input_array(INPUT_POST, FILTER_SANITIZE_STRING);
|
||||
|
||||
error_reporting(E_ERROR);
|
||||
ini_set('display_errors',1);
|
||||
|
||||
@@ -101,6 +106,31 @@ $shifted_path = $home."/BirdSongs/Extracted/By_Date/shifted/";
|
||||
|
||||
if(isset($_GET['shiftfile'])) {
|
||||
|
||||
if (file_exists('./scripts/thisrun.txt')) {
|
||||
$config = parse_ini_file('./scripts/thisrun.txt');
|
||||
} elseif (file_exists('./scripts/firstrun.ini')) {
|
||||
$config = parse_ini_file('./scripts/firstrun.ini');
|
||||
}
|
||||
$user = shell_exec("awk -F: '/1000/{print $1}' /etc/passwd");
|
||||
$home = shell_exec("awk -F: '/1000/{print $6}' /etc/passwd");
|
||||
$home = trim($home);
|
||||
$caddypwd = $config['CADDY_PWD'];
|
||||
if (!isset($_SERVER['PHP_AUTH_USER'])) {
|
||||
header('WWW-Authenticate: Basic realm="My Realm"');
|
||||
header('HTTP/1.0 401 Unauthorized');
|
||||
echo '<table><tr><td>You cannot shift files for this installation</td></tr></table>';
|
||||
exit;
|
||||
} else {
|
||||
$submittedpwd = $_SERVER['PHP_AUTH_PW'];
|
||||
$submitteduser = $_SERVER['PHP_AUTH_USER'];
|
||||
if($submittedpwd !== $caddypwd || $submitteduser !== 'birdnet'){
|
||||
header('WWW-Authenticate: Basic realm="My Realm"');
|
||||
header('HTTP/1.0 401 Unauthorized');
|
||||
echo '<table><tr><td>You cannot shift files for this installation<</td></tr></table>';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$filename = $_GET['shiftfile'];
|
||||
$pp = pathinfo($filename);
|
||||
$dir = $pp['dirname'];
|
||||
@@ -112,24 +142,19 @@ if(isset($_GET['shiftfile'])) {
|
||||
$freqshift_tool = $config['FREQSHIFT_TOOL'];
|
||||
|
||||
if ($freqshift_tool == "ffmpeg") {
|
||||
$cmd = "/usr/bin/nohup /usr/bin/ffmpeg -y -i \"".$pi.$filename."\" -af \"rubberband=pitch=".$config['FREQSHIFT_LO']."/".$config['FREQSHIFT_HI']."\" \"".$shifted_path.$filename."\"";
|
||||
shell_exec("mkdir -p ".$shifted_path.$dir." && echo \"".$cmd."\" > /tmp/shift.sh && chmod +x /tmp/shift.sh");
|
||||
$cmd = "sudo /usr/bin/nohup /usr/bin/ffmpeg -y -i ".escapeshellarg($pi.$filename)." -af \"rubberband=pitch=".$config['FREQSHIFT_LO']."/".$config['FREQSHIFT_HI']."\" ".escapeshellarg($shifted_path.$filename)."";
|
||||
shell_exec("sudo mkdir -p ".$shifted_path.$dir." && ".$cmd);
|
||||
|
||||
} else if ($freqshift_tool == "sox") {
|
||||
//linux.die.net/man/1/sox
|
||||
$soxopt = "-q";
|
||||
$soxpitch = $config['FREQSHIFT_PITCH'];
|
||||
$cmd = "/usr/bin/nohup /usr/bin/sox \"".$pi.$filename."\" \"".$shifted_path.$filename."\" pitch ".$soxopt." ".$soxpitch;
|
||||
shell_exec("mkdir -p ".$shifted_path.$dir." && echo \"".$cmd."\" > /tmp/shift.sh && chmod +x /tmp/shift.sh");
|
||||
$cmd = "sudo /usr/bin/nohup /usr/bin/sox ".escapeshellarg($pi.$filename)." ".escapeshellarg($shifted_path.$filename)." pitch ".$soxopt." ".$soxpitch;
|
||||
shell_exec("sudo mkdir -p ".$shifted_path.$dir." && ".$cmd);
|
||||
}
|
||||
|
||||
shell_exec("/tmp/shift.sh");
|
||||
shell_exec("rm -f /tmp/shift.sh");
|
||||
} else {
|
||||
$cmd = "rm -f " . $shifted_path.$filename;
|
||||
shell_exec("echo \"".$cmd."\" > /tmp/unshift.sh && chmod +x /tmp/unshift.sh");
|
||||
shell_exec("/tmp/unshift.sh");
|
||||
shell_exec("rm -f /tmp/unshift.sh");
|
||||
$cmd = "sudo rm -f " . escapeshellarg($shifted_path.$filename);
|
||||
shell_exec($cmd);
|
||||
}
|
||||
|
||||
echo "OK";
|
||||
@@ -236,7 +261,7 @@ function toggleLock(filename, type, elem) {
|
||||
elem.setAttribute("onclick", elem.getAttribute("onclick").replace("add","del"));
|
||||
} else {
|
||||
elem.setAttribute("src","images/unlock.svg");
|
||||
elem.setAttribute("title", "This file is not excluded from being purged.");
|
||||
elem.setAttribute("title", "This file will be deleted when disk space needs to be freed.");
|
||||
elem.setAttribute("onclick", elem.getAttribute("onclick").replace("del","add"));
|
||||
}
|
||||
}
|
||||
@@ -259,21 +284,31 @@ function toggleShiftFreq(filename, shiftAction, elem) {
|
||||
elem.setAttribute("title", "This file has been shifted down in frequency.");
|
||||
elem.setAttribute("onclick", elem.getAttribute("onclick").replace("shift","unshift"));
|
||||
console.log("shifted freqs of " + filename);
|
||||
video=elem.parentNode.getElementsByTagName("video")[0];
|
||||
video.setAttribute("title", video.getAttribute("title").replace("/By_Date/","/By_Date/shifted/"));
|
||||
source = video.getElementsByTagName("source")[0];
|
||||
video=elem.parentNode.getElementsByTagName("video");
|
||||
if (video.length > 0) {
|
||||
video[0].setAttribute("title", video[0].getAttribute("title").replace("/By_Date/","/By_Date/shifted/"));
|
||||
source = video[0].getElementsByTagName("source")[0];
|
||||
source.setAttribute("src", source.getAttribute("src").replace("/By_Date/","/By_Date/shifted/"));
|
||||
video.load();
|
||||
video[0].load();
|
||||
} else {
|
||||
atag=elem.parentNode.getElementsByTagName("a")[0];
|
||||
atag.setAttribute("href", atag.getAttribute("href").replace("/By_Date/","/By_Date/shifted/"));
|
||||
}
|
||||
} else {
|
||||
elem.setAttribute("src","images/shift.svg");
|
||||
elem.setAttribute("title", "This file is not shifted in frequency.");
|
||||
elem.setAttribute("onclick", elem.getAttribute("onclick").replace("unshift","shift"));
|
||||
console.log("unshifted freqs of " + filename);
|
||||
video=elem.parentNode.getElementsByTagName("video")[0];
|
||||
video.setAttribute("title", video.getAttribute("title").replace("/By_Date/shifted/","/By_Date/"));
|
||||
source = video.getElementsByTagName("source")[0];
|
||||
video=elem.parentNode.getElementsByTagName("video");
|
||||
if (video.length > 0) {
|
||||
video[0].setAttribute("title", video[0].getAttribute("title").replace("/By_Date/shifted/","/By_Date/"));
|
||||
source = video[0].getElementsByTagName("source")[0];
|
||||
source.setAttribute("src", source.getAttribute("src").replace("/By_Date/shifted/","/By_Date/"));
|
||||
video.load();
|
||||
video[0].load();
|
||||
} else {
|
||||
atag=elem.parentNode.getElementsByTagName("a")[0];
|
||||
atag.setAttribute("href", atag.getAttribute("href").replace("/By_Date/shifted/","/By_Date/"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -325,22 +360,76 @@ if(!isset($_GET['species']) && !isset($_GET['filename'])){
|
||||
|
||||
#By Species
|
||||
} elseif($view == "byspecies") {
|
||||
while($results=$result->fetchArray(SQLITE3_ASSOC)){
|
||||
$birds = array();
|
||||
while($results=$result->fetchArray(SQLITE3_ASSOC))
|
||||
{
|
||||
$name = $results['Com_Name'];
|
||||
$birds[] = $name;
|
||||
}
|
||||
|
||||
echo "<td>
|
||||
<button action=\"submit\" name=\"species\" value=\"$name\">$name</button></td></tr>";}
|
||||
if(count($birds) > 45) {
|
||||
$num_cols = 3;
|
||||
} else {
|
||||
$num_cols = 1;
|
||||
}
|
||||
$num_rows = ceil(count($birds) / $num_cols);
|
||||
|
||||
#Specific Date
|
||||
for ($row = 0; $row < $num_rows; $row++) {
|
||||
echo "<tr>";
|
||||
|
||||
for ($col = 0; $col < $num_cols; $col++) {
|
||||
$index = $row + $col * $num_rows;
|
||||
|
||||
if ($index < count($birds)) {
|
||||
?>
|
||||
<td class="spec">
|
||||
<button type="submit" name="species" value="<?php echo $birds[$index];?>"><?php echo $birds[$index];?></button>
|
||||
</td>
|
||||
<?php
|
||||
} else {
|
||||
echo "<td></td>";
|
||||
}
|
||||
}
|
||||
|
||||
echo "</tr>";
|
||||
}
|
||||
} elseif($view == "date") {
|
||||
while($results=$result->fetchArray(SQLITE3_ASSOC)){
|
||||
$birds = array();
|
||||
while($results=$result->fetchArray(SQLITE3_ASSOC))
|
||||
{
|
||||
$name = $results['Com_Name'];
|
||||
if(realpath($home."/BirdSongs/Extracted/By_Date/".$date."/".str_replace(" ", "_",$name)) !== false){
|
||||
echo "<td>
|
||||
<button action=\"submit\" name=\"species\" value=\"$name\">$name</button></td></tr>";
|
||||
$birds[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
if(count($birds) > 45) {
|
||||
$num_cols = 3;
|
||||
} else {
|
||||
$num_cols = 1;
|
||||
}
|
||||
$num_rows = ceil(count($birds) / $num_cols);
|
||||
|
||||
for ($row = 0; $row < $num_rows; $row++) {
|
||||
echo "<tr>";
|
||||
|
||||
for ($col = 0; $col < $num_cols; $col++) {
|
||||
$index = $row + $col * $num_rows;
|
||||
|
||||
if ($index < count($birds)) {
|
||||
?>
|
||||
<td class="spec">
|
||||
<button type="submit" name="species" value="<?php echo $birds[$index];?>"><?php echo $birds[$index];?></button>
|
||||
</td>
|
||||
<?php
|
||||
} else {
|
||||
echo "<td></td>";
|
||||
}
|
||||
}
|
||||
|
||||
echo "</tr>";
|
||||
}
|
||||
|
||||
#Choose
|
||||
} else {
|
||||
echo "<td>
|
||||
@@ -440,7 +529,7 @@ echo "<table>
|
||||
if($config["FULL_DISK"] == "purge") {
|
||||
if(!in_array($filename_formatted, $disk_check_exclude_arr)) {
|
||||
$imageicon = "images/unlock.svg";
|
||||
$title = "This file is not excluded from being purged.";
|
||||
$title = "This file will be deleted when disk space needs to be freed (>95% usage).";
|
||||
$type = "add";
|
||||
} else {
|
||||
$imageicon = "images/lock.svg";
|
||||
@@ -515,7 +604,7 @@ echo "<table>
|
||||
if($config["FULL_DISK"] == "purge") {
|
||||
if(!in_array($filename_formatted, $disk_check_exclude_arr)) {
|
||||
$imageicon = "images/unlock.svg";
|
||||
$title = "This file is not excluded from being purged.";
|
||||
$title = "This file will be deleted when disk space needs to be freed (>95% usage).";
|
||||
$type = "add";
|
||||
} else {
|
||||
$imageicon = "images/lock.svg";
|
||||
@@ -553,4 +642,13 @@ echo "<table>
|
||||
|
||||
}echo "</table>";}?>
|
||||
</div>
|
||||
<style>
|
||||
td.spec {
|
||||
width: calc(100% / <?php echo $num_cols;?>);
|
||||
}
|
||||
tr:first-child td.spec {
|
||||
padding-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
</html>
|
||||
|
||||
+107
-6
@@ -11,6 +11,8 @@ import sqlite3
|
||||
from sqlite3 import Connection
|
||||
import plotly.express as px
|
||||
from sklearn.preprocessing import normalize
|
||||
from suntime import Sun
|
||||
from datetime import datetime
|
||||
|
||||
pio.templates.default = "plotly_white"
|
||||
|
||||
@@ -38,13 +40,13 @@ st.markdown("""
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
|
||||
@st.cache(hash_funcs={Connection: id})
|
||||
# @st.cache(allow_output_mutation=True)
|
||||
@st.cache_resource()
|
||||
def get_connection(path: str):
|
||||
return sqlite3.connect(path, check_same_thread=False)
|
||||
|
||||
|
||||
def get_data(conn: Connection):
|
||||
@st.cache_resource()
|
||||
def get_data(_conn: Connection):
|
||||
df1 = pd.read_sql("SELECT * FROM detections", con=conn)
|
||||
return df1
|
||||
|
||||
@@ -89,7 +91,7 @@ else:
|
||||
# end_date = datetime(2022 ,5 ,17).date()
|
||||
|
||||
|
||||
@st.cache()
|
||||
@st.cache_data()
|
||||
def date_filter(df, start_date, end_date):
|
||||
filt = (df2.index >= pd.Timestamp(start_date)) & (df2.index <= pd.Timestamp(end_date + timedelta(days=1)))
|
||||
df = df[filt]
|
||||
@@ -132,7 +134,7 @@ else:
|
||||
resample_time = resample_times[resample_sel]
|
||||
|
||||
|
||||
@st.cache()
|
||||
@st.cache_data()
|
||||
def time_resample(df, resample_time):
|
||||
if resample_time == 'Raw':
|
||||
df_resample = df['Com_Name']
|
||||
@@ -168,6 +170,68 @@ top_N_species = (df5.value_counts()[:top_N])
|
||||
|
||||
font_size = 15
|
||||
|
||||
def sunrise_sunset_scatter(num_days_to_display):
|
||||
latitude = df['Lat'][0]
|
||||
longitude = df['Lon'][0]
|
||||
|
||||
sun = Sun(latitude, longitude)
|
||||
|
||||
sunrise_list = []
|
||||
sunset_list = []
|
||||
sunrise_week_list = []
|
||||
sunset_week_list = []
|
||||
sunrise_text_list = []
|
||||
sunset_text_list = []
|
||||
|
||||
now = datetime.now()
|
||||
|
||||
for past_day in range(num_days_to_display):
|
||||
d = timedelta(days=num_days_to_display - past_day - 1)
|
||||
|
||||
current_date = now - d
|
||||
# current_date = datetime.fromisocalendar(2022, week + 1, 5)
|
||||
# time_zone = datetime.now()
|
||||
sun_rise = sun.get_local_sunrise_time(current_date)
|
||||
sun_dusk = sun.get_local_sunset_time(current_date)
|
||||
|
||||
sun_rise_time = float(sun_rise.hour) + float(sun_rise.minute) / 60.0
|
||||
sun_dusk_time = float(sun_dusk.hour) + float(sun_dusk.minute) / 60.0
|
||||
|
||||
temp_time = str(sun_rise)[-14:-9] + " Sunrise"
|
||||
sunrise_text_list.append(temp_time)
|
||||
temp_time = str(sun_dusk)[-14:-9] + " Sunset"
|
||||
sunset_text_list.append(temp_time)
|
||||
sunrise_list.append(sun_rise_time)
|
||||
sunset_list.append(sun_dusk_time)
|
||||
sunrise_week_list.append(past_day)
|
||||
sunset_week_list.append(past_day)
|
||||
|
||||
sunrise_week_list.append(None)
|
||||
sunrise_list.append(None)
|
||||
sunrise_text_list.append(None)
|
||||
sun_list = sunrise_list.extend(sunset_list)
|
||||
sun_week_list = sunrise_week_list.extend(sunset_week_list)
|
||||
sunrise_text_list.extend(sunset_text_list)
|
||||
|
||||
return sunrise_week_list, sunrise_list, sunrise_text_list
|
||||
|
||||
def hms_to_dec(t):
|
||||
# (h, m, s) = t.split(':')
|
||||
h = t.hour
|
||||
m = t.minute / 60
|
||||
s = t.second / 3600
|
||||
result = h + m + s
|
||||
return result
|
||||
|
||||
def hms_to_str(t):
|
||||
# (h, m, s) = t.split(':')
|
||||
h = t.hour
|
||||
m = t.minute
|
||||
# s = t.second / 3600
|
||||
# result = h + m + s
|
||||
return "%02d:%02d" % (h, m)
|
||||
|
||||
|
||||
if daily is False:
|
||||
|
||||
if resample_time != '1D':
|
||||
@@ -364,8 +428,11 @@ if daily is False:
|
||||
df4.index = [df4.index.date, df4.index.time]
|
||||
day_hour_freq = df4.unstack().fillna(0)
|
||||
|
||||
saved_time_labels = [hms_to_str(h) for h in day_hour_freq.columns.tolist()]
|
||||
fig_dec_y = [hms_to_dec(h) for h in day_hour_freq.columns.tolist()]
|
||||
fig_x = [d.strftime('%d-%m-%Y') for d in day_hour_freq.index.tolist()]
|
||||
fig_y = [h.strftime('%H:%M') for h in day_hour_freq.columns.tolist()]
|
||||
day_hour_freq.columns = fig_dec_y
|
||||
fig_z = day_hour_freq.values.transpose()
|
||||
# fig_heatmap = go.Figure(data=go.Heatmap(x=fig_x,y=fig_y,z=fig_z))
|
||||
|
||||
@@ -374,7 +441,41 @@ if daily is False:
|
||||
# yaxis={'categoryorder':'total ascending'})
|
||||
color_pals = px.colors.named_colorscales()
|
||||
selected_pal = st.sidebar.selectbox('Select Color Pallet for Daily Detections', color_pals)
|
||||
fig.add_trace(go.Heatmap(x=fig_x, y=fig_y, z=fig_z, autocolorscale=False, colorscale=selected_pal), row=1, col=1)
|
||||
|
||||
heatmap = go.Heatmap(
|
||||
# x=fig_x, y=fig_y,
|
||||
x=fig_x,
|
||||
y=day_hour_freq.columns,
|
||||
z=fig_z, # heat.values,
|
||||
showscale=False,
|
||||
# text=labels,
|
||||
texttemplate="%{text}", autocolorscale=False, colorscale=selected_pal
|
||||
)
|
||||
num_days_to_display = len(fig_x)
|
||||
sunrise_week_list, sunrise_list, sunrise_text_list = sunrise_sunset_scatter(num_days_to_display)
|
||||
daysback_range = fig_x
|
||||
daysback_range.append(None)
|
||||
daysback_range.extend(daysback_range)
|
||||
daysback_range = daysback_range[:-1]
|
||||
|
||||
sunrise_sunset = go.Scatter(x=daysback_range,
|
||||
y=sunrise_list,
|
||||
mode='lines',
|
||||
hoverinfo='text',
|
||||
text=sunrise_text_list,
|
||||
line_color='orange', line_width=1, name=' ')
|
||||
|
||||
fig = go.Figure(data=[heatmap, sunrise_sunset])
|
||||
number_of_y_ticks = 12
|
||||
y_downscale_factor = int(len(saved_time_labels) / number_of_y_ticks)
|
||||
fig.update_layout(
|
||||
yaxis=dict(
|
||||
tickmode = 'array',
|
||||
tickvals = day_hour_freq.columns[::y_downscale_factor],
|
||||
ticktext = saved_time_labels[::y_downscale_factor],
|
||||
nticks = 6
|
||||
)
|
||||
)
|
||||
st.plotly_chart(fig, use_container_width=True) # , config=config)
|
||||
else:
|
||||
fig = make_subplots(
|
||||
|
||||
@@ -9,6 +9,9 @@ for service in "${services[@]}"; do
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "========= Syslog snippet =========="
|
||||
tail -n 100 /var/log/syslog
|
||||
|
||||
# 2. Mounted file systems
|
||||
echo "========== Mounted File Systems =========="
|
||||
df -h
|
||||
|
||||
@@ -13,7 +13,8 @@ services=(chart_viewer.service
|
||||
icecast2.service
|
||||
extraction.service
|
||||
birdnet_recording.service
|
||||
birdnet_log.service)
|
||||
birdnet_log.service
|
||||
birdnet_stats.service)
|
||||
|
||||
for i in "${services[@]}";do
|
||||
sudo systemctl restart "${i}"
|
||||
|
||||
+36
-8
@@ -1,3 +1,4 @@
|
||||
import re
|
||||
from pathlib import Path
|
||||
from tzlocal import get_localzone
|
||||
import datetime
|
||||
@@ -12,6 +13,7 @@ import operator
|
||||
import socket
|
||||
import threading
|
||||
import os
|
||||
import gzip
|
||||
|
||||
from utils.notifications import sendAppriseNotifications
|
||||
from utils.parse_settings import config_to_settings
|
||||
@@ -70,7 +72,7 @@ def loadModel():
|
||||
print('LOADING TF LITE MODEL...', end=' ')
|
||||
|
||||
# Load TFLite model and allocate tensors.
|
||||
# model will either be BirdNET_GLOBAL_3K_V2.2_Model_FP16 (new) or BirdNET_6K_GLOBAL_MODEL (old)
|
||||
# model will either be BirdNET_GLOBAL_3K_V2.3_Model_FP16 (new) or BirdNET_6K_GLOBAL_MODEL (old)
|
||||
modelpath = userDir + '/BirdNET-Pi/model/'+model+'.tflite'
|
||||
myinterpreter = tflite.Interpreter(model_path=modelpath, num_threads=2)
|
||||
myinterpreter.allocate_tensors()
|
||||
@@ -104,7 +106,7 @@ def loadMetaModel():
|
||||
global M_OUTPUT_LAYER_INDEX
|
||||
|
||||
# Load TFLite model and allocate tensors.
|
||||
M_INTERPRETER = tflite.Interpreter(model_path=userDir + '/BirdNET-Pi/model/BirdNET_GLOBAL_3K_V2.2_MData_Model_FP16.tflite')
|
||||
M_INTERPRETER = tflite.Interpreter(model_path=userDir + '/BirdNET-Pi/model/BirdNET_GLOBAL_3K_V2.3_MData_Model_FP16.tflite')
|
||||
M_INTERPRETER.allocate_tensors()
|
||||
|
||||
# Get input and output tensors.
|
||||
@@ -276,7 +278,7 @@ def analyzeAudioData(chunks, lat, lon, week, sensitivity, overlap,):
|
||||
start = time.time()
|
||||
print('ANALYZING AUDIO...', end=' ', flush=True)
|
||||
|
||||
if model == "BirdNET_GLOBAL_3K_V2.2_Model_FP16":
|
||||
if model == "BirdNET_GLOBAL_3K_V2.3_Model_FP16":
|
||||
if len(PREDICTED_SPECIES_LIST) == 0 or len(INCLUDE_LIST) != 0:
|
||||
predictSpeciesList(lat, lon, week)
|
||||
|
||||
@@ -402,16 +404,40 @@ def handle_client(conn, addr):
|
||||
|
||||
birdweather_id = args.birdweather_id
|
||||
|
||||
# Read audio data
|
||||
# Read audio data & handle errors
|
||||
try:
|
||||
audioData = readAudioData(args.i, args.overlap)
|
||||
|
||||
except (NameError, TypeError) as e:
|
||||
print(f"Error with the following info: {e}")
|
||||
open('~/BirdNET-Pi/analyzing_now.txt', 'w').close()
|
||||
|
||||
finally:
|
||||
pass
|
||||
|
||||
# Get Date/Time from filename in case Pi gets behind
|
||||
# now = datetime.now()
|
||||
full_file_name = args.i
|
||||
# print('FULL FILENAME: -' + full_file_name + '-')
|
||||
file_name = Path(full_file_name).stem
|
||||
|
||||
# Get the RSTP stream identifier from the filename if it exists
|
||||
RTSP_ident_for_fn = ""
|
||||
RTSP_ident = re.search("RTSP_[0-9]+-", file_name)
|
||||
if RTSP_ident is not None:
|
||||
RTSP_ident_for_fn = RTSP_ident.group()
|
||||
|
||||
# Find and remove the identifier for the RSTP stream url it was from that is added when more than one
|
||||
# RSTP stream is recorded simultaneously, in order to make the filenames unique as filenames are all
|
||||
# generated at the same time
|
||||
file_name = re.sub("RTSP_[0-9]+-", "", file_name)
|
||||
|
||||
# Now we can read the date and time as normal
|
||||
# First portion of the filename contaning the date in Y m d
|
||||
file_date = file_name.split('-birdnet-')[0]
|
||||
# Second portion of the filename containing the time in H:M:S
|
||||
file_time = file_name.split('-birdnet-')[1]
|
||||
# Join the date and time together to get a complete string representing when the audio was recorded
|
||||
date_time_str = file_date + ' ' + file_time
|
||||
date_time_obj = datetime.datetime.strptime(date_time_str, '%Y-%m-%d %H:%M:%S')
|
||||
# print('Date:', date_time_obj.date())
|
||||
@@ -471,7 +497,7 @@ def handle_client(conn, addr):
|
||||
Overlap = str(args.overlap)
|
||||
Com_Name = Com_Name.replace("'", "")
|
||||
File_Name = Com_Name.replace(" ", "_") + '-' + Confidence + '-' + \
|
||||
Date.replace("/", "-") + '-birdnet-' + Time + audiofmt
|
||||
Date.replace("/", "-") + '-birdnet-' + RTSP_ident_for_fn + Time + audiofmt
|
||||
|
||||
# Connect to SQLite Database
|
||||
for attempt_number in range(3):
|
||||
@@ -543,7 +569,9 @@ def handle_client(conn, addr):
|
||||
|
||||
with open(args.i, 'rb') as f:
|
||||
wav_data = f.read()
|
||||
response = requests.post(url=soundscape_url, data=wav_data, headers={'Content-Type': 'application/octet-stream'})
|
||||
gzip_wav_data = gzip.compress(wav_data)
|
||||
response = requests.post(url=soundscape_url, data=gzip_wav_data, headers={'Content-Type': 'application/octet-stream',
|
||||
'Content-Encoding': 'gzip'})
|
||||
print("Soundscape POST Response Status - ", response.status_code)
|
||||
sdata = response.json()
|
||||
soundscape_id = sdata['soundscape']['id']
|
||||
@@ -565,8 +593,8 @@ def handle_client(conn, addr):
|
||||
post_commonName = "\"commonName\": \"" + entry[0].split('_')[1].split("/")[0] + "\","
|
||||
post_scientificName = "\"scientificName\": \"" + entry[0].split('_')[0] + "\","
|
||||
|
||||
if model == "BirdNET_GLOBAL_3K_V2.2_Model_FP16":
|
||||
post_algorithm = "\"algorithm\": " + "\"2p2\"" + ","
|
||||
if model == "BirdNET_GLOBAL_3K_V2.3_Model_FP16":
|
||||
post_algorithm = "\"algorithm\": " + "\"2p3\"" + ","
|
||||
else:
|
||||
post_algorithm = "\"algorithm\": " + "\"alpha\"" + ","
|
||||
|
||||
|
||||
@@ -25,10 +25,10 @@ function service_status($name) {
|
||||
<div class="servicecontrols">
|
||||
<form action="" method="GET">
|
||||
<h3>Live Audio Stream <?php echo service_status("livestream.service");?></h3>
|
||||
<button type="submit" name="submit" value="sudo systemctl stop livestream.service && sudo /etc/init.d/icecast2 stop">Stop</button>
|
||||
<button type="submit" name="submit" value="sudo systemctl restart livestream.service && sudo /etc/init.d/icecast2 restart">Restart </button>
|
||||
<button type="submit" name="submit" value="sudo systemctl disable --now livestream.service && sudo systemctl disable icecast2 && sudo /etc/init.d/icecast2 stop">Disable</button>
|
||||
<button type="submit" name="submit" value="sudo systemctl enable icecast2 && sudo /etc/init.d/icecast2 start && sudo systemctl enable --now livestream.service">Enable</button>
|
||||
<button type="submit" name="submit" value="sudo systemctl stop livestream.service && sudo systemctl stop icecast2.service">Stop</button>
|
||||
<button type="submit" name="submit" value="sudo systemctl restart livestream.service && sudo systemctl restart icecast2.service">Restart </button>
|
||||
<button type="submit" name="submit" value="sudo systemctl disable --now livestream.service && sudo systemctl disable icecast2 && sudo systemctl stop icecast2.service">Disable</button>
|
||||
<button type="submit" name="submit" value="sudo systemctl enable icecast2 && sudo systemctl start icecast2.service && sudo systemctl enable --now livestream.service">Enable</button>
|
||||
</form>
|
||||
<form action="" method="GET">
|
||||
<h3>Web Terminal <?php echo service_status("web_terminal.service");?></h3>
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ def loadMetaModel():
|
||||
global CLASSES
|
||||
|
||||
# Load TFLite model and allocate tensors.
|
||||
M_INTERPRETER = tflite.Interpreter(model_path=userDir + '/BirdNET-Pi/model/BirdNET_GLOBAL_3K_V2.2_MData_Model_FP16.tflite')
|
||||
M_INTERPRETER = tflite.Interpreter(model_path=userDir + '/BirdNET-Pi/model/BirdNET_GLOBAL_3K_V2.3_MData_Model_FP16.tflite')
|
||||
M_INTERPRETER.allocate_tensors()
|
||||
|
||||
# Get input and output tensors.
|
||||
|
||||
+243
-9
@@ -1,30 +1,84 @@
|
||||
<?php
|
||||
error_reporting(E_ERROR);
|
||||
ini_set('display_errors',1);
|
||||
if(isset($_GET['ajax_csv'])) {
|
||||
|
||||
if (file_exists('./scripts/thisrun.txt')) {
|
||||
$config = parse_ini_file('./scripts/thisrun.txt');
|
||||
} elseif (file_exists('./scripts/firstrun.ini')) {
|
||||
$config = parse_ini_file('./scripts/firstrun.ini');
|
||||
}
|
||||
|
||||
if(!empty($config['FREQSHIFT_RECONNECT_DELAY']) && is_numeric($config['FREQSHIFT_RECONNECT_DELAY'])){
|
||||
$FREQSHIFT_RECONNECT_DELAY = ($config['FREQSHIFT_RECONNECT_DELAY']);
|
||||
}else{
|
||||
$FREQSHIFT_RECONNECT_DELAY = 4000;
|
||||
}
|
||||
|
||||
if(isset($_GET['ajax_csv'])) {
|
||||
$RECS_DIR = $config["RECS_DIR"];
|
||||
|
||||
$user = shell_exec("awk -F: '/1000/{print $1}' /etc/passwd");
|
||||
$home = shell_exec("awk -F: '/1000/{print $6}' /etc/passwd");
|
||||
$home = trim($home);
|
||||
$files = scandir($RECS_DIR."/".date('F-Y')."/".date('d-l')."/", SCANDIR_SORT_ASCENDING);
|
||||
|
||||
//Replace variables to Fix up the file paths just in case the ENV environment variables don't resolve via PHP
|
||||
$RECS_DIR = str_replace("\$HOME", $home, $RECS_DIR);
|
||||
$STREAM_DATA_DIR = $RECS_DIR . "/StreamData/";
|
||||
|
||||
//Try find the latest file out of the soundcard recording folder
|
||||
$look_in_directory = $RECS_DIR."/".date('F-Y')."/".date('d-l')."/";
|
||||
$files = scandir($look_in_directory, SCANDIR_SORT_ASCENDING);
|
||||
//Extract the filename, positions 0 and 1 are the folder hierarchy '.' and '..'
|
||||
$newest_file = $files[2];
|
||||
|
||||
//Couldn't find under e.g /home/pi/April-2023/03-Monday/ (where USB audio streams are recorded
|
||||
//look in the StreamData directory (RTSP streams are recorded here)
|
||||
if (empty($files) || array_key_exists(2, $files) === false) {
|
||||
$look_in_directory = $STREAM_DATA_DIR;
|
||||
|
||||
//Load the file in the directory
|
||||
$files = scandir($look_in_directory, SCANDIR_SORT_ASCENDING);
|
||||
|
||||
//Because there might be more than 1 stream, we can't really assume the file at index 2 is the latest, or even for the stream being listened to
|
||||
//Read the RTSP_STREAM_TO_LIVESTREAM setting, then try to find that CSV file
|
||||
if(!empty($config['RTSP_STREAM_TO_LIVESTREAM']) && is_numeric($config['RTSP_STREAM_TO_LIVESTREAM'])){
|
||||
//The stored setting of RTSP_STREAM_TO_LIVESTREAM is 0 based, but filenames are 1's based, so just add 1 to the config value
|
||||
//so we can match up the stream the user is listening to with the appropriate filename
|
||||
$RTSP_STREAM_LISTENED_TO = ($config['RTSP_STREAM_TO_LIVESTREAM'] + 1);
|
||||
}else{
|
||||
//Setting is invalid somehow
|
||||
//The stored setting of RTSP_STREAM_TO_LIVESTREAM is 0 based, but filenames are 1's based, so just add 1 to the config value
|
||||
//This will be the first stream
|
||||
$RTSP_STREAM_LISTENED_TO = 1;
|
||||
}
|
||||
|
||||
//The RTSP streams contain 'RTSP_X' in the filename, were X is the stream url index in the comma separated list of RTSP streams
|
||||
//We can use this to locate the file for this stream
|
||||
foreach ($files as $file_idx => $stream_file_name) {
|
||||
//Skip the folder hierarchy entries
|
||||
if ($stream_file_name != "." && $stream_file_name != "..") {
|
||||
//See if the filename contains the correct RTSP name, also only check .wav files by excluding the csv files with the filter .wav.csv
|
||||
if (stripos($stream_file_name, 'RTSP_' . $RTSP_STREAM_LISTENED_TO) !== false && stripos($stream_file_name, '.wav.csv') === false) {
|
||||
//Found a match - set it as the newest file
|
||||
$newest_file = $stream_file_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//If the newest file param has been supplied and it's the same as the newest file found
|
||||
//then stop processing
|
||||
if($newest_file == $_GET['newest_file']) {
|
||||
die();
|
||||
}
|
||||
|
||||
//Print out the filename
|
||||
echo "file,".$newest_file."\n";
|
||||
|
||||
//Print out the detected birds as CSV
|
||||
$row = 1;
|
||||
if (($handle = fopen($RECS_DIR."/".date('F-Y')."/".date('d-l')."/".$newest_file.".csv", "r")) !== FALSE) {
|
||||
if (($handle = fopen($look_in_directory . $newest_file . ".csv", "r")) !== FALSE) {
|
||||
while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) {
|
||||
if($row != 1){
|
||||
$num = count($data);
|
||||
@@ -37,8 +91,29 @@ if (($handle = fopen($RECS_DIR."/".date('F-Y')."/".date('d-l')."/".$newest_file.
|
||||
}
|
||||
fclose($handle);
|
||||
}
|
||||
//Kill the script so no further processing or output is done
|
||||
die();
|
||||
}
|
||||
|
||||
//Hold the array of RTSP steams once they are exploded
|
||||
$RTSP_Stream_Config = array();
|
||||
|
||||
//Load the birdnet config so we can read the RTSP setting
|
||||
// Valid config data
|
||||
if (is_array($config) && array_key_exists('RTSP_STREAM',$config)) {
|
||||
if (is_null($config['RTSP_STREAM']) === false && $config['RTSP_STREAM'] !== "") {
|
||||
$RTSP_Stream_Config_Data = explode(",", $config['RTSP_STREAM']);
|
||||
|
||||
//Process the stream further
|
||||
//we need to able to ID it (just do this by position), get the hostname to show in the dropdown box
|
||||
foreach ($RTSP_Stream_Config_Data as $stream_idx => $stream_url) {
|
||||
//$stream_idx is the array position of the the RSP stream URL, idx of 0 is the first, 1 - second etc
|
||||
$RTSP_stream_url = parse_url($stream_url);
|
||||
$RTSP_Stream_Config[$stream_idx] = $RTSP_stream_url['host'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
<script>
|
||||
// CREDITS: https://codepen.io/jakealbaugh/pen/jvQweW
|
||||
@@ -139,21 +214,27 @@ function applyText(text,x,y,opacity) {
|
||||
|
||||
var add =0;
|
||||
var newest_file;
|
||||
var newest_file_tmp;
|
||||
var lastbird;
|
||||
function loadDetectionIfNewExists() {
|
||||
const xhttp = new XMLHttpRequest();
|
||||
xhttp.onload = function() {
|
||||
// if there's a new detection that needs to be updated to the page
|
||||
if(this.responseText.length > 0 && !this.responseText.includes("Database")) {
|
||||
|
||||
// Case insensitive match for RTSP_X
|
||||
var RTSP_regex = /RTSP_[0-9]+-/i;
|
||||
var RTSP_regex_match = false;
|
||||
var split = this.responseText.split("\n")
|
||||
for(var i = 1;i < split.length; i++) {
|
||||
if(parseInt(split[i].split(",")[0]) >= 0){
|
||||
|
||||
newest_file = split[0].split(",")[1]
|
||||
newest_file_tmp = newest_file;//Copy the file name var so we something to work with
|
||||
//Remove the string that denotes the RTSP stream from the filename to make things easier, it's not really needed
|
||||
RTSP_regex_match = RTSP_regex.test(newest_file_tmp)
|
||||
newest_file_tmp = newest_file_tmp.replace(RTSP_regex, "")
|
||||
//applyText(split[i].split(",")[1],document.body.querySelector('canvas').width - ((parseInt(split[i].split(",")[0]))*avgfps), (document.body.querySelector('canvas').height * 0.50))
|
||||
|
||||
d1 = new Date(newest_file.split("-")[0]+"/"+newest_file.split("-")[1]+"/"+newest_file.split("-")[2]+ " "+newest_file.split("-")[4].replace(".wav",""))
|
||||
d1 = new Date(newest_file_tmp.split("-")[0]+"/"+newest_file_tmp.split("-")[1]+"/"+newest_file_tmp.split("-")[2]+ " "+newest_file_tmp.split("-")[4].replace(".wav",""))
|
||||
console.log("d1 "+d1)
|
||||
d2 = new Date(xhttp.getResponseHeader("Date"));
|
||||
console.log("d2 "+d2)
|
||||
@@ -173,7 +254,13 @@ function loadDetectionIfNewExists() {
|
||||
console.log(add)
|
||||
|
||||
// Date csv file was created + relative detection time of bird + mic delay
|
||||
secago = Math.abs(timeDiff) - split[i].split(",")[0] - 6.8;
|
||||
//If we can't find the regex in the filename, then the RTSP_X string doesn't exist, unlikely to be a RTSP stream
|
||||
if (RTSP_regex_match == false){
|
||||
secago = (Math.abs(timeDiff) - split[i].split(",")[0]) - 6.8;
|
||||
}else{
|
||||
//half the delay for RTSP streams ~ just a rough guess
|
||||
secago = (Math.abs(timeDiff) - split[i].split(",")[0]) - 4;
|
||||
}
|
||||
|
||||
x = document.body.querySelector('canvas').width - ((parseInt(secago))*avgfps);
|
||||
// if the text is too close to the right side of the canvas and will be cut off, wait 3 seconds before adding text
|
||||
@@ -235,6 +322,48 @@ function toggleCompression(state) {
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFreqshift(state) {
|
||||
if (state == true) {
|
||||
console.log("freqshift activated")
|
||||
} else {
|
||||
console.log("freqshift deactivated")
|
||||
}
|
||||
|
||||
freqShiftReconnectDelay = <?php echo $FREQSHIFT_RECONNECT_DELAY; ?>;
|
||||
|
||||
var livestream_freqshift_spinner = document.getElementById('livestream_freqshift_spinner');
|
||||
livestream_freqshift_spinner.style.display = "inline";
|
||||
// Create the XMLHttpRequest object.
|
||||
const xhr = new XMLHttpRequest();
|
||||
// Initialize the request
|
||||
xhr.open("GET", './views.php?activate_freqshift_in_livestream=' + state + '&view=Advanced&submit=advanced');
|
||||
// Send the request
|
||||
xhr.send();
|
||||
// Fired once the request completes successfully
|
||||
xhr.onload = function (e) {
|
||||
// Check if the request was a success
|
||||
if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
|
||||
// Restart the audio player in case it stopped working while the livestream service was restarted
|
||||
var audio_player = document.querySelector('audio#player');
|
||||
if (audio_player !== 'undefined') {
|
||||
//central_controls_element.appendChild(h1_loading);
|
||||
//Wait 2 seconds before restarting the stream
|
||||
setTimeout(function () {
|
||||
console.log("Restarting connection with livestream");
|
||||
audio_player.pause();
|
||||
audio_player.setAttribute('src', '/stream');
|
||||
audio_player.load();
|
||||
audio_player.play();
|
||||
|
||||
livestream_freqshift_spinner.style.display = "none";
|
||||
},
|
||||
freqShiftReconnectDelay
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
document.body.querySelector('h1').remove();
|
||||
const CVS = document.body.querySelector('canvas');
|
||||
@@ -273,6 +402,7 @@ function initialize() {
|
||||
gainNode.connect(ACTX.destination);
|
||||
|
||||
document.getElementById("compression").removeAttribute("disabled");
|
||||
document.getElementById("freqshift").removeAttribute("disabled");
|
||||
|
||||
console.log(SOURCE);
|
||||
const DATA = new Uint8Array(ANALYSER.frequencyBinCount);
|
||||
@@ -340,6 +470,46 @@ h1 {
|
||||
<img id="spectrogramimage" style="width:100%;height:100%;display:none" src="/spectrogram.png?nocache=<?php echo $time;?>">
|
||||
|
||||
<div class="centered">
|
||||
<?php
|
||||
if (isset($RTSP_Stream_Config) && !empty($RTSP_Stream_Config)) {
|
||||
?>
|
||||
<div style="display:inline" id="RTSP_streams">
|
||||
<label>RTSP Stream: </label>
|
||||
<select id="rtsp_stream_select" name="RTSP Streams">
|
||||
<?php
|
||||
//The setting representing which livestream to stream is more than the number of RTSP streams available
|
||||
//maybe the list of streams has been modified
|
||||
//This isn't the ideal for this, but needed a way to fix this setting without calling the advanced setting page
|
||||
if (array_key_exists($config['RTSP_STREAM_TO_LIVESTREAM'], $RTSP_Stream_Config) === false) {
|
||||
$contents = file_get_contents('/etc/birdnet/birdnet.conf');
|
||||
$contents2 = file_get_contents('./scripts/thisrun.txt');
|
||||
$contents = preg_replace("/RTSP_STREAM_TO_LIVESTREAM=.*/", "RTSP_STREAM_TO_LIVESTREAM=\"0\"", $contents);
|
||||
$contents2 = preg_replace("/RTSP_STREAM_TO_LIVESTREAM=.*/", "RTSP_STREAM_TO_LIVESTREAM=\"0\"", $contents2);
|
||||
$fh = fopen("/etc/birdnet/birdnet.conf", "w");
|
||||
$fh2 = fopen("./scripts/thisrun.txt", "w");
|
||||
fwrite($fh, $contents);
|
||||
fwrite($fh2, $contents2);
|
||||
exec("sudo systemctl restart livestream.service");
|
||||
}
|
||||
|
||||
//Print out the dropdown list for the RTSP streams
|
||||
foreach ($RTSP_Stream_Config as $stream_id => $stream_host) {
|
||||
$isSelected = "";
|
||||
//Match up the selected value saved in config so we can preselect it
|
||||
if ($config['RTSP_STREAM_TO_LIVESTREAM'] == $stream_id) {
|
||||
$isSelected = 'selected="selected"';
|
||||
}
|
||||
//Create the select option
|
||||
echo "<option value=" . $stream_id . " $isSelected >" . $stream_host . "</option>";
|
||||
}
|
||||
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
—
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
<div style="display:inline" id="gain" >
|
||||
<label>Gain: </label>
|
||||
<span class="slidecontainer">
|
||||
@@ -347,18 +517,77 @@ h1 {
|
||||
<span id="gain_value"></span>%
|
||||
</span>
|
||||
</div>
|
||||
—
|
||||
—
|
||||
<div style="display:inline" id="comp" >
|
||||
<label>Compression: </label>
|
||||
<input name="compression" type="checkbox" id="compression" disabled>
|
||||
</div>
|
||||
<div style="display:inline" id="fshift" >
|
||||
<label>Freq shift: </label>
|
||||
<?php
|
||||
if ($config['ACTIVATE_FREQSHIFT_IN_LIVESTREAM'] == "true") {
|
||||
$freqshift_state = "checked";
|
||||
} else {
|
||||
$freqshift_state = "";
|
||||
}
|
||||
?>
|
||||
<input name="freqshift" type="checkbox" id="freqshift" <?php echo($freqshift_state); ?> disabled>
|
||||
<img id="livestream_freqshift_spinner" src=images/spinner.gif style="height: 25px; vertical-align: top; display: none">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<audio style="display:none" controls="" crossorigin="anonymous" id='player' preload="none"><source id="playersrc" src="/stream"></audio>
|
||||
<h1>Loading...</h1>
|
||||
<h1 id="loading-h1">Loading...</h1>
|
||||
<canvas></canvas>
|
||||
|
||||
<script>
|
||||
var rtsp_stream_select = document.getElementById("rtsp_stream_select");
|
||||
if (typeof (rtsp_stream_select) !== 'undefined' && rtsp_stream_select !== null) {
|
||||
//When the dropdown selection is changed set the new value is settings, then restart the livestream service so it broadcasts newly selected RTSP stream
|
||||
rtsp_stream_select.onchange = function () {
|
||||
if (this.value !== 'undefined') {
|
||||
// Get the audio player element
|
||||
var audio_player = document.querySelector('audio#player');
|
||||
var central_controls_element = document.getElementsByClassName('centered')[0];
|
||||
|
||||
//Create the loading header again as a placeholder while we're waiting to reload the stream
|
||||
var h1_loading = document.createElement("H1");
|
||||
var h1_loading_text = document.createTextNode("Loading...");
|
||||
h1_loading.setAttribute("id", "loading-h1");
|
||||
h1_loading.setAttribute("style", "font-size:48px; font-weight: bolder; color: #FFF");
|
||||
h1_loading.appendChild(h1_loading_text);
|
||||
|
||||
// Create the XMLHttpRequest object.
|
||||
const xhr = new XMLHttpRequest();
|
||||
// Initialize the request
|
||||
xhr.open("GET", './views.php?rtsp_stream_to_livestream=' + this.value + '&view=Advanced&submit=advanced');
|
||||
// Send the request
|
||||
xhr.send();
|
||||
// Fired once the request completes successfully
|
||||
xhr.onload = function (e) {
|
||||
// Check if the request was a success
|
||||
if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
|
||||
// Restart the audio player in case it stopped working while the livestream service was restarted
|
||||
if (audio_player !== 'undefined') {
|
||||
central_controls_element.appendChild(h1_loading);
|
||||
//Wait 5 seconds before restarting the stream
|
||||
setTimeout(function () {
|
||||
audio_player.pause();
|
||||
audio_player.setAttribute('src', '/stream');
|
||||
audio_player.load();
|
||||
audio_player.play();
|
||||
|
||||
document.getElementById('loading-h1').remove()
|
||||
},
|
||||
10000
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var slider = document.getElementById("gain_input");
|
||||
var output = document.getElementById("gain_value");
|
||||
output.innerHTML = slider.value; // Display the default slider value
|
||||
@@ -374,4 +603,9 @@ var compression = document.getElementById("compression");
|
||||
compression.onclick = function() {
|
||||
toggleCompression(this.checked);
|
||||
}
|
||||
|
||||
var freqshift = document.getElementById("freqshift");
|
||||
freqshift.onclick = function() {
|
||||
toggleFreqshift(this.checked);
|
||||
}
|
||||
</script>
|
||||
+26
-3
@@ -1,6 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# Make sox spectrogram
|
||||
source /etc/birdnet/birdnet.conf
|
||||
analyzing_now="$(cat $HOME/BirdNET-Pi/analyzing_now.txt)"
|
||||
spectrogram_png=${EXTRACTED}/spectrogram.png
|
||||
sox -V1 "${analyzing_now}" -n remix 1 rate 24k spectrogram -c "${analyzing_now//$HOME\/}" -o "${spectrogram_png}"
|
||||
|
||||
# Read the logging level from the configuration option
|
||||
LOGGING_LEVEL="${LogLevel_SpectrogramViewerService}"
|
||||
# If empty for some reason default to log level of error
|
||||
[ -z $LOGGING_LEVEL ] && LOGGING_LEVEL='error'
|
||||
# Additionally if we're at debug or info level then allow printing of script commands and variables
|
||||
if [ "$LOGGING_LEVEL" == "info" ] || [ "$LOGGING_LEVEL" == "debug" ];then
|
||||
# Enable printing of commands/variables etc to terminal for debugging
|
||||
set -x
|
||||
fi
|
||||
|
||||
# Time to sleep between generating spectrogram's, default set the recording length
|
||||
# To try catch the spectrogram as soon as possible run at a smaller intervals
|
||||
SLEEP_DELAY=$((RECORDING_LENGTH / 4))
|
||||
|
||||
# Continuously loop generating a spectrogram every 10 seconds
|
||||
while true; do
|
||||
analyzing_now="$(cat $HOME/BirdNET-Pi/analyzing_now.txt)"
|
||||
|
||||
if [ ! -z "${analyzing_now}" ] && [ -f "${analyzing_now}" ]; then
|
||||
spectrogram_png=${EXTRACTED}/spectrogram.png
|
||||
sox -V1 "${analyzing_now}" -n remix 1 rate 24k spectrogram -c "${analyzing_now//$HOME\//}" -o "${spectrogram_png}"
|
||||
fi
|
||||
|
||||
sleep $SLEEP_DELAY
|
||||
done
|
||||
|
||||
+58
-18
@@ -1,4 +1,10 @@
|
||||
<?php
|
||||
|
||||
/* Prevent XSS input */
|
||||
$_GET = filter_input_array(INPUT_GET, FILTER_SANITIZE_STRING);
|
||||
$_POST = filter_input_array(INPUT_POST, FILTER_SANITIZE_STRING);
|
||||
|
||||
ini_set('user_agent', 'PHP_Flickr/1.0');
|
||||
error_reporting(0);
|
||||
ini_set('display_errors', 0);
|
||||
|
||||
@@ -87,28 +93,62 @@ if(!file_exists($home."/BirdNET-Pi/scripts/disk_check_exclude.txt") || strpos(fi
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<table>
|
||||
<?php
|
||||
while($results=$result2->fetchArray(SQLITE3_ASSOC))
|
||||
{
|
||||
$comname = preg_replace('/ /', '_', $results['Com_Name']);
|
||||
$comname = preg_replace('/\'/', '', $comname);
|
||||
$filename = "/By_Date/".$results['Date']."/".$comname."/".$results['File_Name'];
|
||||
?>
|
||||
<tr>
|
||||
<table style="padding-top:10px">
|
||||
<?php
|
||||
$birds = array();
|
||||
while($results=$result2->fetchArray(SQLITE3_ASSOC))
|
||||
{
|
||||
$comname = preg_replace('/ /', '_', $results['Com_Name']);
|
||||
$comname = preg_replace('/\'/', '', $comname);
|
||||
$filename = "/By_Date/".$results['Date']."/".$comname."/".$results['File_Name'];
|
||||
$birds[] = $results['Com_Name'];
|
||||
}
|
||||
|
||||
if(count($birds) > 45) {
|
||||
$num_cols = 3;
|
||||
} else {
|
||||
$num_cols = 1;
|
||||
}
|
||||
$num_rows = ceil(count($birds) / $num_cols);
|
||||
|
||||
for ($row = 0; $row < $num_rows; $row++) {
|
||||
echo "<tr>";
|
||||
|
||||
for ($col = 0; $col < $num_cols; $col++) {
|
||||
$index = $row + $col * $num_rows;
|
||||
|
||||
if ($index < count($birds)) {
|
||||
?>
|
||||
<td>
|
||||
<form action="" method="GET">
|
||||
<td><input type="hidden" name="sort" value="<?php if(isset($_GET['sort'])){echo $_GET['sort'];}?>">
|
||||
<input type="hidden" name="sort" value="<?php if(isset($_GET['sort'])){echo $_GET['sort'];}?>">
|
||||
<input type="hidden" name="view" value="Species Stats">
|
||||
<button type="submit" name="species" value="<?php echo $results['Com_Name'];?>"><?php echo $results['Com_Name'];?></button>
|
||||
</td>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
<button type="submit" name="species" value="<?php echo $birds[$index];?>"><?php echo $birds[$index];?></button>
|
||||
</form>
|
||||
</tr>
|
||||
</td>
|
||||
<?php
|
||||
} else {
|
||||
echo "<td></td>";
|
||||
}
|
||||
}
|
||||
|
||||
echo "</tr>";
|
||||
}
|
||||
?>
|
||||
</table>
|
||||
<style>
|
||||
td {
|
||||
padding: 0px;
|
||||
width: calc(100% / <?php echo $num_cols;?>);
|
||||
}
|
||||
tr:first-child td {
|
||||
padding-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
</div>
|
||||
<dialog id="attribution-dialog">
|
||||
<dialog style="margin-top: 5px;max-height: 95vh;
|
||||
overflow-y: auto;overscroll-behavior:contain" id="attribution-dialog">
|
||||
<h1 id="modalHeading"></h1>
|
||||
<p id="modalText"></p>
|
||||
<button onclick="hideDialog()">Close</button>
|
||||
@@ -128,7 +168,7 @@ function hideDialog() {
|
||||
|
||||
function setModalText(iter, title, text, authorlink) {
|
||||
document.getElementById('modalHeading').innerHTML = "Photo "+iter+": \""+title+"\" Attribution";
|
||||
document.getElementById('modalText').innerHTML = "Image link: <a target='_blank' href="+text+">"+text+"</a><br>Author link: <a target='_blank' href="+authorlink+">"+authorlink+"</a>";
|
||||
document.getElementById('modalText').innerHTML = "<div style='white-space:nowrap'>Image link: <a target='_blank' href="+text+">"+text+"</a><br>Author link: <a target='_blank' href="+authorlink+">"+authorlink+"</a></div>";
|
||||
showDialog();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -10,6 +10,12 @@ if (preg_match("/behind '.*?' by (\d+) commit(s?)\b/", $str, $matches)) {
|
||||
$num_commits_behind = $matches[1];
|
||||
$_SESSION['behind'] = $num_commits_behind;
|
||||
}
|
||||
if (preg_match('/\b(\d+)\b and \b(\d+)\b different commits each/', $str, $matches)) {
|
||||
$num1 = (int) $matches[1];
|
||||
$num2 = (int) $matches[2];
|
||||
$sum = $num1 + $num2;
|
||||
$_SESSION['behind'] = $sum;
|
||||
}
|
||||
?><html>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<br>
|
||||
@@ -38,4 +44,12 @@ function update() {
|
||||
<form action="" method="GET">
|
||||
<button type="submit" name="submit" value="sudo clear_all_data.sh" onclick="return confirm('Clear ALL Data? Note that this cannot be undone and will take up to 90 seconds.')">Clear ALL data</button>
|
||||
</form>
|
||||
<?php
|
||||
$cmd="cd ".$home."/BirdNET-Pi && sudo -u ".$user." git rev-list --max-count=1 HEAD";
|
||||
$curr_hash = shell_exec($cmd);
|
||||
?>
|
||||
<p style="font-size:11px;text-align:center"></br></br>Running version: </p>
|
||||
<a href="https://github.com/mcguirepr89/BirdNET-Pi/commit/<?php echo $curr_hash; ?>" target="_blank">
|
||||
<p style="font-size:11px;text-align:center;box-sizing: border-box"><?php echo $curr_hash; ?></p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
+322
-22
@@ -1,5 +1,11 @@
|
||||
<?php
|
||||
|
||||
/* Prevent XSS input */
|
||||
$_GET = filter_input_array(INPUT_GET, FILTER_SANITIZE_STRING);
|
||||
$_POST = filter_input_array(INPUT_POST, FILTER_SANITIZE_STRING);
|
||||
|
||||
ini_set('session.gc_maxlifetime', 7200);
|
||||
ini_set('user_agent', 'PHP_Flickr/1.0');
|
||||
session_set_cookie_params(7200);
|
||||
session_start();
|
||||
error_reporting(E_ERROR);
|
||||
@@ -80,6 +86,52 @@ $user = shell_exec("awk -F: '/1000/{print $1}' /etc/passwd");
|
||||
$home = shell_exec("awk -F: '/1000/{print $6}' /etc/passwd");
|
||||
$home = trim($home);
|
||||
|
||||
if(isset($_GET['comname'])) {
|
||||
$birdName = $_GET['comname'];
|
||||
$birdName = str_replace("_", " ", $birdName);
|
||||
|
||||
|
||||
// Prepare a SQL statement to retrieve the detection data for the specified bird
|
||||
$stmt = $db->prepare('SELECT Date, COUNT(*) AS Detections FROM detections WHERE Com_Name = :com_name AND Date BETWEEN DATE("now", "-30 days") AND DATE("now") GROUP BY Date');
|
||||
|
||||
// Bind the bird name parameter to the SQL statement
|
||||
$stmt->bindValue(':com_name', $birdName);
|
||||
|
||||
// Execute the SQL statement and get the result set
|
||||
$result = $stmt->execute();
|
||||
|
||||
// Fetch the result set as an associative array
|
||||
$data = array();
|
||||
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
|
||||
$data[$row['Date']] = $row['Detections'];
|
||||
}
|
||||
|
||||
// Create an array of all dates in the last 14 days
|
||||
$last14Days = array();
|
||||
for ($i = 0; $i < 31; $i++) {
|
||||
$last14Days[] = date('Y-m-d', strtotime("-$i days"));
|
||||
}
|
||||
|
||||
// Merge the data array with the last14Days array
|
||||
$data = array_merge(array_fill_keys($last14Days, 0), $data);
|
||||
|
||||
// Sort the data by date in ascending order
|
||||
ksort($data);
|
||||
|
||||
// Convert the data to an array of objects
|
||||
$data = array_map(function($date, $count) {
|
||||
return array('date' => $date, 'count' => $count);
|
||||
}, array_keys($data), $data);
|
||||
|
||||
// Close the database connection
|
||||
$db->close();
|
||||
|
||||
// Return the data as JSON
|
||||
echo json_encode($data);
|
||||
die();
|
||||
|
||||
}
|
||||
|
||||
// from https://stackoverflow.com/questions/2690504/php-producing-relative-date-time-from-timestamps
|
||||
function relativeTime($ts)
|
||||
{
|
||||
@@ -166,7 +218,8 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" ) {
|
||||
$_SESSION['images'] = [];
|
||||
}
|
||||
$iterations = 0;
|
||||
$lines;
|
||||
$lines=null;
|
||||
$licenses_urls = array();
|
||||
|
||||
if (file_exists('./scripts/thisrun.txt')) {
|
||||
$config = parse_ini_file('./scripts/thisrun.txt');
|
||||
@@ -175,7 +228,6 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" ) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
while($todaytable=$result0->fetchArray(SQLITE3_ASSOC))
|
||||
{
|
||||
$iterations++;
|
||||
@@ -183,6 +235,7 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" ) {
|
||||
$comname = preg_replace('/ /', '_', $todaytable['Com_Name']);
|
||||
$comname = preg_replace('/\'/', '_', $comname);
|
||||
$filename = "/By_Date/".date('Y-m-d')."/".$comname."/".$todaytable['File_Name'];
|
||||
$filename_formatted = $todaytable['Date']."/".$comname."/".$todaytable['File_Name'];
|
||||
$sciname = preg_replace('/ /', '_', $todaytable['Sci_Name']);
|
||||
$args = "&license=2%2C3%2C4%2C5%2C6%2C9&orientation=square,portrait";
|
||||
$comnameprefix = "%20bird";
|
||||
@@ -207,6 +260,19 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" ) {
|
||||
if($key !== false) {
|
||||
$image = $_SESSION['images'][$key];
|
||||
} else {
|
||||
// Get license information if we haven't already
|
||||
if (empty($licenses_urls)) {
|
||||
$licenses_url = "https://api.flickr.com/services/rest/?method=flickr.photos.licenses.getInfo&api_key=".$config["FLICKR_API_KEY"]."&format=json&nojsoncallback=1";
|
||||
$licenses_response = file_get_contents($licenses_url);
|
||||
$licenses_data = json_decode($licenses_response, true)["licenses"]["license"];
|
||||
foreach ($licenses_data as $license) {
|
||||
$license_id = $license["id"];
|
||||
$license_name = $license["name"];
|
||||
$license_url = $license["url"];
|
||||
$licenses_urls[$license_id] = $license_url;
|
||||
}
|
||||
}
|
||||
|
||||
// only open the file once per script execution
|
||||
if(!isset($lines)) {
|
||||
$lines = file($home."/BirdNET-Pi/model/labels_flickr.txt");
|
||||
@@ -218,48 +284,88 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$flickrjson = json_decode(file_get_contents("https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key=".$config["FLICKR_API_KEY"]."&text=".str_replace(" ", "%20", $engname).$comnameprefix."&sort=relevance".$args."&per_page=5&media=photos&format=json&nojsoncallback=1"), true)["photos"]["photo"][0];
|
||||
$modaltext = "https://flickr.com/photos/".$flickrjson["owner"]."/".$flickrjson["id"];
|
||||
$authorlink = "https://flickr.com/people/".$flickrjson["owner"];
|
||||
$imageurl = 'https://farm' .$flickrjson["farm"]. '.static.flickr.com/' .$flickrjson["server"]. '/' .$flickrjson["id"]. '_' .$flickrjson["secret"]. '.jpg';
|
||||
array_push($_SESSION['images'], array($comname,$imageurl,$flickrjson["title"], $modaltext, $authorlink));
|
||||
// Read the blacklisted image ids from the file into an array
|
||||
$blacklisted_ids = array_map('trim', file($home."/BirdNET-Pi/scripts/blacklisted_images.txt"));
|
||||
|
||||
// Make the API call
|
||||
$flickrjson = json_decode(file_get_contents("https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key=".$config["FLICKR_API_KEY"]."&text=".str_replace(" ", "%20", $engname).$comnameprefix."&sort=relevance".$args."&per_page=5&media=photos&format=json&nojsoncallback=1"), true)["photos"]["photo"];
|
||||
|
||||
// Find the first photo that is not blacklisted or is not the specific blacklisted id
|
||||
$photo = null;
|
||||
foreach ($flickrjson as $flickrphoto) {
|
||||
if ($flickrphoto["id"] !== "4892923285" && !in_array($flickrphoto["id"], $blacklisted_ids)) {
|
||||
$photo = $flickrphoto;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$license_url = "https://api.flickr.com/services/rest/?method=flickr.photos.getInfo&api_key=".$config["FLICKR_API_KEY"]."&photo_id=".$photo["id"]."&format=json&nojsoncallback=1";
|
||||
$license_response = file_get_contents($license_url);
|
||||
$license_info = json_decode($license_response, true)["photo"]["license"];
|
||||
$license_url = $licenses_urls[$license_info];
|
||||
|
||||
$modaltext = "https://flickr.com/photos/".$photo["owner"]."/".$photo["id"];
|
||||
$authorlink = "https://flickr.com/people/".$photo["owner"];
|
||||
$imageurl = 'https://farm' .$photo["farm"]. '.static.flickr.com/' .$photo["server"]. '/' .$photo["id"]. '_' .$photo["secret"]. '.jpg';
|
||||
array_push($_SESSION['images'], array($comname,$imageurl,$photo["title"], $modaltext, $authorlink, $license_url));
|
||||
$image = $_SESSION['images'][count($_SESSION['images'])-1];
|
||||
}
|
||||
}
|
||||
?>
|
||||
<?php if(isset($_GET['display_limit']) && is_numeric($_GET['display_limit'])){ ?>
|
||||
<tr class="relative" id="<?php echo $iterations; ?>">
|
||||
<td class="relative"><a target="_blank" href="index.php?filename=<?php echo $todaytable['File_Name']; ?>"><img class="copyimage" title="Open in new tab" width=25 src="images/copy.png"></a>
|
||||
<td class="relative">
|
||||
<img style='cursor:pointer;right:45px' src='images/delete.svg' onclick='deleteDetection("<?php echo $filename_formatted; ?>")' class="copyimage" width=25 title='Delete Detection'>
|
||||
<a target="_blank" href="index.php?filename=<?php echo $todaytable['File_Name']; ?>"><img class="copyimage" title="Open in new tab" width=25 src="images/copy.png"></a>
|
||||
|
||||
|
||||
<div class="centered_image_container">
|
||||
<?php if(!empty($config["FLICKR_API_KEY"]) && strlen($image[2]) > 0) { ?>
|
||||
<img onclick='setModalText(<?php echo $iterations; ?>,"<?php echo urlencode($image[2]); ?>", "<?php echo $image[3]; ?>", "<?php echo $image[4]; ?>", "<?php echo $image[1]; ?>")' src="<?php echo $image[1]; ?>" class="img1">
|
||||
<img onclick='setModalText(<?php echo $iterations; ?>,"<?php echo urlencode($image[2]); ?>", "<?php echo $image[3]; ?>", "<?php echo $image[4]; ?>", "<?php echo $image[1]; ?>", "<?php echo $image[5]; ?>")' src="<?php echo $image[1]; ?>" class="img1">
|
||||
<?php } ?>
|
||||
|
||||
<?php echo $todaytable['Time'];?><br>
|
||||
<b><a class="a2" href="https://allaboutbirds.org/guide/<?php echo $comname;?>" target="top"><?php echo $todaytable['Com_Name'];?></a></b><br>
|
||||
<b><a class="a2" href="https://allaboutbirds.org/guide/<?php echo $comname;?>" target="top"><?php echo $todaytable['Com_Name'];?></a></b><img style="height: 1em;cursor:pointer" title="View species stats" onclick="generateMiniGraph(this, '<?php echo $comname; ?>')" width=25 src="images/chart.svg"><br>
|
||||
<a class="a2" href="https://wikipedia.org/wiki/<?php echo $sciname;?>" target="top"><i><?php echo $todaytable['Sci_Name'];?></i></a><br>
|
||||
<b>Confidence:</b> <?php echo round((float)round($todaytable['Confidence'],2) * 100 ) . '%';?><br></div><br>
|
||||
<video onplay='setLiveStreamVolume(0)' onended='setLiveStreamVolume(1)' onpause='setLiveStreamVolume(1)' controls poster="<?php echo $filename.".png";?>" preload="none" title="<?php echo $filename;?>"><source preload="none" src="<?php echo $filename;?>"></video>
|
||||
</td>
|
||||
<?php } else { //legacy mode ?>
|
||||
<tr class="relative" id="<?php echo $iterations; ?>">
|
||||
<td><?php if($_GET['kiosk'] == true) { echo relativeTime(strtotime($todaytable['Time'])); } else {echo $todaytable['Time'];}?><br></td><td id="recent_detection_middle_td">
|
||||
<td><?php if($_GET['kiosk'] == true) { echo relativeTime(strtotime($todaytable['Time'])); } else {echo $todaytable['Time'];}?><br></td>
|
||||
<td id="recent_detection_middle_td">
|
||||
<div>
|
||||
<div>
|
||||
<?php if(!empty($config["FLICKR_API_KEY"]) && (isset($_GET['hard_limit']) || $_GET['kiosk'] == true) && strlen($image[2]) > 0) { ?>
|
||||
<img style="float:left;height:75px;" onclick='setModalText(<?php echo $iterations; ?>,"<?php echo urlencode($image[2]); ?>", "<?php echo $image[3]; ?>", "<?php echo $image[4]; ?>", "<?php echo $image[1]; ?>")' src="<?php echo $image[1]; ?>" id="birdimage" class="img1">
|
||||
<img style="float:left;height:75px;" onclick='setModalText(<?php echo $iterations; ?>,"<?php echo urlencode($image[2]); ?>", "<?php echo $image[3]; ?>", "<?php echo $image[4]; ?>", "<?php echo $image[1]; ?>", "<?php echo $image[5]; ?>")' src="<?php echo $image[1]; ?>" id="birdimage" class="img1">
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div>
|
||||
<b><a class="a2" <?php if($_GET['kiosk'] == false){?>href="https://allaboutbirds.org/guide/<?php echo $comname;?>"<?php } else {echo "style='color:blue;'";} ?> target="top"><?php echo $todaytable['Com_Name'];?></a></b><br>
|
||||
<a class="a2" <?php if($_GET['kiosk'] == false){?>href="https://wikipedia.org/wiki/<?php echo $sciname;?>"<?php } else {echo "style='color:blue;'";} ?> target="top"><i><?php echo $todaytable['Sci_Name'];?></i></a><br></td>
|
||||
</div></div>
|
||||
<b><a class="a2" <?php if($_GET['kiosk'] == false){?>href="https://allaboutbirds.org/guide/<?php echo $comname;?>"<?php } else {echo "style='color:blue;'";} ?> target="top"><?php echo $todaytable['Com_Name'];?></a></b>
|
||||
<?php
|
||||
//If on mobile, add in a icon to link off to the recording so the user can see more info
|
||||
if (isset($_GET['mobile'])) {
|
||||
?>
|
||||
<br>
|
||||
<img style="height: 1em;cursor:pointer;float:unset;display:inline" title="View species stats" onclick="generateMiniGraph(this, '<?php echo $comname; ?>')" width=25 src="images/chart.svg">
|
||||
<a target="_blank" href="index.php?filename=<?php echo $todaytable['File_Name']; ?>"><img style="height: 1em;cursor:pointer;float:unset;display:inline" class="copyimage-mobile" title="Open in new tab" width=16 src="images/copy.png"></a>'
|
||||
<?php
|
||||
}else{
|
||||
//Else just put the species stats icon
|
||||
?>
|
||||
<img style="height: 1em;cursor:pointer;float:unset;display:inline" title="View species stats" onclick="generateMiniGraph(this, '<?php echo $comname; ?>')" width=25 src="images/chart.svg">
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
<br>
|
||||
<a class="a2" <?php if($_GET['kiosk'] == false){?>href="https://wikipedia.org/wiki/<?php echo $sciname;?>"<?php } else {echo "style='color:blue;'";} ?> target="top"><i><?php echo $todaytable['Sci_Name'];?></i></a><br>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><b>Confidence:</b> <?php echo round((float)round($todaytable['Confidence'],2) * 100 ) . '%';?><br></td>
|
||||
<?php if(!isset($_GET['mobile'])) { ?>
|
||||
<td style="min-width:180px"><audio controls preload="none" title="<?php echo $filename;?>"><source preload="none" src="<?php echo $filename;?>"></video>
|
||||
<td style="min-width:180px"><audio controls preload="none" title="<?php echo $filename;?>"><source preload="none" src="<?php echo $filename;?>"></audio></td>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<?php } ?>
|
||||
<?php }?>
|
||||
</tr>
|
||||
@@ -321,13 +427,38 @@ die();
|
||||
</style>
|
||||
</head>
|
||||
<div class="viewdb">
|
||||
<dialog id="attribution-dialog">
|
||||
<dialog style="margin-top: 5px;max-height: 95vh;
|
||||
overflow-y: auto;overscroll-behavior:contain" id="attribution-dialog">
|
||||
<h1 id="modalHeading"></h1>
|
||||
<p id="modalText"></p>
|
||||
<button style="background-color: #9fe29b;padding:20px" onclick="hideDialog()">Close</button>
|
||||
<button style="font-weight:bold;color:blue" onclick="hideDialog()">Close</button>
|
||||
<button style="font-weight:bold;color:blue" onclick="if(confirm('Are you sure you want to blacklist this image?')) { blacklistImage(); }">Blacklist this image</button>
|
||||
</dialog>
|
||||
<script src="static/dialog-polyfill.js"></script>
|
||||
<script src="static/Chart.bundle.js"></script>
|
||||
<script src="static/chartjs-plugin-trendline.min.js"></script>
|
||||
|
||||
<script>
|
||||
function deleteDetection(filename,copylink=false) {
|
||||
if (confirm("Are you sure you want to delete this detection from the database?") == true) {
|
||||
const xhttp = new XMLHttpRequest();
|
||||
xhttp.onload = function() {
|
||||
if(this.responseText == "OK"){
|
||||
if(copylink == true) {
|
||||
window.top.close();
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
} else {
|
||||
alert("Database busy.")
|
||||
}
|
||||
}
|
||||
xhttp.open("GET", "play.php?deletefile="+filename, true);
|
||||
xhttp.send();
|
||||
}
|
||||
}
|
||||
|
||||
var last_photo_link;
|
||||
var dialog = document.querySelector('dialog');
|
||||
dialogPolyfill.registerDialog(dialog);
|
||||
|
||||
@@ -339,13 +470,29 @@ die();
|
||||
document.getElementById('attribution-dialog').close();
|
||||
}
|
||||
|
||||
function setModalText(iter, title, text, authorlink, photolink) {
|
||||
function blacklistImage() {
|
||||
const match = last_photo_link.match(/\d+$/); // match one or more digits
|
||||
const result = match ? match[0] : null; // extract the first match or return null if no match is found
|
||||
console.log(last_photo_link)
|
||||
const xhttp = new XMLHttpRequest();
|
||||
xhttp.onload = function() {
|
||||
if(this.responseText.length > 0) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
xhttp.open("GET", "overview.php?blacklistimage="+result, true);
|
||||
xhttp.send();
|
||||
|
||||
}
|
||||
|
||||
function setModalText(iter, title, text, authorlink, photolink, licenseurl) {
|
||||
document.getElementById('modalHeading').innerHTML = "Photo: \""+decodeURIComponent(title.replaceAll("+"," "))+"\" Attribution";
|
||||
<?php if($kiosk == false) { ?>
|
||||
document.getElementById('modalText').innerHTML = "<div><img style='border-radius:5px' src='"+photolink+"'></div><br><div>Image link: <a target='_blank' href="+text+">"+text+"</a><br>Author link: <a target='_blank' href="+authorlink+">"+authorlink+"</a></div>";
|
||||
document.getElementById('modalText').innerHTML = "<div><img style='border-radius:5px;max-height: calc(100vh - 15rem);display: block;margin: 0 auto;' src='"+photolink+"'></div><br><div style='white-space:nowrap'>Image link: <a target='_blank' href="+text+">"+text+"</a><br>Author link: <a target='_blank' href="+authorlink+">"+authorlink+"</a><br>License URL: <a href="+licenseurl+" target='_blank'>"+licenseurl+"</a></div>";
|
||||
<?php } else { ?>
|
||||
document.getElementById('modalText').innerHTML = "<div><img style='border-radius:5px' src='"+photolink+"'></div><br><div>Image link: <a target='_blank'>"+text+"</a><br>Author link: <a target='_blank'>"+authorlink+"</a></div>";
|
||||
document.getElementById('modalText').innerHTML = "<div><img style='border-radius:5px;max-height: calc(100vh - 15rem);display: block;margin: 0 auto;' src='"+photolink+"'></div><br><div style='white-space:nowrap'>Image link: <a target='_blank'>"+text+"</a><br>Author link: <a target='_blank'>"+authorlink+"</a><br>License URL: <a target='_blank'>"+licenseurl+"</a></div>";
|
||||
<?php } ?>
|
||||
last_photo_link = text;
|
||||
showDialog();
|
||||
}
|
||||
</script>
|
||||
@@ -450,7 +597,9 @@ function searchDetections(searchvalue) {
|
||||
function loadDetections(detections_limit, element=undefined) {
|
||||
const xhttp = new XMLHttpRequest();
|
||||
xhttp.onload = function() {
|
||||
<?php if($kiosk == false) { ?>
|
||||
document.getElementsByClassName("legacyview")[0].style.display="unset";
|
||||
<?php } ?>
|
||||
if(typeof element !== "undefined")
|
||||
{
|
||||
element.remove();
|
||||
@@ -497,3 +646,154 @@ window.addEventListener("load", function(){
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tooltip {
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
padding: 10px;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function generateMiniGraph(elem, comname) {
|
||||
// Make an AJAX call to fetch the number of detections for the bird species
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '/todays_detections.php?comname=' + comname);
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
var detections = JSON.parse(xhr.responseText);
|
||||
|
||||
// Create a div element for the chart window
|
||||
if (typeof(window.chartWindow) != 'undefined') {
|
||||
document.body.removeChild(window.chartWindow);
|
||||
}
|
||||
var chartWindow = document.createElement('div');
|
||||
chartWindow.className = "chartdiv"
|
||||
chartWindow.style.position = 'fixed';
|
||||
chartWindow.style.top = '0%';
|
||||
chartWindow.style.left = '50%';
|
||||
chartWindow.style.width = window.innerWidth < 700 ? '40%' : '20%';
|
||||
chartWindow.style.height = window.innerWidth < 700 ? '25%' : '16%';
|
||||
chartWindow.style.backgroundColor = '#fff';
|
||||
chartWindow.style.zIndex = '9999';
|
||||
chartWindow.style.overflow = 'auto';
|
||||
chartWindow.style.borderRadius = '5px';
|
||||
chartWindow.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.2)';
|
||||
|
||||
document.body.appendChild(chartWindow);
|
||||
|
||||
|
||||
// Create a canvas element for the chart
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = chartWindow.offsetWidth;
|
||||
canvas.height = chartWindow.offsetHeight;
|
||||
chartWindow.appendChild(canvas);
|
||||
|
||||
// Create a new Chart.js chart
|
||||
var ctx = canvas.getContext('2d');
|
||||
var chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: detections.map(item => item.date),
|
||||
datasets: [{
|
||||
label: 'Detections',
|
||||
data: detections.map(item => item.count),
|
||||
backgroundColor: '#9fe29b',
|
||||
borderColor: '#77c487',
|
||||
borderWidth: 1,
|
||||
lineTension: 0.3, // Add smoothing to the line
|
||||
pointRadius: 1, // Make the data points smaller
|
||||
pointHitRadius: 10, // Increase the area around data points for mouse events
|
||||
|
||||
trendlineLinear: {
|
||||
style: "rgba(55, 99, 64, 0.5)",
|
||||
lineStyle: "solid",
|
||||
width: 1.5
|
||||
}
|
||||
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
layout: {
|
||||
padding: {
|
||||
right: 10
|
||||
}
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Detections Over 30d'
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
display: false,
|
||||
gridLines: {
|
||||
display: false // Hide the gridlines on the x-axis
|
||||
},
|
||||
ticks: {
|
||||
autoSkip: true,
|
||||
maxTicksLimit: 2
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
gridLines: {
|
||||
display: false // Hide the gridlines on the y-axis
|
||||
},
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
precision: 0,
|
||||
stepSize: 1
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Position the chart window to the right of the button
|
||||
var buttonRect = elem.getBoundingClientRect();
|
||||
var chartRect = chartWindow.getBoundingClientRect();
|
||||
if (window.innerWidth < 700) {
|
||||
chartWindow.style.left = 'calc(75% - ' + (chartRect.width / 2) + 'px)';
|
||||
} else {
|
||||
chartWindow.style.left = (buttonRect.right + 10) + 'px';
|
||||
}
|
||||
|
||||
// Calculate the top position of the chart to center it with the button
|
||||
var buttonCenter = buttonRect.top + (buttonRect.height / 2);
|
||||
var chartHeight = chartWindow.offsetHeight;
|
||||
var chartTop = buttonCenter - (chartHeight / 2);
|
||||
chartWindow.style.top = chartTop + 'px';
|
||||
|
||||
// Add a close button to the chart window
|
||||
var closeButton = document.createElement('button');
|
||||
closeButton.id = "chartcb";
|
||||
closeButton.innerText = 'X';
|
||||
closeButton.style.position = 'absolute';
|
||||
closeButton.style.top = '5px';
|
||||
closeButton.style.right = '5px';
|
||||
closeButton.addEventListener('click', function() {
|
||||
document.body.removeChild(chartWindow);
|
||||
});
|
||||
chartWindow.appendChild(closeButton);
|
||||
window.chartWindow = chartWindow;
|
||||
}
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
// Listen for the scroll event on the window object
|
||||
window.addEventListener('scroll', function() {
|
||||
// Get all chart elements
|
||||
var charts = document.querySelectorAll('.chartdiv');
|
||||
|
||||
// Loop through all chart elements and remove them
|
||||
charts.forEach(function(chart) {
|
||||
chart.parentNode.removeChild(chart);
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -41,6 +41,9 @@ sudo_with_user () {
|
||||
set +x
|
||||
}
|
||||
|
||||
# Get current HEAD hash
|
||||
commit_hash=$(sudo_with_user git -C $HOME/BirdNET-Pi rev-parse HEAD)
|
||||
|
||||
# Reset current HEAD to remove any local changes
|
||||
sudo_with_user git -C $HOME/BirdNET-Pi reset --hard
|
||||
|
||||
@@ -50,6 +53,9 @@ sudo_with_user git -C $HOME/BirdNET-Pi fetch $remote $branch
|
||||
# Switches git to specified branch
|
||||
sudo_with_user git -C $HOME/BirdNET-Pi switch -C $branch --track $remote/$branch
|
||||
|
||||
# Prints out changes
|
||||
sudo_with_user git -C $HOME/BirdNET-Pi diff --stat $commit_hash HEAD
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo ln -sf $my_dir/* /usr/local/bin/
|
||||
|
||||
|
||||
@@ -7,8 +7,17 @@ HOME=$(awk -F: '/1000/ {print $6}' /etc/passwd)
|
||||
my_dir=$HOME/BirdNET-Pi/scripts
|
||||
|
||||
# Sets proper permissions and ownership
|
||||
sudo -E chown -R $USER:$USER $HOME/*
|
||||
sudo chmod -R g+wr $HOME/*
|
||||
#sudo -E chown -R $USER:$USER $HOME/*
|
||||
#sudo chmod -R g+wr $HOME/*
|
||||
find $HOME/Bird* -type f ! -perm -g+wr -exec chmod g+wr {} + 2>/dev/null
|
||||
find $HOME/Bird* -not -user $USER -execdir sudo -E chown $USER:$USER {} \+
|
||||
chmod 666 ~/BirdNET-Pi/scripts/*.txt
|
||||
chmod 666 ~/BirdNET-Pi/*.txt
|
||||
find $HOME/BirdNET-Pi -path "$HOME/BirdNET-Pi/birdnet" -prune -o -type f ! -perm /o=w -exec chmod a+w {} \;
|
||||
|
||||
# remove world-writable perms
|
||||
chmod -R o-w ~/BirdNET-Pi/templates/*
|
||||
|
||||
|
||||
# Create blank sitename as it's optional. First time install will use $HOSTNAME.
|
||||
if ! grep SITE_NAME /etc/birdnet/birdnet.conf &>/dev/null;then
|
||||
@@ -66,7 +75,7 @@ fi
|
||||
apprise_installation_status=$(~/BirdNET-Pi/birdnet/bin/python3 -c 'import pkgutil; print("installed" if pkgutil.find_loader("apprise") else "not installed")')
|
||||
if [[ "$apprise_installation_status" = "not installed" ]];then
|
||||
$HOME/BirdNET-Pi/birdnet/bin/pip3 install -U pip
|
||||
$HOME/BirdNET-Pi/birdnet/bin/pip3 install apprise
|
||||
$HOME/BirdNET-Pi/birdnet/bin/pip3 install apprise==1.2.1
|
||||
fi
|
||||
[ -f $HOME/BirdNET-Pi/apprise.txt ] || sudo -E -ucaddy touch $HOME/BirdNET-Pi/apprise.txt
|
||||
if ! which lsof &>/dev/null;then
|
||||
@@ -125,8 +134,7 @@ if ! grep '\-\-browser.gatherUsageStats false' $HOME/BirdNET-Pi/templates/birdne
|
||||
fi
|
||||
|
||||
# Make IceCast2 a little more secure
|
||||
sudo sed -i 's|<!-- <bind-address>.*|<bind-address>127.0.0.1</bind-address>|;s|<!-- <shoutcast-mount>.*|<shoutcast-mount>/stream</shoutcast-mount>|' /etc/icecast2/icecast.xml
|
||||
sudo systemctl restart icecast2
|
||||
sudo sed -i.bak -e 's|<!-- <bind-address>.*|<bind-address>127.0.0.1</bind-address>|;s|<!-- <shoutcast-mount>.*|<shoutcast-mount>/stream</shoutcast-mount>|' /etc/icecast2/icecast.xml && if [ -s /etc/icecast2/icecast.xml.bak ] && ! sudo diff /etc/icecast2/icecast.xml /etc/icecast2/icecast.xml.bak > /dev/null; then sudo systemctl restart icecast2; fi
|
||||
|
||||
if ! grep FREQSHIFT_TOOL /etc/birdnet/birdnet.conf &>/dev/null;then
|
||||
sudo -u$USER echo "FREQSHIFT_TOOL=sox" >> /etc/birdnet/birdnet.conf
|
||||
@@ -140,6 +148,12 @@ fi
|
||||
if ! grep FREQSHIFT_PITCH /etc/birdnet/birdnet.conf &>/dev/null;then
|
||||
sudo -u$USER echo "FREQSHIFT_PITCH=-1500" >> /etc/birdnet/birdnet.conf
|
||||
fi
|
||||
if ! grep ACTIVATE_FREQSHIFT_IN_LIVESTREAM /etc/birdnet/birdnet.conf &>/dev/null;then
|
||||
sudo -u$USER echo "ACTIVATE_FREQSHIFT_IN_LIVESTREAM=\"false\"" >> /etc/birdnet/birdnet.conf
|
||||
fi
|
||||
if ! grep FREQSHIFT_RECONNECT_DELAY /etc/birdnet/birdnet.conf &>/dev/null;then
|
||||
sudo -u$USER echo "FREQSHIFT_RECONNECT_DELAY=4000" >> /etc/birdnet/birdnet.conf
|
||||
fi
|
||||
if ! grep HEARTBEAT_URL /etc/birdnet/birdnet.conf &>/dev/null;then
|
||||
sudo -u$USER echo "HEARTBEAT_URL=" >> /etc/birdnet/birdnet.conf
|
||||
fi
|
||||
@@ -157,12 +171,67 @@ CREATE INDEX IF NOT EXISTS "detections_Com_Name" ON "detections" ("Com_Name");
|
||||
CREATE INDEX IF NOT EXISTS "detections_Date_Time" ON "detections" ("Date" DESC, "Time" DESC);
|
||||
EOF
|
||||
|
||||
$HOME/BirdNET-Pi/birdnet/bin/pip3 install apprise==1.2.1 >/dev/null
|
||||
apprise_version=$($HOME/BirdNET-Pi/birdnet/bin/python3 -c "import apprise; print(apprise.__version__)")
|
||||
streamlit_version=$($HOME/BirdNET-Pi/birdnet/bin/pip3 show streamlit 2>/dev/null | grep Version | awk '{print $2}')
|
||||
|
||||
[[ $apprise_version != "1.2.1" ]] && $HOME/BirdNET-Pi/birdnet/bin/pip3 install apprise==1.2.1
|
||||
[[ $streamlit_version != "1.19.0" ]] && $HOME/BirdNET-Pi/birdnet/bin/pip3 install streamlit==1.19.0
|
||||
|
||||
if ! grep -q 'RuntimeMaxSec=' "$HOME/BirdNET-Pi/templates/birdnet_analysis.service"&>/dev/null; then
|
||||
sudo -E sed -i '/\[Service\]/a RuntimeMaxSec=3600' "$HOME/BirdNET-Pi/templates/birdnet_analysis.service"
|
||||
sudo systemctl daemon-reload && restart_services.sh
|
||||
fi
|
||||
|
||||
if ! grep RAW_SPECTROGRAM /etc/birdnet/birdnet.conf &>/dev/null;then
|
||||
sudo -u$USER echo "RAW_SPECTROGRAM=0" >> /etc/birdnet/birdnet.conf
|
||||
fi
|
||||
|
||||
if ! grep CUSTOM_IMAGE /etc/birdnet/birdnet.conf &>/dev/null;then
|
||||
sudo -u$USER echo "CUSTOM_IMAGE=" >> /etc/birdnet/birdnet.conf
|
||||
fi
|
||||
if ! grep CUSTOM_IMAGE_TITLE /etc/birdnet/birdnet.conf &>/dev/null;then
|
||||
sudo -u$USER echo "CUSTOM_IMAGE_TITLE=\"\"" >> /etc/birdnet/birdnet.conf
|
||||
fi
|
||||
|
||||
if ! grep APPRISE_ONLY_NOTIFY_SPECIES_NAMES /etc/birdnet/birdnet.conf &>/dev/null;then
|
||||
sudo -u$USER echo "APPRISE_ONLY_NOTIFY_SPECIES_NAMES=\"\"" >> /etc/birdnet/birdnet.conf
|
||||
fi
|
||||
if ! grep APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2 /etc/birdnet/birdnet.conf &>/dev/null;then
|
||||
sudo -u$USER echo "APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2=\"\"" >> /etc/birdnet/birdnet.conf
|
||||
fi
|
||||
|
||||
if ! grep RTSP_STREAM_TO_LIVESTREAM /etc/birdnet/birdnet.conf &>/dev/null;then
|
||||
sudo -u$USER echo "RTSP_STREAM_TO_LIVESTREAM=\"0\"" >> /etc/birdnet/birdnet.conf
|
||||
fi
|
||||
|
||||
suntime_installation_status=$(~/BirdNET-Pi/birdnet/bin/python3 -c 'import pkgutil; print("installed" if pkgutil.find_loader("suntime") else "not installed")')
|
||||
if [[ "$suntime_installation_status" = "not installed" ]];then
|
||||
$HOME/BirdNET-Pi/birdnet/bin/pip3 install -U pip
|
||||
$HOME/BirdNET-Pi/birdnet/bin/pip3 install suntime
|
||||
fi
|
||||
|
||||
# For new Advanced Setting Logging level options
|
||||
if ! grep LogLevel_BirdnetRecordingService /etc/birdnet/birdnet.conf &>/dev/null;then
|
||||
sudo -u$USER echo "LogLevel_BirdnetRecordingService=\"error\"" >> /etc/birdnet/birdnet.conf
|
||||
fi
|
||||
|
||||
if ! grep LogLevel_LiveAudioStreamService /etc/birdnet/birdnet.conf &>/dev/null;then
|
||||
sudo -u$USER echo "LogLevel_LiveAudioStreamService=\"error\"" >> /etc/birdnet/birdnet.conf
|
||||
fi
|
||||
|
||||
if ! grep LogLevel_SpectrogramViewerService /etc/birdnet/birdnet.conf &>/dev/null;then
|
||||
sudo -u$USER echo "LogLevel_SpectrogramViewerService=\"error\"" >> /etc/birdnet/birdnet.conf
|
||||
fi
|
||||
|
||||
if grep -q '^MODEL=BirdNET_GLOBAL_3K_V2.2_Model_FP16$' /etc/birdnet/birdnet.conf;then
|
||||
language=$(grep "^DATABASE_LANG=" /etc/birdnet/birdnet.conf | cut -d= -f2)
|
||||
sed -i 's/BirdNET_GLOBAL_3K_V2.2_Model_FP16/BirdNET_GLOBAL_3K_V2.3_Model_FP16/' /etc/birdnet/birdnet.conf
|
||||
sed -i 's/BirdNET_GLOBAL_3K_V2.2_Model_FP16/BirdNET_GLOBAL_3K_V2.3_Model_FP16/' $HOME/BirdNET-Pi/scripts/thisrun.txt
|
||||
sed -i 's/BirdNET_GLOBAL_3K_V2.2_Model_FP16/BirdNET_GLOBAL_3K_V2.3_Model_FP16/' $HOME/BirdNET-Pi/birdnet.conf
|
||||
cp -f $HOME/BirdNET-Pi/model/labels.txt $HOME/BirdNET-Pi/model/labels.txt.old
|
||||
sudo chmod +x $HOME/BirdNET-Pi/scripts/install_language_label_nm.sh && $HOME/BirdNET-Pi/scripts/install_language_label_nm.sh -l "$language"
|
||||
fi
|
||||
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
restart_services.sh
|
||||
|
||||
@@ -50,6 +50,16 @@ def sendAppriseNotifications(species, confidence, confidencepct, path,
|
||||
body = settings_dict.get('APPRISE_NOTIFICATION_BODY')
|
||||
sciName, comName = species.split("_")
|
||||
|
||||
APPRISE_ONLY_NOTIFY_SPECIES_NAMES = settings_dict.get('APPRISE_ONLY_NOTIFY_SPECIES_NAMES')
|
||||
if APPRISE_ONLY_NOTIFY_SPECIES_NAMES is not None and APPRISE_ONLY_NOTIFY_SPECIES_NAMES.strip() != "":
|
||||
if any(bird.lower().replace(" ", "") in comName.lower().replace(" ", "") for bird in APPRISE_ONLY_NOTIFY_SPECIES_NAMES.split(",")):
|
||||
return
|
||||
|
||||
APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2 = settings_dict.get('APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2')
|
||||
if APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2 is not None and APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2.strip() != "":
|
||||
if not any(bird.lower().replace(" ", "") in comName.lower().replace(" ", "") for bird in APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2.split(",")):
|
||||
return
|
||||
|
||||
APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES = settings_dict.get('APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES')
|
||||
if APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES != "0":
|
||||
if species_last_notified.get(comName) is not None:
|
||||
@@ -75,9 +85,10 @@ def sendAppriseNotifications(species, confidence, confidencepct, path,
|
||||
if comName not in flickr_images:
|
||||
try:
|
||||
# TODO: Make this work with non-english comnames. Implement the "// convert sci name to English name" logic from overview.php here
|
||||
url = 'https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key='+str(settings_dict.get('FLICKR_API_KEY'))+'&text='+str(
|
||||
comName)+' bird&sort=relevance&per_page=5&media=photos&format=json&license=2%2C3%2C4%2C5%2C6%2C9&nojsoncallback=1'
|
||||
resp = requests.get(url=url)
|
||||
headers = {'User-Agent': 'Python_Flickr/1.0'}
|
||||
url = 'https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key='+str(settings_dict.get('FLICKR_API_KEY'))+'&text='+str(comName)+' bird&sort=relevance&per_page=5&media=photos&format=json&license=2%2C3%2C4%2C5%2C6%2C9&nojsoncallback=1'
|
||||
resp = requests.get(url=url, headers=headers)
|
||||
|
||||
resp.encoding = "utf-8"
|
||||
data = resp.json()["photos"]["photo"][0]
|
||||
|
||||
@@ -171,7 +182,7 @@ def sendAppriseNotifications(species, confidence, confidencepct, path,
|
||||
except sqlite3.Error as e:
|
||||
print(e)
|
||||
print("Database busy")
|
||||
time.sleep(2)
|
||||
timeim.sleep(2)
|
||||
|
||||
if settings_dict.get('APPRISE_NOTIFY_NEW_SPECIES') == "1":
|
||||
try:
|
||||
@@ -220,7 +231,7 @@ def sendAppriseNotifications(species, confidence, confidencepct, path,
|
||||
con.close()
|
||||
except sqlite3.Error:
|
||||
print("Database busy")
|
||||
time.sleep(2)
|
||||
timeim.sleep(2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -100,6 +100,7 @@ if(isset($_GET['ascii'])) {
|
||||
$priorweekcount = $totalcount['COUNT(*)'];
|
||||
|
||||
// really percent changed
|
||||
if($priorweekcount > 0){
|
||||
$percentagediff = round( (($scount - $priorweekcount) / $priorweekcount) * 100 );
|
||||
|
||||
if($percentagediff > 0) {
|
||||
@@ -109,6 +110,9 @@ if(isset($_GET['ascii'])) {
|
||||
}
|
||||
|
||||
echo $com_name." - ".$scount." (".$percentagediff.")<br>";
|
||||
} else {
|
||||
echo $com_name." - ".$scount ."<br>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +210,11 @@ while($detection=$result1->fetchArray(SQLITE3_ASSOC))
|
||||
$totalcount = $result2->fetchArray(SQLITE3_ASSOC);
|
||||
$priorweekcount = $totalcount['COUNT(*)'];
|
||||
|
||||
if ($priorweekcount > 0) {
|
||||
$percentagediff = round( (($scount - $priorweekcount) / $priorweekcount) * 100 );
|
||||
} else {
|
||||
$percentagediff = 0;
|
||||
}
|
||||
|
||||
if($percentagediff > 0) {
|
||||
$percentagediff = "<span style='color:green;font-size:small'>+".$percentagediff."%</span>";
|
||||
|
||||
@@ -11,3 +11,4 @@ sed -i s/'^BIRDWEATHER_ID=$'/"BIRDWEATHER_ID=${birdweather_id}"/g ${birders_conf
|
||||
sed -i s/'^BIRDNETPI_URL=$'/"BIRDNETPI_URL=${birdnetpi_url/\/\//\\\/\\\/}"/g ${birders_conf}
|
||||
sed -i s/'^WEBTERMINAL_URL=$'/"WEBTERMINAL_URL=${extractionlog_url/\/\//\\\/\\\/}"/g ${birders_conf}
|
||||
sed -i s/'^BIRDNETLOG_URL=$'/"BIRDNETLOG_URL=${birdnetlog_url/\/\//\\\/\\\/}"/g ${birders_conf}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user