diff --git a/birdnet.conf-defaults b/birdnet.conf-defaults index 2b6f9f3..e53af20 100644 --- a/birdnet.conf-defaults +++ b/birdnet.conf-defaults @@ -60,7 +60,12 @@ RTSP_STREAM= APPRISE_NOTIFICATION_TITLE="New BirdNET-Pi Detection" APPRISE_NOTIFICATION_BODY="A \$sciname \$comname was just detected with a confidence of \$confidence" -APPRISE_NOTIFY_EACH_DETECTION=false +APPRISE_NOTIFY_EACH_DETECTION=0 +APPRISE_NOTIFY_NEW_SPECIES=0 + +#---------------------- Flickr Images API Configuration -----------------------# + +FLICKR_API_KEY= ################################################################################ #-------------------------------- Defaults ----------------------------------# diff --git a/homepage/images/bnp.png b/homepage/images/bnp.png new file mode 100644 index 0000000..db5334b Binary files /dev/null and b/homepage/images/bnp.png differ diff --git a/homepage/images/lock.svg b/homepage/images/lock.svg new file mode 100644 index 0000000..a3bd577 --- /dev/null +++ b/homepage/images/lock.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/homepage/images/sort_date.svg b/homepage/images/sort_date.svg new file mode 100644 index 0000000..ab7fbd0 --- /dev/null +++ b/homepage/images/sort_date.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/homepage/images/unlock.svg b/homepage/images/unlock.svg new file mode 100644 index 0000000..e8ddb69 --- /dev/null +++ b/homepage/images/unlock.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/homepage/index.php b/homepage/index.php index 03f8c34..536f673 100644 --- a/homepage/index.php +++ b/homepage/index.php @@ -5,7 +5,7 @@ body::-webkit-scrollbar { display:none } - + "; } else { header('WWW-Authenticate: Basic realm="My Realm"'); @@ -46,12 +46,12 @@ if(isset($_GET['stream'])){ } } } else { - echo " + echo "
-

BirdNET-Pi

+

