diff --git a/README.md b/README.md index 699facc..df8e235 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Check out birds from around the world - [Norman, Oklahoma, United States](https://normanok.birdnetpi.com)
- [Nijmegen, the Netherlands](https://nijmegen.birdnetpi.com)
- [Murrysville, Pennsylvania, United States](https://murrysvillepa.birdnetpi.com/) +- [Berowra, New South Wales, Australia](https://berowra.birdnetpi.com/) [Share your installation!!](https://github.com/mcguirepr89/BirdNET-Pi/wiki/Sharing-Your-BirdNET-Pi) diff --git a/birdnet.conf-defaults b/birdnet.conf-defaults index 2b6f9f3..e75cda1 100644 --- a/birdnet.conf-defaults +++ b/birdnet.conf-defaults @@ -62,6 +62,10 @@ 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 +#---------------------- Flickr Images API Configuration -----------------------# + +FLICKR_API_KEY= + ################################################################################ #-------------------------------- Defaults ----------------------------------# ################################################################################ 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/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..b43b2c3 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; } @@ -132,6 +138,8 @@ button:hover { margin-left: auto; margin-right: auto; margin-bottom: 15px; + box-shadow: 0px 0px 17px 1px rgba(0, 0, 0, 0.10); !important; + border-radius: 4px; } .topnav form { @@ -178,7 +186,6 @@ button:hover { .overview th { background-color: rgb(219, 255, 235); - border: 2px solid black; text-align: center; padding: 12px; } @@ -238,7 +245,6 @@ button:hover { } .views .centered button { - border: 2px solid black; background-color: rgb(219, 255, 235); padding: 12px; } @@ -257,7 +263,6 @@ button:hover { } .settings button { - border: 2px solid black; background-color: rgb(219, 255, 235); padding: 12px; } @@ -284,7 +289,7 @@ button:hover { .customlabels button,.customlabels2 button { padding: 12px; background-color: rgb(219, 255, 235); - border: 2px solid black; + width: 100%; } @@ -339,7 +344,7 @@ button:hover { } .systemcontrols button,.servicecontrols button { - border: 2px solid black; + background-color: rgb(219, 255, 235); padding: 12px; width: 50%; @@ -541,7 +546,7 @@ button:hover { font-size:x-large; background:#dbffeb; padding:5px; - border: 2px solid black; + } button.legacyview { color:gray; @@ -552,12 +557,65 @@ button.legacyview { font-size:small; background:#dbffeb; padding:5px; - border: 2px solid black; + } button.loadmore { margin-top:10px; font-size:x-large; background:#dbffeb; padding:10px; - border: 2px solid black; + } + +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; +} +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 { + cursor:pointer; + height:95%; + position:absolute; + right:110%; + top:0px; + border-radius: 5px; + width:unset; +} +.centered_image_container * { + font-size:19px !important; +} +.centered_image_container form { + margin-bottom:0px; +} \ No newline at end of file diff --git a/homepage/views.php b/homepage/views.php index 942f6bf..e190569 100644 --- a/homepage/views.php +++ b/homepage/views.php @@ -47,6 +47,17 @@ body::-webkit-scrollbar { } else { topbuttons[0].classList.add("button-hover"); } + function copyOutput(elem) { + elem.innerHTML = 'Copied!'; + const copyText = document.getElementsByTagName("pre")[0].textContent; + const textArea = document.createElement('textarea'); + textArea.style.position = 'absolute'; + textArea.style.left = '-100%'; + textArea.textContent = copyText; + document.body.append(textArea); + textArea.select(); + document.execCommand("copy"); + }
@@ -80,7 +91,7 @@ if(isset($_GET['view'])){ if($submittedpwd == $caddypwd && $submitteduser == 'birdnet'){ $url = $_SERVER['SERVER_NAME']."/scripts/adminer.php"; echo "
-
+ @@ -90,8 +101,8 @@ if(isset($_GET['view'])){ -
-
"; + +
"; } 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'); @@ -201,9 +212,20 @@ if(isset($_GET['view'])){ $submitteduser = $_SERVER['PHP_AUTH_USER']; if($submittedpwd == $caddypwd && $submitteduser == 'birdnet'){ $command = $_GET['submit']; - if(isset($command)){ + 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"); - echo "
$results
"; + $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_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 c662dfb..4fecbba 100644 --- a/scripts/config.php +++ b/scripts/config.php @@ -10,6 +10,7 @@ $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 { @@ -45,6 +46,7 @@ $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("/FLICKR_API_KEY=.*/", "FLICKR_API_KEY=$flickr_api_key", $contents); $contents2 = file_get_contents("./scripts/thisrun.txt"); @@ -54,6 +56,8 @@ $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("/FLICKR_API_KEY=.*/", "FLICKR_API_KEY=$flickr_api_key", $contents2); + $fh = fopen("/etc/birdnet/birdnet.conf", "w"); $fh2 = fopen("./scripts/thisrun.txt", "w"); @@ -144,6 +148,10 @@ 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

>
+ >
> >


diff --git a/scripts/daily_plot.py b/scripts/daily_plot.py index e2147b0..2d80dc3 100755 --- a/scripts/daily_plot.py +++ b/scripts/daily_plot.py @@ -7,6 +7,8 @@ import matplotlib.pyplot as plt from matplotlib.colors import LogNorm from datetime import datetime import textwrap +import matplotlib.font_manager as font_manager +from matplotlib import rcParams userDir = os.path.expanduser('~') conn = sqlite3.connect(userDir + '/BirdNET-Pi/scripts/birds.db') @@ -29,6 +31,14 @@ df['Hour of Day'] = [r.hour for r in df.Time] #Create separate dataframes for separate locations df_plt=df #Default to use the whole Dbase +# Add every font at the specified location +font_dir = ['$HOME/BirdNET-Pi/homepage/static'] +for font in font_manager.findSystemFonts(font_dir): + font_manager.fontManager.addfont(font) + +# Set font family globally +rcParams['font.family'] = 'Roboto Flex' + #Get todays readings now = datetime.now() df_plt_today = df_plt[df_plt['Date']==now.strftime("%Y-%m-%d")] diff --git a/scripts/history.php b/scripts/history.php index 9a5e1df..88ca352 100644 --- a/scripts/history.php +++ b/scripts/history.php @@ -37,6 +37,7 @@ $totalcount = $result1->fetchArray(SQLITE3_ASSOC); +

"; diff --git a/scripts/install_config.sh b/scripts/install_config.sh index 5d68d64..c526cda 100755 --- a/scripts/install_config.sh +++ b/scripts/install_config.sh @@ -75,6 +75,10 @@ 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 +#---------------------- Flickr Images API Configuration -----------------------# + +FLICKR_API_KEY= + ################################################################################ #-------------------------------- Defaults ----------------------------------# ################################################################################ diff --git a/scripts/overview.php b/scripts/overview.php index cba8a9f..a5a3bdc 100644 --- a/scripts/overview.php +++ b/scripts/overview.php @@ -8,6 +8,12 @@ if($db == False) { header("refresh: 0;"); } +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(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'); @@ -16,6 +22,8 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" && isse header("refresh: 0;"); } $result4 = $statement4->execute(); + $images = []; + $iterations = 0; // 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']); @@ -37,6 +45,22 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" && isse // we've found our valid detection! ignore everything else from the database loop if(strpos($headers[0],'200')) { if($_GET['previous_detection_identifier'] == $filename) { die(); } + + 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($images, 0)); + if($key !== false) { + $image = $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&format=json&media=photos&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($images, array($comname,$imageurl,$flickrjson["title"], $modaltext, $authorlink)); + $image = $images[count($images)-1]; + } + } + ?>
+ +

+

+ +
+
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 +123,6 @@ if(isset($_GET['byfilename'])){ session_unset(); $view = "choose"; } - ?> @@ -90,6 +133,31 @@ if(isset($_GET['byfilename'])){ + + - @@ -121,13 +188,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 +204,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 +223,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 +268,7 @@ if(isset($_GET['species'])){ "; + $iter=0; while($results=$result2->fetchArray(SQLITE3_ASSOC)) { $comname = preg_replace('/ /', '_', $results['Com_Name']); @@ -176,13 +278,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 +334,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/plotly_streamlit.py b/scripts/plotly_streamlit.py index b42906d..da8d771 100755 --- a/scripts/plotly_streamlit.py +++ b/scripts/plotly_streamlit.py @@ -60,7 +60,8 @@ df2=df2.set_index('DateTime') # Date as slider Start_Date = pd.to_datetime(df2.index.min()).date() End_Date = pd.to_datetime(df2.index.max()).date() -Date_Slider = st.slider('Date Range', +cols1,cols2= st.columns((1,1)) +Date_Slider = cols1.slider('Date Range', min_value = Start_Date-timedelta(days=1), max_value = End_Date, value=(Start_Date, @@ -72,14 +73,29 @@ Date_Slider = st.slider('Date Range', filt = (df2.index >= pd.Timestamp(Date_Slider[0])) & (df2.index <= pd.Timestamp(Date_Slider[1]+timedelta(days=1))) df2 = df2[filt] +st.write('', unsafe_allow_html=True) +st.write('', unsafe_allow_html=True) + +resample_sel=cols2.radio("Select Resample Resolution - To downsample and make run faster select longer period, Daily provides a view on detections at 15 min intervals through the day", ('1 minute', '5 minutes', '10 minutes', 'Hourly', 'Daily')) + +resample_times = {'1 minute':'1min', + '5 minutes':'5min', + '10 minutes':'10min', + 'Hourly':'1H', + 'Daily':'1D' + } +resample_time = resample_times[resample_sel] + +df5=df2.resample(resample_time)['Com_Name'].aggregate('unique').explode() + #Create species count for selected date range -Specie_Count=df2['Com_Name'].value_counts() +Specie_Count=df5.value_counts() #Create species treemap # Create Hourly Crosstab -hourly=pd.crosstab(df2['Com_Name'],df2.index.hour, dropna=False) +hourly=pd.crosstab(df5,df5.index.hour, dropna=False) # Filter on species species = list(hourly.index) @@ -91,7 +107,7 @@ top_N = cols1.slider( value=min(10,len(Specie_Count)) ) -top_N_species = (df2['Com_Name'].value_counts()[:top_N]) +top_N_species = (df5.value_counts()[:top_N]) specie = cols2.selectbox('Which bird would you like to explore for the dates '+str(Date_Slider[0])+' to '+str(Date_Slider[1])+'?', species, @@ -104,13 +120,13 @@ font_size=15 #specie filter filt=df2['Com_Name']==specie -df_counts=df2[filt].resample('D').count() +df_counts=sum(df5==specie) fig = make_subplots( rows=3, cols =2, specs= [[{"type":"xy","rowspan":3}, {"type":"polar","rowspan":2}], [{"rowspan":1}, {"rowspan":1} ], [None, {"type":"xy","rowspan":1}]], subplot_titles=('Top '+ str(top_N) + ' Species in Date Range '+str(Date_Slider[0])+' to '+str(Date_Slider[1])+'', - 'Total Detect:'+str('{:,}'.format(sum(df_counts.Time)))+ + 'Total Detect:'+str('{:,}'.format(df_counts))+ ' Confidence Max:'+str('{:.2f}%'.format(max(df2[df2['Com_Name']==specie]['Confidence'])*100))+ ' '+' Median:'+str('{:.2f}%'.format(np.median(df2[df2['Com_Name']==specie]['Confidence'])*100)) ) @@ -123,43 +139,80 @@ fig.add_trace(go.Bar(y=top_N_species.index, x=top_N_species, orientation='h'), r fig.update_layout( margin=dict(l=0, r=0, t=50, b=0), yaxis={'categoryorder':'total ascending'}) + + # Set 360 degrees, 24 hours for polar plot theta = np.linspace(0.0, 360, 24, endpoint=False) +specie_filt= df5==specie +df3=df5[specie_filt] + +detections2= pd.crosstab(df3, df3.index.hour) + + + + d=pd.DataFrame(np.zeros((23,1))).squeeze() detections = hourly.loc[specie] detections=(d+detections).fillna(0) -fig.add_trace(go.Barpolar(r = detections, theta=theta), row=1, col=2) -fig.update_layout( - autosize=False, - width = 1000, - height = 500, - showlegend=False, - polar = dict( - radialaxis = dict( - tickfont_size = font_size, - showticklabels = True, - hoverformat = "#%{theta}:
Popularity: %{percent}
%{r}" + +if resample_time != '1D': + fig.add_trace(go.Barpolar(r = detections, theta=theta), row=1, col=2) + fig.update_layout( + autosize=False, + width = 1000, + height = 500, + showlegend=False, + polar = dict( + radialaxis = dict( + tickfont_size = font_size, + showticklabels = False, + hoverformat = "#%{theta}:
Popularity: %{percent}
%{r}" + ), + angularaxis = dict( + tickfont_size= font_size, + rotation = -90, + direction = 'clockwise', + tickmode='array', + tickvals=[0,15,35,45,60,75,90,105,120,135,150,165,180,195,210,225,240,255,270,285,300,315,330,345], + ticktext=['12am','1am','2am','3am','4am','5am', '6am','7am','8am','9am','10am','11am','12pm','1pm','2pm','3pm','4pm','5pm', '6pm','7pm','8pm','9pm','10pm','11pm'], + hoverformat = "#%{theta}:
Popularity: %{percent}
%{r}" ), - angularaxis = dict( - tickfont_size= font_size, - rotation = -90, - direction = 'clockwise', - tickmode='array', - tickvals=[0,15,35,45,60,75,90,105,120,135,150,165,180,195,210,225,240,255,270,285,300,315,330,345], - ticktext=['12am','1am','2am','3am','4am','5am', '6am','7am','8am','9am','10am','11am','12pm','1pm','2pm','3pm','4pm','5pm', '6pm','7pm','8pm','9pm','10pm','11pm'], - hoverformat = "#%{theta}:
Popularity: %{percent}
%{r}" - ), - ), - ) + ), + ) -daily=pd.crosstab(df2['Com_Name'],df2.index.date, dropna=False) + daily=pd.crosstab(df5,df5.index.date, dropna=False) -fig.add_trace(go.Bar(x=daily.columns, y=daily.loc[specie]), row=3, col=2) + fig.add_trace(go.Bar(x=daily.columns, y=daily.loc[specie]), row=3, col=2) +else: + fig = make_subplots( + rows=1, cols =2, +# specs= [[{"type":"xy","rowspan":1}], +# [{"rowspan":1}], +# ], +# subplot_titles=('Top '+ str(top_N) + ' Species in Date Range '+str(Date_Slider[0])+' to '+str(Date_Slider[1])+'', +# 'Total Detect:'+str('{:,}'.format(df_counts))+ +# ' Confidence Max:'+str('{:.2f}%'.format(max(df2[df2['Com_Name']==specie]['Confidence'])*100))+ +# ' '+' Median:'+str('{:.2f}%'.format(np.median(df2[df2['Com_Name']==specie]['Confidence'])*100)) +# ) + ) + df4=df2['Com_Name'][df2['Com_Name']==specie].resample('15min').count() + df4.index=[df4.index.date, df4.index.time] + day_hour_freq=df4.unstack().fillna(0) + + 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()] + fig_z = day_hour_freq.values.transpose() + fig_heatmap = go.Figure(data=go.Heatmap(x=fig_x,y=fig_y,z=fig_z)) + fig.add_trace(go.Bar(y=top_N_species.index, x=top_N_species, orientation='h'), row=1,col=1) + fig.update_layout( + margin=dict(l=0, r=0, t=50, b=0), + yaxis={'categoryorder':'total ascending'}) + fig.add_trace(go.Heatmap(x=fig_x,y=fig_y,z=fig_z, autocolorscale = False, colorscale = 'blackbody'), row=1, col=2) # container=st.container() # config={'displayModelBar': False} st.plotly_chart(fig, use_container_width=True) #, config=config) diff --git a/scripts/restart_services.sh b/scripts/restart_services.sh index f0cead9..98993af 100755 --- a/scripts/restart_services.sh +++ b/scripts/restart_services.sh @@ -22,6 +22,6 @@ sudo systemctl restart "${i}" done until grep 5050 <(netstat -tulpn 2>&1);do sudo systemctl restart birdnet_server.service -sleep 20 +sleep 30 done sudo systemctl restart birdnet_analysis.service diff --git a/scripts/server.py b/scripts/server.py index 6c96e12..35833e1 100755 --- a/scripts/server.py +++ b/scripts/server.py @@ -261,7 +261,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 +335,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 +463,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/spectrogram.php b/scripts/spectrogram.php index 8140cfc..48eb20f 100644 --- a/scripts/spectrogram.php +++ b/scripts/spectrogram.php @@ -1,16 +1,85 @@ -"; -?> + + + +

Loading...

+ + diff --git a/scripts/stats.php b/scripts/stats.php index 230a54a..fce469b 100644 --- a/scripts/stats.php +++ b/scripts/stats.php @@ -56,7 +56,18 @@ 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")) { + file_put_contents($home."/BirdNET-Pi/scripts/disk_check_exclude.txt", "##start\n##end"); +} +function editfile( $sourcefile, $start='##start', $end='##end', $data=array() ){ + $lines=array_filter( file( $sourcefile , FILE_SKIP_EMPTY_LINES | FILE_IGNORE_NEW_LINES ) ); + $output=array_merge( + array_splice( $lines, 0, array_search( strtolower( $start ), array_map('strtolower', $lines ) ) + 1 ), + $data, + array_splice( $lines, array_search( strtolower( $end ), array_map('strtolower', $lines ) ) ) + ); + file_put_contents( $sourcefile, implode( PHP_EOL, $output ) ); +} ?> @@ -106,17 +117,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,46 +170,47 @@ 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 ""; + $flickrjson = json_decode(file_get_contents("https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key=2158f6c5d66e89778bd0d340953f1bcf&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 = 'http://farm' .$val["farm"]. '.static.flickr.com/' .$val["server"]. '/' .$val["id"]. '_' .$val["secret"]. '.jpg'; + echo "".$iter.""; } -}} +} +} ?>


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:
@@ -183,4 +218,4 @@ fwrite($excludefile, $txt);
- \ No newline at end of file + diff --git a/scripts/todays_detections.php b/scripts/todays_detections.php index 1b4db75..bacd06f 100644 --- a/scripts/todays_detections.php +++ b/scripts/todays_detections.php @@ -67,7 +67,7 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" ) { $statement0 = $db->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 '.$searchquery); } if($statement0 == False){ echo "Database is busy"; @@ -77,7 +77,15 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" ) { ?> fetchArray(SQLITE3_ASSOC)) { $iterations++; @@ -86,13 +94,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($images, 0)); + if($key !== false) { + $image = $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($images, array($comname,$imageurl,$flickrjson["title"], $modaltext, $authorlink)); + $image = $images[count($images)-1]; + } + } ?> - @@ -100,7 +130,7 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" ) { - + @@ -132,6 +162,29 @@ if(isset($_GET['ajax_detections']) && $_GET['ajax_detections'] == "true" ) {
+ +

+

+ +
+

Number of Detections


+
+ +
+ + + + +


- Confidence:
+ Confidence:




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

Today's Detections —

-
+

Loading...

diff --git a/scripts/update_birdnet_snippets.sh b/scripts/update_birdnet_snippets.sh index 038683f..8fb4ec5 100755 --- a/scripts/update_birdnet_snippets.sh +++ b/scripts/update_birdnet_snippets.sh @@ -54,3 +54,7 @@ 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