diff --git a/scripts/species_tools.php b/scripts/species_tools.php index cb483ac..cf1d39b 100644 --- a/scripts/species_tools.php +++ b/scripts/species_tools.php @@ -7,14 +7,44 @@ require_once __DIR__ . '/common.php'; ensure_authenticated(); $home = get_home(); -// Open database read-only for typical operations; enable writes only for deletions + +/* ---------- disk species counts (AJAX endpoint) ---------- */ +if (isset($_GET['diskcounts'])) { + header('Content-Type: application/json'); + $script = __DIR__ . '/disk_species_count.sh'; + $cmd = 'HOME=' . escapeshellarg($home) . ' bash ' . escapeshellarg($script) . ' 2>&1'; + $output = @shell_exec($cmd); + $counts = []; + if ($output !== null) { + foreach (preg_split('/\\r?\\n/', $output) as $line) { + $line = trim($line); + if ($line === '') continue; + if (preg_match('/^([0-9]+(?:\\.[0-9]+)?)(k?)\\s*:\\s*(.+)$/i', $line, $m)) { + $num = (float)$m[1]; + if (strtolower($m[2]) === 'k') $num *= 1000; + $counts[$m[3]] = (int)round($num); + } + } + } + echo json_encode($counts, JSON_UNESCAPED_UNICODE); + exit; +} + +/* ---------- DB open (RO unless deleting) ---------- */ $flags = isset($_GET['delete']) ? SQLITE3_OPEN_READWRITE : SQLITE3_OPEN_READONLY; $db = new SQLite3(__DIR__ . '/birds.db', $flags); $db->busyTimeout(1000); +$db->exec(" + PRAGMA journal_mode=WAL; -- safe read concurrency + PRAGMA synchronous=NORMAL; -- cheaper fsyncs (read-mostly) + PRAGMA temp_store=MEMORY; -- temp data stays in RAM + PRAGMA cache_size=-80000; -- ~80MB page cache (tune to your RAM) + PRAGMA mmap_size=268435456; -- 256MB mmap (set 0 if kernel disallows) +"); /* Paths / lists */ $base_symlink = $home . '/BirdSongs/Extracted/By_Date'; -$base = realpath($base_symlink); // used only for safety checks +$base = realpath($base_symlink); // safety checks $confirm_file = __DIR__ . '/confirmed_species_list.txt'; $exclude_file = __DIR__ . '/exclude_species_list.txt'; @@ -32,16 +62,11 @@ $config = get_config(); $sf_thresh = isset($config['SF_THRESH']) ? (float)$config['SF_THRESH'] : 0.0; /* ---------- helpers ---------- */ -function join_path(...$parts): string { - return preg_replace('#/+#', '/', implode('/', $parts)); -} -function can_unlink(string $p): bool { - return is_link($p) || is_file($p); -} +function join_path(...$parts): string { return preg_replace('#/+#', '/', implode('/', $parts)); } +function can_unlink(string $p): bool { return is_link($p) || is_file($p); } function under_base(string $path, string $base): bool { if ($base === false) return false; $baseReal = rtrim(realpath($base) ?: $base, DIRECTORY_SEPARATOR); - $resolved = realpath($path); if ($resolved === false) { $parent = realpath(dirname($path)); @@ -51,50 +76,31 @@ function under_base(string $path, string $base): bool { return $resolved === $baseReal || strpos($resolved, $baseReal . DIRECTORY_SEPARATOR) === 0; } -/** - * Collect detection count, files to delete (unique), first scientific name, - * and dirs to try rmdir later — for a given species. - * NOTE: Row-wise enumeration (no aggregates) so we gather every file. - */ +/* Collect files/dirs for a species */ function collect_species_targets(SQLite3 $db, string $species, string $home, $base): array { - $stmt = $db->prepare('SELECT Date, Com_Name, Sci_Name, File_Name - FROM detections - WHERE Com_Name = :name'); + $stmt = $db->prepare('SELECT Date, Com_Name, Sci_Name, File_Name FROM detections WHERE Com_Name = :name'); ensure_db_ok($stmt); $stmt->bindValue(':name', $species, SQLITE3_TEXT); $res = $stmt->execute(); $count = 0; $files = []; $dirs = []; $sci = null; - while ($row = $res->fetchArray(SQLITE3_ASSOC)) { - $count++; - if ($sci === null) $sci = $row['Sci_Name']; + $count++; if ($sci === null) $sci = $row['Sci_Name']; $dir = str_replace([' ', "'"], ['_', ''], $row['Com_Name']); - $candidates = [ join_path($home, 'BirdSongs/Extracted/By_Date', $row['Date'], $dir, $row['File_Name']), join_path($home, 'BirdSongs/Extracted/By_Date/shifted', $row['Date'], $dir, $row['File_Name']), ]; - foreach ($candidates as $c) { - if (can_unlink($c) && under_base($c, $base)) { - $files[$c] = true; $dirs[] = dirname($c); continue; - } + if (can_unlink($c) && under_base($c, $base)) { $files[$c] = true; $dirs[] = dirname($c); continue; } $d = realpath(dirname($c)); if ($d !== false) { $alt = $d . DIRECTORY_SEPARATOR . basename($c); - if (can_unlink($alt) && under_base($alt, $base)) { - $files[$alt] = true; $dirs[] = dirname($alt); - } + if (can_unlink($alt) && under_base($alt, $base)) { $files[$alt] = true; $dirs[] = dirname($alt); } } } } - return [ - 'count' => $count, - 'files' => array_keys($files), - 'dirs' => array_values(array_unique($dirs)), - 'sci' => $sci, - ]; + return ['count'=>$count, 'files'=>array_keys($files), 'dirs'=>array_values(array_unique($dirs)), 'sci'=>$sci]; } /* ---------- toggle exclude/whitelist/confirmed ---------- */ @@ -117,7 +123,7 @@ if (isset($_GET['toggle'], $_GET['species'], $_GET['action'])) { header('Content-Type: text/plain'); echo 'OK'; exit; } -/* ---------- count (keeps your old "getcounts=" API) ---------- */ +/* ---------- count ---------- */ if (isset($_GET['getcounts'])) { header('Content-Type: application/json'); if ($base === false) { http_response_code(500); exit(json_encode(['error' => 'Base directory not found'])); } @@ -126,7 +132,7 @@ if (isset($_GET['getcounts'])) { echo json_encode(['count' => $info['count'], 'files' => count($info['files'])]); exit; } -/* ---------- delete (keeps your old "delete=" API) ---------- */ +/* ---------- delete ---------- */ if (isset($_GET['delete'])) { header('Content-Type: application/json'); if ($base === false) { http_response_code(500); exit(json_encode(['error' => 'Base directory not found'])); } @@ -138,24 +144,19 @@ if (isset($_GET['delete'])) { if (!under_base($fp, $base)) continue; if (can_unlink($fp) && @unlink($fp)) { $deleted++; - // thumbnails: "file.wav.png" and "file.png" foreach ([$fp . '.png', preg_replace('/\.[^.]+$/', '.png', $fp)] as $png) { if (can_unlink($png)) @unlink($png); } } } - foreach ($info['dirs'] as $dir) { - if (under_base($dir, $base)) @rmdir($dir); // best effort - } + foreach ($info['dirs'] as $dir) { if (under_base($dir, $base)) @rmdir($dir); } - // DB rows $del = $db->prepare('DELETE FROM detections WHERE Com_Name = :name'); ensure_db_ok($del); $del->bindValue(':name', $species, SQLITE3_TEXT); $del->execute(); $lines_deleted = $db->changes(); - // Remove from confirmed list if ($info['sci'] !== null && file_exists($confirm_file)) { $identifier = str_replace("'", '', $info['sci']); $lines = array_values(array_filter($confirmed_species, fn($l) => $l !== $identifier)); @@ -167,15 +168,9 @@ if (isset($_GET['delete'])) { /* ---------- query species aggregates ---------- */ $sql = <<query($sql); ?> @@ -190,27 +185,26 @@ $result = $db->query($sql);
- +
- - - - - - - - - - - - - - - - +
Common NameScientific NameIdentificationsMax ConfidenceLast SeenProbabilityConfirmedExcludedWhitelistedDelete
+ + + + + + + + + + + + + + + fetchArray(SQLITE3_ASSOC)) { $common = htmlspecialchars($row['Com_Name'], ENT_QUOTES); $scient = htmlspecialchars($row['Sci_Name'], ENT_QUOTES); @@ -223,8 +217,7 @@ $result = $db->query($sql); $lastSeenSort = $lastSeen ? (strtotime($lastSeen) ?: 0) : 0; $lastSeenDisplay = htmlspecialchars($lastSeen, ENT_QUOTES); - $common_link = "{$common}"; + $common_link = "{$common}"; $is_confirmed = in_array($identifier_sci, $confirmed_species, true); $is_excluded = in_array($identifier, $excluded_species, true); @@ -246,8 +239,8 @@ $result = $db->query($sql); . "" . "" . "" - . "" /* Max Confidence */ - . "" /* Last Seen */ + . "" + . "" . "" . "" . "" @@ -255,20 +248,18 @@ $result = $db->query($sql); . "" . ""; } ?> - -
Common NameScientific NameIdentificationsMax ConfidenceLast SeenProbabilityConfirmedExcludedWhitelistedDelete
{$common_link}{$scient}{$count}{$max_confidence}%{$lastSeenDisplay}{$max_confidence}%{$lastSeenDisplay}0.0000".$confirm_cell."".$excl_cell."
+ +