"; } if(isset($_GET['filename'])) { diff --git a/homepage/static/RobotoFlex-Regular.ttf b/homepage/static/RobotoFlex-Regular.ttf new file mode 100644 index 0000000..3f13b17 Binary files /dev/null and b/homepage/static/RobotoFlex-Regular.ttf differ diff --git a/homepage/style.css b/homepage/style.css index ba5e360..3319185 100644 --- a/homepage/style.css +++ b/homepage/style.css @@ -1,6 +1,10 @@ +@font-face { + font-family: 'Roboto Flex' ; + src: url('static/RobotoFlex-Regular.ttf') format('truetype'); +} + * { - font-family: 'Arial', 'Gill Sans', 'Gill Sans MT', - ' Calibri', 'Trebuchet MS', 'sans-serif'; + font-family: 'Roboto Flex', sans-serif; box-sizing: border-box; font-size: medium; } @@ -37,6 +41,9 @@ table { border-spacing: 0; margin-left: auto; margin-right: auto; + box-shadow: 0px 0px 17px 1px rgba(0, 0, 0, 0.10); + border-radius:3px; + overflow: hidden; } td { @@ -44,7 +51,6 @@ td { vertical-align: top; background-color: rgb(219, 255, 235); font-weight: lighter; - border: 2px solid black; text-align: center; } @@ -64,11 +70,16 @@ label { font-weight: bold; } +hr { + border-color:black; +} + button { background-color: transparent; border: none; color: black; cursor: pointer; + transition:background-color 0.2s; } button:hover { @@ -132,6 +143,13 @@ button:hover { margin-left: auto; margin-right: auto; margin-bottom: 15px; + box-shadow: 0px 0px 28px 1px rgba(0, 0, 0, 0.10); !important; + border-radius: 4px; +} + +.topimage { + width:175px; + display:initial !important; } .topnav form { @@ -173,12 +191,11 @@ button:hover { } .stats table { - width: 75%; + width: 60%; } .overview th { background-color: rgb(219, 255, 235); - border: 2px solid black; text-align: center; padding: 12px; } @@ -238,14 +255,16 @@ button:hover { } .views .centered button { - border: 2px solid black; background-color: rgb(219, 255, 235); padding: 12px; + transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + box-shadow:0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%); } .views .centered button:hover { background-color: rgb(159, 226, 155); color: black; + box-shadow:0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%); } .settings { @@ -257,7 +276,6 @@ button:hover { } .settings button { - border: 2px solid black; background-color: rgb(219, 255, 235); padding: 12px; } @@ -284,7 +302,7 @@ button:hover { .customlabels button,.customlabels2 button { padding: 12px; background-color: rgb(219, 255, 235); - border: 2px solid black; + width: 100%; } @@ -339,7 +357,7 @@ button:hover { } .systemcontrols button,.servicecontrols button { - border: 2px solid black; + background-color: rgb(219, 255, 235); padding: 12px; width: 50%; @@ -541,7 +559,7 @@ button:hover { font-size:x-large; background:#dbffeb; padding:5px; - border: 2px solid black; + box-shadow: 0px 0px 5px 2px rgba(0, 0, 0, 0.10); } button.legacyview { color:gray; @@ -552,12 +570,98 @@ button.legacyview { font-size:small; background:#dbffeb; padding:5px; - border: 2px solid black; + transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + box-shadow:0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%); } +button.legacyview:hover { + box-shadow:0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%); +} + button.loadmore { margin-top:10px; font-size:x-large; background:#dbffeb; padding:10px; - border: 2px solid black; + transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + box-shadow:0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%); + } +button.loadmore:hover { + box-shadow:0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%); +} +#searchterm { + transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); +} +#searchterm:hover { + box-shadow:0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%); +} + +tr { + background-color:#9fe29b; +} +.history.centered form { + display:flex; + justify-content: center; +} +.history.centered input { + margin-right:5px; + border:0px; +} +.centered form#views button { + box-shadow: 0px 0px 5px 2px rgba(0, 0, 0, 0.10); + margin:2px; + transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + box-shadow:0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%); +} +.centered form#views button:hover { + box-shadow:0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%); +} +input { + box-shadow: 0px 0px 17px 1px rgba(0, 0, 0, 0.10); +} +dialog { + border:none; +} +dialog::backdrop { + background: repeating-linear-gradient( + 30deg, + rgba(24, 194, 236, 0.2), + rgba(24, 194, 236, 0.2) 1px, + rgba(24, 194, 236, 0.3) 1px, + rgba(24, 194, 236, 0.3) 20px + ); + backdrop-filter: blur(1px) +} +.centered_image_container { + font-size:19px !important; + display:inline-block; + position:relative; + margin-bottom:3px; +} +.centered_image_container img.img1 { + transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + box-shadow:0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%); + cursor:pointer; + height:95%; + position:absolute; + right:110%; + top:0px; + border-radius: 5px; + width:unset; +} +.centered_image_container img.img1:hover{ + opacity:0.8; + box-shadow:0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%); +} +.centered_image_container * { + font-size:19px !important; +} +.centered_image_container form { + margin-bottom:0px; +} +.brbanner { + padding:15px; + background-color:rgb(159, 226, 155); + text-align:center; + font-size:large; +} \ No newline at end of file diff --git a/homepage/views.php b/homepage/views.php index 942f6bf..33f4621 100644 --- a/homepage/views.php +++ b/homepage/views.php @@ -37,16 +37,27 @@ body::-webkit-scrollbar {
@@ -59,7 +70,7 @@ if(isset($_GET['view'])){ if($_GET['view'] == "View Log"){echo "";} if($_GET['view'] == "Overview"){include('overview.php');} if($_GET['view'] == "Today's Detections"){include('todays_detections.php');} - if($_GET['view'] == "Species Stats"){echo "

";include('stats.php');} + if($_GET['view'] == "Species Stats"){include('stats.php');} if($_GET['view'] == "Streamlit"){echo "";} if($_GET['view'] == "Daily Charts"){include('history.php');} if($_GET['view'] == "Tools"){ @@ -80,18 +91,18 @@ if(isset($_GET['view'])){ if($submittedpwd == $caddypwd && $submitteduser == 'birdnet'){ $url = $_SERVER['SERVER_NAME']."/scripts/adminer.php"; echo "
-
- - - - - - - - - -
-
"; +
+ + + + + + + + + +
+
"; } else { header('WWW-Authenticate: Basic realm="My Realm"'); header('HTTP/1.0 401 Unauthorized'); @@ -175,7 +186,7 @@ if(isset($_GET['view'])){ $submitteduser = $_SERVER['PHP_AUTH_USER']; if($submittedpwd == $caddypwd && $submitteduser == 'birdnet'){ #ACCESS THE WEB TERMINAL - echo ""; + echo ""; } else { header('WWW-Authenticate: Basic realm="My Realm"'); header('HTTP/1.0 401 Unauthorized'); @@ -185,25 +196,82 @@ if(isset($_GET['view'])){ } } } elseif(isset($_GET['submit'])) { - 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'); - } - $caddypwd = $config['CADDY_PWD']; - if (!isset($_SERVER['PHP_AUTH_USER'])) { - header('WWW-Authenticate: Basic realm="My Realm"'); - header('HTTP/1.0 401 Unauthorized'); - echo 'You cannot access the web terminal'; - exit; - } else { - $submittedpwd = $_SERVER['PHP_AUTH_PW']; - $submitteduser = $_SERVER['PHP_AUTH_USER']; - if($submittedpwd == $caddypwd && $submitteduser == 'birdnet'){ - $command = $_GET['submit']; - if(isset($command)){ - $results = shell_exec("$command 2>&1"); - echo "
$results
"; + 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'); + } + $caddypwd = $config['CADDY_PWD']; + if (!isset($_SERVER['PHP_AUTH_USER'])) { + header('WWW-Authenticate: Basic realm="My Realm"'); + header('HTTP/1.0 401 Unauthorized'); + echo 'You cannot access the web terminal'; + exit; + } 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', + 'sudo systemctl stop web_terminal.service', + 'sudo systemctl restart web_terminal.service', + 'sudo systemctl disable --now web_terminal.service', + 'sudo systemctl enable --now web_terminal.service', + 'sudo systemctl stop birdnet_log.service', + 'sudo systemctl restart birdnet_log.service', + 'sudo systemctl disable --now birdnet_log.service', + 'sudo systemctl enable --now birdnet_log.service', + 'sudo systemctl stop extraction.service', + 'sudo systemctl restart extraction.service', + 'sudo systemctl disable --now extraction.service', + 'sudo systemctl enable --now extraction.service', + 'sudo systemctl stop birdnet_server.service', + 'sudo systemctl restart birdnet_server.service', + 'sudo systemctl disable --now birdnet_server.service', + 'sudo systemctl enable --now birdnet_server.service', + 'sudo systemctl stop birdnet_analysis.service', + 'sudo systemctl restart birdnet_analysis.service', + 'sudo systemctl disable --now birdnet_analysis.service', + 'sudo systemctl enable --now birdnet_analysis.service', + 'sudo systemctl stop birdnet_stats.service', + 'sudo systemctl restart birdnet_stats.service', + 'sudo systemctl disable --now birdnet_stats.service', + 'sudo systemctl enable --now birdnet_stats.service', + 'sudo systemctl stop birdnet_recording.service', + 'sudo systemctl restart birdnet_recording.service', + 'sudo systemctl disable --now birdnet_recording.service', + 'sudo systemctl enable --now birdnet_recording.service', + 'sudo systemctl stop chart_viewer.service', + 'sudo systemctl restart chart_viewer.service', + 'sudo systemctl disable --now chart_viewer.service', + 'sudo systemctl enable --now chart_viewer.service', + 'sudo systemctl stop spectrogram_viewer.service', + 'sudo systemctl restart spectrogram_viewer.service', + 'sudo systemctl disable --now spectrogram_viewer.service', + 'sudo systemctl enable --now spectrogram_viewer.service', + 'stop_core_services.sh', + 'restart_services.sh', + 'sudo reboot', + 'update_birdnet.sh', + 'sudo shutdown now', + 'sudo clear_all_data.sh'); + $command = $_GET['submit']; + if($submittedpwd == $caddypwd && $submitteduser == 'birdnet' && in_array($command,$allowedCommands)){ + if(isset($command)){ + $initcommand = $command; + if (strpos($command, "systemctl") !== false) { + $tmp = explode(" ",trim($command)); + $command .= "& sleep 3;sudo systemctl status ".end($tmp); + } + $results = shell_exec("$command 2>&1"); + $results = str_replace("FAILURE", "FAILURE", $results); + $results = str_replace("failed", "failed",$results); + $results = str_replace("active (running)", "active (running)",$results); + if(strlen($results) == 0) { + $results = "This command has no output."; + } + echo "
Output of command:`".$initcommand."`
$results
"; } else { header('WWW-Authenticate: Basic realm="My Realm"'); header('HTTP/1.0 401 Unauthorized'); diff --git a/scripts/advanced.php b/scripts/advanced.php index 1ed2317..25e4e7f 100644 --- a/scripts/advanced.php +++ b/scripts/advanced.php @@ -204,7 +204,7 @@ if (file_exists('./scripts/thisrun.txt')) { >Purge -

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.

+

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.
Note: you can exclude specific files from 'purge' on the Recordings page.


Set Audio Card to 'default' to use PulseAudio (always recommended), or an ALSA recognized sound card device from the output of `aplay -L`.

diff --git a/scripts/birdnet_analysis.sh b/scripts/birdnet_analysis.sh index b9dab47..d951292 100755 --- a/scripts/birdnet_analysis.sh +++ b/scripts/birdnet_analysis.sh @@ -20,11 +20,13 @@ make_thisrun() { make_thisrun &> /dev/null if ! diff ${LAST_RUN} ${THIS_RUN};then echo "The birdnet.conf file has changed" - echo "Reloading services" + if grep REC <(diff $LAST_RUN $THIS_RUN);then + echo "Recording element changed -- restarting 'birdnet_recording.service'" + sudo systemctl stop birdnet_recording.service + sudo rm -rf ${RECS_DIR}/$(date +%B-%Y/%d-%A)/* + sudo systemctl start birdnet_recording.service + fi cat ${THIS_RUN} > ${LAST_RUN} - sudo systemctl stop birdnet_recording.service - sudo rm -rf ${RECS_DIR}/$(date +%B-%Y/%d-%A)/* - sudo systemctl start birdnet_recording.service fi INCLUDE_LIST="$HOME/BirdNET-Pi/include_species_list.txt" diff --git a/scripts/birdnet_log.sh b/scripts/birdnet_log.sh index e480664..1d49023 100755 --- a/scripts/birdnet_log.sh +++ b/scripts/birdnet_log.sh @@ -1,2 +1,2 @@ #!/usr/bin/env bash -journalctl --no-hostname -q -o short -fu birdnet_analysis -ubirdnet_server -uextraction | sed "s/$(date "+%b %d ")//g;s/${HOME//\//\\/}\///g" +journalctl --no-hostname -q -o short -fu birdnet_analysis -ubirdnet_server -uextraction | sed "s/$(date "+%b %d ")//g;s/${HOME//\//\\/}\///g;/Line/d;/systemd/d;s/ .*\[.*\]: /---/" diff --git a/scripts/config.php b/scripts/config.php index b925780..8b13084 100644 --- a/scripts/config.php +++ b/scripts/config.php @@ -10,15 +10,16 @@ $birdweather_id = $_GET["birdweather_id"]; $apprise_input = $_GET['apprise_input']; $apprise_notification_title = $_GET['apprise_notification_title']; $apprise_notification_body = $_GET['apprise_notification_body']; +$flickr_api_key = $_GET['flickr_api_key']; if(isset($_GET['apprise_notify_each_detection'])) { $apprise_notify_each_detection = 1; } else { $apprise_notify_each_detection = 0; } -if(isset($_GET['apprise_notify_each_species'])) { - exec('sudo systemctl start pushed_notifications.service'); +if(isset($_GET['apprise_notify_new_species'])) { + $apprise_notify_new_species = 1; } else { - exec('sudo systemctl stop pushed_notifications.service'); + $apprise_notify_new_species = 0; } // logic for setting the date and time based on user inputs from the form below @@ -45,6 +46,8 @@ $contents = preg_replace("/BIRDWEATHER_ID=.*/", "BIRDWEATHER_ID=$birdweather_id" $contents = preg_replace("/APPRISE_NOTIFICATION_TITLE=.*/", "APPRISE_NOTIFICATION_TITLE=\"$apprise_notification_title\"", $contents); $contents = preg_replace("/APPRISE_NOTIFICATION_BODY=.*/", "APPRISE_NOTIFICATION_BODY=\"$apprise_notification_body\"", $contents); $contents = preg_replace("/APPRISE_NOTIFY_EACH_DETECTION=.*/", "APPRISE_NOTIFY_EACH_DETECTION=$apprise_notify_each_detection", $contents); +$contents = preg_replace("/APPRISE_NOTIFY_NEW_SPECIES=.*/", "APPRISE_NOTIFY_NEW_SPECIES=$apprise_notify_new_species", $contents); +$contents = preg_replace("/FLICKR_API_KEY=.*/", "FLICKR_API_KEY=$flickr_api_key", $contents); $contents2 = file_get_contents("./scripts/thisrun.txt"); @@ -54,6 +57,9 @@ $contents2 = preg_replace("/BIRDWEATHER_ID=.*/", "BIRDWEATHER_ID=$birdweather_id $contents2 = preg_replace("/APPRISE_NOTIFICATION_TITLE=.*/", "APPRISE_NOTIFICATION_TITLE=\"$apprise_notification_title\"", $contents2); $contents2 = preg_replace("/APPRISE_NOTIFICATION_BODY=.*/", "APPRISE_NOTIFICATION_BODY=\"$apprise_notification_body\"", $contents2); $contents2 = preg_replace("/APPRISE_NOTIFY_EACH_DETECTION=.*/", "APPRISE_NOTIFY_EACH_DETECTION=$apprise_notify_each_detection", $contents2); +$contents2 = preg_replace("/APPRISE_NOTIFY_NEW_SPECIES=.*/", "APPRISE_NOTIFY_NEW_SPECIES=$apprise_notify_new_species", $contents2); +$contents2 = preg_replace("/FLICKR_API_KEY=.*/", "FLICKR_API_KEY=$flickr_api_key", $contents2); + $fh = fopen("/etc/birdnet/birdnet.conf", "w"); $fh2 = fopen("./scripts/thisrun.txt", "w"); @@ -76,7 +82,7 @@ if ($language != "none"){ $home = shell_exec("awk -F: '/1000/{print $6}' /etc/passwd"); $home = trim($home); $command = "sudo -u".$user." mv ".$home."/BirdNET-Pi/model/labels.txt ".$home."/BirdNET-Pi/model/labels.txt.old && sudo -u".$user." unzip ".$home."/BirdNET-Pi/model/labels_l18n.zip ".$language." -d ".$home."/BirdNET-Pi/model && sudo -u".$user." mv ".$home."/BirdNET-Pi/model/".$language." ".$home."/BirdNET-Pi/model/labels.txt"; - $command_output = `sudo $command`; + $command_output = `$command`; `sudo restart_services.sh`; } } @@ -140,10 +146,14 @@ https://discordapp.com/api/webhooks/{WebhookID}/{WebhookToken}

- > -
+ > +
>

+

Bird Photos from Flickr

+ +
+

Set your Flickr API key to enable the display of bird images next to detections. Get your free key here.

Localization

-
- -
Confidence:
- - +
+ + + +
+ +
+ +
Confidence:

+ +
+ +

+

+ +
+
0 && !this.responseText.includes("Database is busy.")) { + if(this.responseText.length > 0 && !this.responseText.includes("Database is busy")) { document.getElementById("most_recent_detection").innerHTML = this.responseText; // only going to load left chart if there's a new detection diff --git a/scripts/play.php b/scripts/play.php index ad869be..cf7e30f 100644 --- a/scripts/play.php +++ b/scripts/play.php @@ -1,7 +1,5 @@ prepare('SELECT DISTINCT(Date) FROM detections GROUP BY Date'); - if($statement == False){ - echo "Database is busy"; - header("refresh: 0;"); +if (file_exists('./scripts/thisrun.txt')) { + $config = parse_ini_file('./scripts/thisrun.txt'); +} elseif (file_exists('firstrun.ini')) { + $config = parse_ini_file('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(isset($_GET['excludefile'])) { + if(isset($_SERVER['PHP_AUTH_USER'])) { + $submittedpwd = $_SERVER['PHP_AUTH_PW']; + $submitteduser = $_SERVER['PHP_AUTH_USER']; + if($submittedpwd == $config['CADDY_PWD'] && $submitteduser == 'birdnet'){ + if(!file_exists($home."/BirdNET-Pi/scripts/disk_check_exclude.txt")) { + file_put_contents($home."/BirdNET-Pi/scripts/disk_check_exclude.txt", "##start\n##end"); + } + if(isset($_GET['exclude_add'])) { + $myfile = fopen($home."/BirdNET-Pi/scripts/disk_check_exclude.txt", "a") or die("Unable to open file!"); + $txt = $_GET['excludefile']; + fwrite($myfile, $txt."\n"); + fwrite($myfile, $txt.".png\n"); + fclose($myfile); + echo "OK"; + die(); + } else { + $lines = file($home."/BirdNET-Pi/scripts/disk_check_exclude.txt"); + $search = $_GET['excludefile']; + + $result = ''; + foreach($lines as $line) { + if(stripos($line, $search) === false && stripos($line, $search.".png") === false) { + $result .= $line; + } + } + file_put_contents($home."/BirdNET-Pi/scripts/disk_check_exclude.txt", $result); + echo "OK"; + die(); + } + } else { + header('WWW-Authenticate: Basic realm="My Realm"'); + header('HTTP/1.0 401 Unauthorized'); + echo 'You must be authenticated to change the protection of files.'; + exit; + } + } else { + header('WWW-Authenticate: Basic realm="My Realm"'); + header('HTTP/1.0 401 Unauthorized'); + echo 'You must be authenticated to change the protection of files.'; + exit; } - $result = $statement->execute(); - $view = "bydate"; - #By Date -}elseif(isset($_GET['bydate'])){ - $statement = $db->prepare('SELECT DISTINCT(Date) FROM detections GROUP BY Date'); +} + +if(isset($_GET['bydate'])){ + $statement = $db->prepare('SELECT DISTINCT(Date) FROM detections GROUP BY Date ORDER BY Date DESC'); if($statement == False){ echo "Database is busy"; header("refresh: 0;"); @@ -79,7 +121,6 @@ if(isset($_GET['byfilename'])){ session_unset(); $view = "choose"; } - ?> @@ -90,6 +131,31 @@ if(isset($_GET['byfilename'])){ + +
- @@ -121,13 +186,15 @@ if(!isset($_GET['species']) && !isset($_GET['filename'])){ if($view == "bydate") { while($results=$result->fetchArray(SQLITE3_ASSOC)){ $date = $results['Date']; + if(realpath($home."/BirdSongs/Extracted/By_Date/".$date) !== false){ echo "";} + ";}} #By Species } elseif($view == "byspecies") { while($results=$result->fetchArray(SQLITE3_ASSOC)){ $name = $results['Com_Name']; + echo "";} @@ -135,8 +202,11 @@ if(!isset($_GET['species']) && !isset($_GET['filename'])){ } elseif($view == "date") { while($results=$result->fetchArray(SQLITE3_ASSOC)){ $name = $results['Com_Name']; - echo "";} + if(realpath($home."/BirdSongs/Extracted/By_Date/".$date."/".str_replace(" ", "_",$name)) !== false){ + echo ""; + } + } #Choose } else { @@ -151,13 +221,42 @@ if(!isset($_GET['species']) && !isset($_GET['filename'])){ } #Specific Species -if(isset($_GET['species'])){ +if(isset($_GET['species'])){ ?> +
+ + + + + + +
+prepare("SELECT * FROM detections where Com_Name == \"$name\" AND Date == \"$date\" ORDER BY Time DESC"); + if(isset($_GET['sort']) && $_GET['sort'] == "confidence") { + $statement2 = $db->prepare("SELECT * FROM detections where Com_Name == \"$name\" AND Date == \"$date\" ORDER BY Confidence DESC"); + } else { + $statement2 = $db->prepare("SELECT * FROM detections where Com_Name == \"$name\" AND Date == \"$date\" ORDER BY Time DESC"); + } } else { - $statement2 = $db->prepare("SELECT * FROM detections where Com_Name == \"$name\" ORDER BY Date DESC, Time DESC");} + if(isset($_GET['sort']) && $_GET['sort'] == "confidence") { + $statement2 = $db->prepare("SELECT * FROM detections where Com_Name == \"$name\" ORDER BY Confidence DESC"); + } else { + $statement2 = $db->prepare("SELECT * FROM detections where Com_Name == \"$name\" ORDER BY Date DESC, Time DESC"); + } + } if($statement2 == False){ echo "Database is busy"; header("refresh: 0;"); @@ -167,6 +266,7 @@ if(isset($_GET['species'])){ "; + $iter=0; while($results=$result2->fetchArray(SQLITE3_ASSOC)) { $comname = preg_replace('/ /', '_', $results['Com_Name']); @@ -176,13 +276,40 @@ if(isset($_GET['species'])){ $sciname = preg_replace('/ /', '_', $results['Sci_Name']); $sci_name = $results['Sci_Name']; $time = $results['Time']; - $confidence = $results['Confidence']; - echo " - - "; + $confidence = round((float)round($results['Confidence'],2) * 100 ) . '%'; + $filename_formatted = $date."/".$comname."/".$results['File_Name']; - }echo "
-
-
+
$name
$date $time
$confidence
-
";} + // file was deleted by disk check, no need to show the detection in recordings + if(!file_exists($home."/BirdSongs/Extracted/".$filename)) { + continue; + } + $iter++; + + if($config["FULL_DISK"] == "purge") { + if(!in_array($filename_formatted, $disk_check_exclude_arr)) { + $imageicon = "images/unlock.svg"; + $title = "This file is not delete protected."; + $type = "add"; + } else { + $imageicon = "images/lock.svg"; + $title = "This file is delete protected."; + $type = "del"; + } + + echo " + $date $time
$confidence
+ + + "; + } else { + echo " + $date $time
$confidence
+ + + "; + } + + }if($iter == 0){ echo "No recordings were found.

They may have been deleted to make space for new recordings. You can modify this setting for the future in Tools -> Settings -> Advanced Settings -> Full Disk Behavior.";}echo "";} if(isset($_GET['filename'])){ $name = $_GET['filename']; @@ -205,11 +332,36 @@ if(isset($_GET['filename'])){ $sciname = preg_replace('/ /', '_', $results['Sci_Name']); $sci_name = $results['Sci_Name']; $time = $results['Time']; - $confidence = $results['Confidence']; - echo " - $date $time
$confidence
- - "; + $confidence = round((float)round($results['Confidence'],2) * 100 ) . '%'; + $filename_formatted = $date."/".$comname."/".$results['File_Name']; + + // add disk_check_exclude.txt lines into an array for grepping + $fp = @fopen($home."/BirdNET-Pi/scripts/disk_check_exclude.txt", 'r'); + if ($fp) { + $disk_check_exclude_arr = explode("\n", fread($fp, filesize($home."/BirdNET-Pi/scripts/disk_check_exclude.txt"))); + } + + if($config["FULL_DISK"] == "purge") { + if(!in_array($filename_formatted, $disk_check_exclude_arr)) { + $imageicon = "images/unlock.svg"; + $title = "This file is not delete protected."; + $type = "add"; + } else { + $imageicon = "images/lock.svg"; + $title = "This file is delete protected."; + $type = "del"; + } + + echo " + $date $time
$confidence
+ + "; + } else { + echo " + $date $time
$confidence
+ + "; + } }echo "";}?>
diff --git a/scripts/restart_services.sh b/scripts/restart_services.sh index f0cead9..ad4e9f3 100755 --- a/scripts/restart_services.sh +++ b/scripts/restart_services.sh @@ -9,7 +9,6 @@ sudo pkill server.py sudo systemctl stop birdnet_recording.service services=(web_terminal.service spectrogram_viewer.service -pushed_notifications.service livestream.service icecast2.service extraction.service @@ -22,6 +21,6 @@ sudo systemctl restart "${i}" done until grep 5050 <(netstat -tulpn 2>&1);do sudo systemctl restart birdnet_server.service -sleep 20 +sleep 45 done sudo systemctl restart birdnet_analysis.service diff --git a/scripts/server.py b/scripts/server.py index 6c96e12..38a06fc 100755 --- a/scripts/server.py +++ b/scripts/server.py @@ -243,6 +243,33 @@ def sendAppriseNotifications(species,confidence): title=title, ) + if str(str(str([i for i in this_run if i.startswith('APPRISE_NOTIFY_NEW_SPECIES')]).split('=')[1]).split('\\')[0]) == "1": + try: + con = sqlite3.connect(userDir + '/BirdNET-Pi/scripts/birds.db') + con.row_factory = lambda cursor, row: row[0] + cur = con.cursor() + cur.execute("SELECT DISTINCT(Com_Name) FROM detections") + known_species = cur.fetchall() + sciName,comName = species.split("_") + + print("\ncomName: ",comName) + print("\nknown_species: ",known_species) + if comName not in known_species: + apobj = apprise.Apprise() + config = apprise.AppriseConfig() + config.add(userDir + '/BirdNET-Pi/apprise.txt') + apobj.add(config) + + apobj.notify( + body=body.replace("$sciname",species.split("_")[0]).replace("$comname",species.split("_")[1]).replace("$confidence",confidence), + title=title, + ) + + con.close() + except: + print("Database busy") + time.sleep(2) + def writeResultsToFile(detections, min_conf, path): print('WRITING RESULTS TO', path, '...', end=' ') @@ -261,7 +288,7 @@ def writeResultsToFile(detections, min_conf, path): def handle_client(conn, addr): global INCLUDE_LIST global EXCLUDE_LIST - print(f"[NEW CONNECTION] {addr} connected.") + #print(f"[NEW CONNECTION] {addr} connected.") connected = True while connected: @@ -335,7 +362,7 @@ def handle_client(conn, addr): # Get Date/Time from filename in case Pi gets behind #now = datetime.now() full_file_name = args.i - print('FULL FILENAME: -' + full_file_name + '-') + #print('FULL FILENAME: -' + full_file_name + '-') file_name = Path(full_file_name).stem file_date = file_name.split('-birdnet-')[0] file_time = file_name.split('-birdnet-')[1] @@ -463,13 +490,13 @@ def start(): global INTERPRETER, INCLUDE_LIST, EXCLUDE_LIST INTERPRETER = loadModel() server.listen() - print(f"[LISTENING] Server is listening on {SERVER}") + #print(f"[LISTENING] Server is listening on {SERVER}") while True: conn, addr = server.accept() thread = threading.Thread(target=handle_client, args=(conn, addr)) thread.start() - print(f"[ACTIVE CONNECTIONS] {threading.activeCount() - 1}") + #print(f"[ACTIVE CONNECTIONS] {threading.activeCount() - 1}") -print("[STARTING] server is starting...") +#print("[STARTING] server is starting...") start() diff --git a/scripts/service_controls.php b/scripts/service_controls.php index 8abde5e..e81952b 100644 --- a/scripts/service_controls.php +++ b/scripts/service_controls.php @@ -73,13 +73,6 @@ -
-

Pushed Notifications

- - - - -
diff --git a/scripts/spectrogram.php b/scripts/spectrogram.php index 8140cfc..f95ee72 100644 --- a/scripts/spectrogram.php +++ b/scripts/spectrogram.php @@ -1,16 +1,160 @@ - +// CREDITS: https://codepen.io/jakealbaugh/pen/jvQweW + +// UPDATE: there is a problem in chrome with starting audio context +// before a user gesture. This fixes it. +var started = null; +var player = null; +const ctx = null; +window.onload = function(){ + // if user agent includes iPhone or Mac use legacy mode + if(window.navigator.userAgent.includes("iPhone") || window.navigator.userAgent.includes("Mac")) { + document.getElementById("spectrogramimage").style.display=""; + document.body.querySelector('canvas').remove(); + document.getElementById('player').remove(); + document.body.querySelector('h1').remove(); + + + // every $refresh seconds, this loop will run and refresh the spectrogram image + window.setInterval(function(){ + document.getElementById("spectrogramimage").src = "/spectrogram.png?nocache="+Date.now(); + }, *1000); + } else { + document.getElementById("spectrogramimage").remove(); + + var audioelement = window.parent.document.getElementsByTagName("audio")[0]; + if (typeof(audioelement) != 'undefined') { + + document.getElementById('player').remove(); + + player = audioelement; + } else { + player = document.getElementById('player'); + } + player.play(); + if (started) return; + started = true; + initialize(); + } +}; + +function fitTextOnCanvas(text,fontface,yPosition){ + var fontsize=300; + do{ + fontsize--; + CTX.font=fontsize+"px "+fontface; + }while(CTX.measureText(text).width>document.body.querySelector('canvas').width) + CTX.font = CTX.font=(fontsize*0.35)+"px "+fontface; + CTX.fillText(text,document.body.querySelector('canvas').width - (document.body.querySelector('canvas').width * 0.50),yPosition); } -$refresh = $config['RECORDING_LENGTH']; -$time = time(); -echo ""; -?> - + + + + + +

Loading...

+ diff --git a/scripts/stats.php b/scripts/stats.php index 230a54a..3ad11f0 100644 --- a/scripts/stats.php +++ b/scripts/stats.php @@ -56,7 +56,10 @@ if(isset($_GET['species'])){ $user = shell_exec("awk -F: '/1000/{print $1}' /etc/passwd"); $home = shell_exec("awk -F: '/1000/{print $6}' /etc/passwd"); $home = trim($home); -file_put_contents($home."/BirdNET-Pi/scripts/disk_check_exclude.txt", ""); +if(!file_exists($home."/BirdNET-Pi/scripts/disk_check_exclude.txt") || strpos(file_get_contents($home."/BirdNET-Pi/scripts/disk_check_exclude.txt"),"##start") === false) { + file_put_contents($home."/BirdNET-Pi/scripts/disk_check_exclude.txt", ""); + file_put_contents($home."/BirdNET-Pi/scripts/disk_check_exclude.txt", "##start\n##end\n"); +} ?> @@ -78,10 +81,10 @@ file_put_contents($home."/BirdNET-Pi/scripts/disk_check_exclude.txt", "");
@@ -106,17 +109,40 @@ $filename = "/By_Date/".$results['Date']."/".$comname."/".$results['File_Name'];
+ +

+

+ +
+

Choose a species to load images from Wikimedia Commons.

+?>

Choose a species to load images from Flickr.

fetchArray(SQLITE3_ASSOC)){ $count = $results['COUNT(*)']; - $maxconf = $results['MAX(Confidence)']; + $maxconf = round((float)round($results['MAX(Confidence)'],2) * 100 ) . '%'; $date = $results['Date']; $time = $results['Time']; $name = $results['Com_Name']; @@ -136,51 +162,67 @@ while($results=$result3->fetchArray(SQLITE3_ASSOC)){ -

Loading Images from Wikimedia Commons

", '6096'); +

Loading Images from Flickr

", '6096'); echo ""; ob_flush(); flush(); - $imagelink = "https://commons.wikimedia.org/w/index.php?search=$linkname&title=Special:MediaSearch&go=Go&type=image"; - $homepage = file_get_contents($imagelink); - preg_match_all("{}ims", $homepage, $matches, PREG_SET_ORDER); - foreach ($matches as $val) { - $pos = strpos($val[2],"/"); - $link = substr($val[2],1,-1); - if($pos !== 1 && strpos($link, "upload") == true && strpos($link, "CentralAutoLogin") == false) - echo ""; + 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["FLICKR_API_KEY"])) { + $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('_', '+', $comname)."\"&license=2%2C3%2C4%2C5%2C6%2C9&sort=relevance&per_page=15&format=json&nojsoncallback=1"), true)["photos"]["photo"]; + + foreach ($flickrjson as $val) { + $iter++; + $modaltext = "https://flickr.com/photos/".$val["owner"]."/".$val["id"]; + $authorlink = "https://flickr.com/people/".$val["owner"]; + $imageurl = 'https://farm' .$val["farm"]. '.static.flickr.com/' .$val["server"]. '/' .$val["id"]. '_' .$val["secret"]. '.jpg'; + echo ""; + } + } +} +} ?> -


+ +

+
Best Recordings for Other Species:

+ +

+ fetchArray(SQLITE3_ASSOC)) { $comname = preg_replace('/ /', '_', $results['Com_Name']); $comname = preg_replace('/\'/', '', $comname); $filename = "/By_Date/".$results['Date']."/".$comname."/".$results['File_Name']; -$excludefile = fopen($home."/BirdNET-Pi/scripts/disk_check_exclude.txt", "a") or die("Unable to open file!"); -$txt = $results['Date']."/".$comname."/".$results['File_Name']."\n".$results['Date']."/".$comname."/".$results['File_Name'].".png\n"; -fwrite($excludefile, $txt); +array_push($excludelines, $results['Date']."/".$comname."/".$results['File_Name']); +array_push($excludelines, $results['Date']."/".$comname."/".$results['File_Name'].".png"); ?>

Occurrences:
- Max Confidence:
+ Max Confidence:
Best Recording:
- \ No newline at end of file + diff --git a/scripts/todays_detections.php b/scripts/todays_detections.php index 1b4db75..b37b89d 100644 --- a/scripts/todays_detections.php +++ b/scripts/todays_detections.php @@ -1,4 +1,7 @@ prepare('SELECT Time, Com_Name, Sci_Name, Confidence, File_Name FROM detections WHERE Date == Date(\'now\', \'localtime\') '.$searchquery.' ORDER BY Time DESC LIMIT '.(intval($_GET['display_limit'])-40).',40'); } else { // legacy mode - $statement0 = $db->prepare('SELECT Time, Com_Name, Sci_Name, Confidence, File_Name FROM detections WHERE Date == Date(\'now\', \'localtime\') '.$searchquery.' ORDER BY Time DESC'.$searchquery); + $statement0 = $db->prepare('SELECT Time, Com_Name, Sci_Name, Confidence, File_Name FROM detections WHERE Date == Date(\'now\', \'localtime\') '.$searchquery.' ORDER BY Time DESC'); } if($statement0 == False){ echo "Database is busy"; @@ -77,7 +80,18 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" ) { ?> fetchArray(SQLITE3_ASSOC)) { $iterations++; @@ -86,13 +100,35 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" ) { $comname = preg_replace('/\'/', '_', $comname); $filename = "/By_Date/".date('Y-m-d')."/".$comname."/".$todaytable['File_Name']; $sciname = preg_replace('/ /', '_', $todaytable['Sci_Name']); + + if (!empty($config["FLICKR_API_KEY"])) { + // if we already searched flickr for this species before, use the previous image rather than doing an unneccesary api call + $key = array_search($comname, array_column($_SESSION['images'], 0)); + if($key !== false) { + $image = $_SESSION['images'][$key]; + } else { + $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", $comname)."&license=2%2C3%2C4%2C5%2C6%2C9&sort=relevance&per_page=5&orientation=square,portrait&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 = 'http://farm' .$flickrjson["farm"]. '.static.flickr.com/' .$flickrjson["server"]. '/' .$flickrjson["id"]. '_' .$flickrjson["secret"]. '.jpg'; + array_push($_SESSION['images'], array($comname,$imageurl,$flickrjson["title"], $modaltext, $authorlink)); + $image = $_SESSION['images'][count($_SESSION['images'])-1]; + } + } ?> - @@ -100,7 +136,7 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" ) { - + @@ -132,6 +168,29 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" ) {
+ +

+

+ +
+

Number of Detections


+
+ +
+ + + + +


- Confidence:
+ Confidence:




Confidence:
Confidence:
@@ -158,7 +217,7 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" ) {

Today's Detections —

-
+

Loading...

@@ -188,7 +247,11 @@ document.getElementById("searchterm").onkeydown = (function(e) { }); function switchViews(element) { - document.getElementById("detections_table").innerHTML = ""; + if(searchterm == ""){ + document.getElementById("detections_table").innerHTML = "

Loading detections...

"; + } else { + document.getElementById("detections_table").innerHTML = "

Loading...

"; + } if(element.innerHTML == "Legacy view") { element.innerHTML = "Normal view"; loadDetections(undefined); @@ -200,7 +263,11 @@ function switchViews(element) { function searchDetections(searchvalue) { document.getElementById("detections_table").innerHTML = "

Loading...

"; searchterm = searchvalue; - loadDetections(40,undefined); + if(document.getElementsByClassName('legacyview')[0].innerHTML == "Normal view") { + loadDetections(undefined,undefined); + } else { + loadDetections(40,undefined); + } } function loadDetections(detections_limit, element=undefined) { const xhttp = new XMLHttpRequest(); @@ -214,7 +281,7 @@ function loadDetections(detections_limit, element=undefined) { } } - if(searchterm !== ""){ + if(searchterm != ""){ xhttp.open("GET", "todays_detections.php?ajax_detections=true&display_limit="+detections_limit+"&searchterm="+searchterm, true); } else { xhttp.open("GET", "todays_detections.php?ajax_detections=true&display_limit="+detections_limit, true); diff --git a/scripts/update_birdnet_snippets.sh b/scripts/update_birdnet_snippets.sh index e9c89d3..912574d 100755 --- a/scripts/update_birdnet_snippets.sh +++ b/scripts/update_birdnet_snippets.sh @@ -30,7 +30,10 @@ if ! grep APPRISE_NOTIFICATION_BODY /etc/birdnet/birdnet.conf &>/dev/null;then sudo -u$USER echo "APPRISE_NOTIFICATION_BODY=\"A \$sciname \$comname was just detected with a confidence of \$confidence\"" >> /etc/birdnet/birdnet.conf fi if ! grep APPRISE_NOTIFY_EACH_DETECTION /etc/birdnet/birdnet.conf &>/dev/null;then - sudo -u$USER echo "APPRISE_NOTIFY_EACH_DETECTION=false" >> /etc/birdnet/birdnet.conf + sudo -u$USER echo "APPRISE_NOTIFY_EACH_DETECTION=0 " >> /etc/birdnet/birdnet.conf +fi +if ! grep APPRISE_NOTIFY_NEW_SPECIES /etc/birdnet/birdnet.conf &>/dev/null;then + sudo -u$USER echo "APPRISE_NOTIFY_EACH_DETECTION=0 " >> /etc/birdnet/birdnet.conf 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 @@ -49,3 +52,12 @@ if grep bash $HOME/BirdNET-Pi/templates/web_terminal.service;then sudo systemctl daemon-reload sudo systemctl restart web_terminal.service fi +[ -L ~/BirdSongs/Extracted/static ] || ln -sf ~/BirdNET-Pi/homepage/static ~/BirdSongs/Extracted +if ! grep FLICKR_API_KEY /etc/birdnet/birdnet.conf &>/dev/null;then + sudo -u$USER echo "FLICKR_API_KEY=" >> /etc/birdnet/birdnet.conf +fi +if systemctl list-unit-files pushed_notifications.service;then + sudo systemctl disable --now pushed_notifications.service + sudo rm -f /usr/lib/systemd/system/pushed_notifications.service + sudo rm $HOME/BirdNET-Pi/templates/pushed_notifications.service +fi