Compare commits

...

8 Commits

9 changed files with 672 additions and 18 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ src/img/**
src/pages/infophp.php src/pages/infophp.php
src/pages/gethash.php src/pages/gethash.php
vars.php vars.php
**.github

221
install Normal file
View File

@@ -0,0 +1,221 @@
#!/bin/bash
set -e
# --- Setup ---
BASEDIR="$(cd "$(dirname "$0")" && pwd)"
SRCDIR="$BASEDIR/.src"
SCRIPTDIR="$SRCDIR/scripts"
BACKUPDIR="/tmp/shutterlinksort-backup"
# --- Functions ---
usage() {
cat <<EOF
Usage: sudo $0 [OPTIONS] [WEBROOT]
Options:
--help Show this help message
--dry-run Preview actions without changing any files
--rollback Restore the last backup
--uninstall Remove installed files and web content
Arguments:
WEBROOT Required: destination for web content
Examples:
Install normally (default web root):
sudo $0
Install to custom web root:
sudo $0 /srv/http/shutterlinksort
Dry-run install (preview actions):
./install.sh --dry-run /srv/http/shutterlinksort
Rollback from last backup:
sudo $0 --rollback
Uninstall installed files:
sudo $0 --uninstall
EOF
exit 0
}
backup_if_exists() {
local target="$1"
if [ -e "$target" ]; then
local target_dir
target_dir="$(dirname "$target")"
if [ "$DRYRUN" = true ]; then
echo " [DRY-RUN] Would back up existing $(basename "$target") to $BACKUPDIR/$target_dir"
return
fi
mkdir -p "$BACKUPDIR/$target_dir"
cp -a "$target" "$BACKUPDIR/$target_dir/"
echo " • Backed up existing $(basename "$target")"
fi
}
rollback() {
if [ ! -d "$BACKUPDIR" ]; then
echo "No backup found at $BACKUPDIR. Nothing to restore."
exit 1
fi
read -pr "Are you sure you want to rollback all files from backup? [y/N]: " confirm
if [[ "$confirm" =~ ^[Yy]$ ]]; then
echo "=== Rolling back from backup ==="
if [ "$DRYRUN" = true ]; then
echo " [DRY-RUN] Would restore backup from $BACKUPDIR to /"
else
cp -a "$BACKUPDIR/"* /
echo "Rollback complete ✅"
fi
else
echo "Rollback cancelled."
fi
exit 0
}
uninstall() {
read -pr "Are you sure you want to uninstall all shutterlinksort files and web content? [y/N]: " confirm
if [[ "$confirm" =~ ^[Yy]$ ]]; then
echo "=== Uninstalling shutterlinksort ==="
for srcfile in "$SCRIPTDIR"/shutterlinksort-*; do
filename="$(basename "$srcfile")"
progname="${filename%%-*}"
pathpart="${filename#*-}"
targetpath="${pathpart//-//}"
target="/$targetpath/$progname"
if [ "$DRYRUN" = true ] && [ -e "$target" ]; then
echo " [DRY-RUN] Would remove $target"
continue
fi
if [ -e "$target" ]; then
rm -f "$target"
echo " • Removed $target"
fi
done
if [ -d "$WEBROOT" ]; then
if [ "$DRYRUN" = true ]; then
echo " [DRY-RUN] Would remove web content at $WEBROOT"
else
rm -rf "$WEBROOT"
echo " • Removed web content at $WEBROOT"
fi
fi
echo "Uninstall complete ✅"
else
echo "Uninstall cancelled."
fi
exit 0
}
# --- Parse arguments ---
DRYRUN=false
ACTION="install"
# Show usage if no arguments were provided
if [ $# -eq 0 ]; then
usage
fi
while [ $# -gt 0 ]; do
case "$1" in
--help)
usage
;;
--dry-run)
DRYRUN=true
shift
;;
--rollback|--uninstall)
ACTION="$1"
shift
;;
*)
WEBROOT="$1"
shift
;;
esac
done
# --- Main ---
case "$ACTION" in
--rollback)
rollback
;;
--uninstall)
uninstall
;;
install)
echo "=== Installing shutterlinksort components ==="
echo "Using web root: $WEBROOT"
echo "Backup directory: $BACKUPDIR"
if [ "$DRYRUN" = true ]; then
echo "[DRY-RUN mode: no files will be changed]"
fi
echo
# Clear previous backup
if [ "$DRYRUN" = true ]; then
echo "[DRY-RUN] Would remove existing $BACKUPDIR"
else
rm -rf "$BACKUPDIR"
fi
if [ "$DRYRUN" = true ]; then
echo "[DRY-RUN] Would create $BACKUPDIR"
else
mkdir -p "$BACKUPDIR"
fi
# Install scripts
for srcfile in "$SCRIPTDIR"/shutterlinksort-*; do
[ -e "$srcfile" ] || continue
filename="$(basename "$srcfile")"
progname="${filename%%-*}"
pathpart="${filename#*-}"
targetpath="${pathpart//-//}"
target="/$targetpath/$progname"
echo "→ Installing $progname → $target"
mkdir -p "/$targetpath" 2>/dev/null || true
backup_if_exists "$target"
if [ "$DRYRUN" = true ]; then
echo " [DRY-RUN] Would install $srcfile → $target"
else
install -m 755 "$srcfile" "$target"
fi
done
# Install web content
echo
echo "=== Installing web content ==="
if [ "$DRYRUN" = true ]; then
echo " [DRY-RUN] Would create $WEBROOT"
else
mkdir -p "$WEBROOT"
fi
if [ -d "$WEBROOT" ]; then
if [ "$DRYRUN" = true ]; then
echo " [DRY-RUN] Would back up existing web content to $BACKUPDIR/$(basename "$WEBROOT")"
else
cp -a "$WEBROOT" "$BACKUPDIR/"
fi
fi
if [ "$DRYRUN" = true ]; then
echo " [DRY-RUN] Would copy $SRCDIR/web/ → $WEBROOT"
else
cp -r "$SRCDIR/web/"* "$WEBROOT/"
fi
echo
echo "Web content installed to: $WEBROOT"
echo "All backups stored in: $BACKUPDIR"
echo "Installation complete ✅"
echo "To rollback, run: sudo $0 --rollback"
echo "To uninstall, run: sudo $0 --uninstall"
;;
esac

View File

@@ -0,0 +1,67 @@
#!/bin/bash /etc/rc.common
# shutterlinksort.init
# Copyright (C) 2025 reclusejay (James Blackmore)
# OpenWrt init script for shutterlinksort
# shellcheck disable=SC2034 # START/STOP used in rc.common
# START/STOP handled by /etc/rc.common
START=96
STOP=10
SCRIPT_NAME="shutterlinksort"
LOCK_FILE="/var/lock/$SCRIPT_NAME"
start() {
if [ -f "$LOCK_FILE" ]; then
echo "$SCRIPT_NAME is already running (pid=$(cat "$LOCK_FILE"))"
return 0
fi
echo "Starting $SCRIPT_NAME..."
"$SCRIPT_NAME" >/dev/null 2>&1 &
pid=$!
# Verify the process started successfully
if kill -0 "$pid" 2>/dev/null; then
echo "$pid" > "$LOCK_FILE"
echo "$SCRIPT_NAME started with pid=$pid"
else
echo "Failed to start $SCRIPT_NAME"
return 1
fi
}
stop() {
if [ ! -f "$LOCK_FILE" ]; then
echo "$SCRIPT_NAME not running"
return 0
fi
pid=$(cat "$LOCK_FILE")
echo "Stopping $SCRIPT_NAME (pid=$pid)..."
if kill "$pid" 2>/dev/null; then
sleep 1
rm -f "$LOCK_FILE"
echo "$SCRIPT_NAME stopped"
else
echo "Failed to stop $SCRIPT_NAME (process may not exist)"
rm -f "$LOCK_FILE"
return 1
fi
}
restart() {
echo "Restarting $SCRIPT_NAME..."
stop
sleep 2
start
}
boot() {
start
}
shutdown() {
stop
}

View File

@@ -0,0 +1,296 @@
#!/bin/bash
# ======================================================================
# shutterlinksort
# ----------------
# Camera image downloader and organizer for PTP/IP-connected devices.
#
# Connects to a camera over network (using gphoto2), downloads new
# photos, extracts EXIF metadata (date, shutter count), and sorts
# images into folders by date.
#
# Usage:
# shutterlinksort
# Dependencies:
# gphoto2, exiftool, bash
#
# Author: James Blackmore (reclusejay)
# Version: 1.0
# ======================================================================
# --- Safety options ---
# -e : Exit immediately if a command exits with non-zero status.
# -u : Treat unset variables as an error and exit.
# -o pipefail : Fail a pipeline if *any* command in it fails.
set -euo pipefail
# ============================================================
# Cleanup handler — runs on interrupt/termination
# ============================================================
cleanup() {
# Disable any special globbing options we enabled
shopt -u nullglob nocaseglob || true
# Delete any 0-byte (incomplete) downloads if present
find "$cam_dl_dir" -type f -size 0 -delete 2>/dev/null || true
log "Caught interrupt, exiting"
exit 0
}
# Trap cleanup on Ctrl+C, kill, or quit signals
trap cleanup SIGINT SIGTERM SIGQUIT
# ============================================================
# Function: php_get
# Pulls a config value from the PHP $CONFIG array
# ============================================================
php_get() {
if ! command -v php >/dev/null 2>&1; then
# fallback to php-cli
if command -v php-cli >/dev/null 2>&1; then
php-cli -r "include('../src/includes/config.php'); print \$CONFIG['$1'];"
else
echo "Error: Neither 'php' nor 'php-cli' is available."
exit 1
fi
else
php -r "include('../src/includes/config.php'); print \$CONFIG['$1'];"
fi
return 0
}
# ============================================================
# Load all settings from PHP config
# ============================================================
cam_uuid=$(php_get "camera_uuid")
cam_dl_dir=$(php_get "upload_dir")
cam_sort_dir=$(php_get "sorted_img_dir")
cam_index_file=$(php_get "index_file")
cam_xml_url=$(php_get "camera_xml_url")
cam_ext_format=$(php_get "camera_ext_formats")
cam_mac=$(php_get "camera_mac")
g2_port=$(php_get "gphoto2_config_port")
g2_model=$(php_get "gphoto2_config_model")
g2_guid=$(php_get "gphoto2_config_guid")
g2_conf=$(php_get "gphoto2_config_file")
# Create the index file if missing
[ -f "$cam_index_file" ] || echo 0 > "$cam_index_file"
# ============================================================
# Global variables
# ============================================================
debug=true
ver="v.25.10.1"
STATUS="INITIALIZING"
CAM_IP=""
# ============================================================
# Function: log
# Provides timestamped console and syslog logging
# ============================================================
log() {
local msg lev full_msg
if $debug; then
msg="${1:-NO MESSAGE!}"
lev="${2:-info}"
full_msg="[debug $lev] Version: $ver - $(date +[%Y/%m/%d-%H:%M:%S]) - $msg"
# Always print to stdout for debugging
echo "$full_msg"
# Try syslog first; fallback to a file log if logger fails
logger -t camera-download-sort "$full_msg" 2>/dev/null || \
echo "$full_msg" >> /var/log/camera-download-sort.log
fi
}
# ============================================================
# Function: find_ip_by_mac
# Locates the IP address of a device by its MAC address
# ============================================================
find_ip_by_mac() {
local mac ip file
# Normalize MAC to lowercase for comparison
mac=$(echo "$1" | tr '[:upper:]' '[:lower:]')
[ -z "$mac" ] && log "No MAC provided" && return 1
# 1⃣ Primary source: /proc/net/arp (most reliable)
ip=$(awk -v mac="$mac" 'NR>1 && tolower($4)==mac {print $1; exit}' /proc/net/arp)
# 2⃣ Fallback: BusyBox arp command output
if [ -z "$ip" ] && command -v arp >/dev/null 2>&1; then
ip=$(arp | awk -v mac="$mac" 'NR>1 && tolower($4)==mac {print $1; exit}')
fi
# 3⃣ Fallback: dnsmasq lease files
if [ -z "$ip" ]; then
for file in /tmp/dhcp.leases /var/lib/misc/dnsmasq.leases; do
[ -f "$file" ] || continue
ip=$(awk -v mac="$mac" 'tolower($2)==mac {print $3; exit}' "$file")
[ -n "$ip" ] && break
done
fi
# If found, set global CAM_IP variable
if [ -n "$ip" ]; then
CAM_IP="$ip"
log "Camera found at $CAM_IP"
return 0
fi
# Not found
return 1
}
# ============================================================
# Function: is_camera_connected
# Checks whether the cameras MAC address is visible
# ============================================================
is_camera_connected() {
if find_ip_by_mac "$cam_mac"; then
if [ "$STATUS" = "WAITING" ]; then
# Already in waiting state — skip
return 1
fi
STATUS="CONNECTED"
return 0
fi
STATUS="DISCONNECTED"
return 1
}
# ============================================================
# Function: download_images
# Uses gphoto2 (PTP/IP) to pull new images from camera
# ============================================================
download_images() {
local wd="$PWD"
STATUS="WAITING"
cd "$cam_dl_dir" || return 1
# Create gphoto2 config file if it doesnt exist
if [ ! -f "$g2_conf" ]; then
{
echo "$g2_port$CAM_IP"
echo "$g2_model"
echo "$g2_guid$cam_uuid"
} > "$g2_conf"
fi
# Check for gphoto2 availability
if command -v gphoto2 >/dev/null 2>&1; then
# Download all new files, skipping ones that already exist
if gphoto2 --port "ptpip:$CAM_IP" -P --skip-existing; then
log "Downloaded new images from $CAM_IP"
cd "$wd" || true
return 0
fi
log "gphoto2 download failed" error
else
log "gphoto2 not found — skipping download" warn
fi
cd "$wd" || true
return 1
}
# ============================================================
# Function: sort_images
# Sorts downloaded images into folders by EXIF date
# ============================================================
sort_images() {
local ext img datetime shuttercount yyyy mm dd timepart hhmmss ext_lower \
output_dir new_filename new_file uid index
STATUS="SORTING"
log "$STATUS"
# Ensure exiftool exists
command -v exiftool >/dev/null 2>&1 || {
log "exiftool missing — cannot sort" error
return 1
}
# Get current image index number
index=$(cat "$cam_index_file")
# Loop over each configured image extension (e.g. JPG ARW)
for ext in $cam_ext_format; do
# Enable flexible globbing (case-insensitive, no literal fail)
shopt -s nullglob nocaseglob
# Iterate through each matching file
for img in "$cam_dl_dir"/*."$ext"; do
# Pull EXIF date and shutter count
readarray -t exif_data < <(exiftool -s -s -s -DateTimeOriginal -ShutterCount "$img")
datetime="${exif_data[0]:-0000:00:00 00:00:00}"
shuttercount="${exif_data[1]:-}"
log "Processing $img (index $index)"
# Generate a unique ID if no shuttercount found
if [ -z "$shuttercount" ]; then
uid="rn${RANDOM}idx${index}"
log "No shuttercount in $img, uid=$uid"
else
uid="sc${shuttercount}idx${index}"
log "Shuttercount=$shuttercount uid=$uid"
fi
# Parse EXIF date into components
yyyy=$(echo "$datetime" | cut -d':' -f1)
mm=$(echo "$datetime" | cut -d':' -f2)
dd=$(echo "$datetime" | cut -d':' -f3 | cut -d' ' -f1)
timepart=$(echo "$datetime" | cut -d' ' -f2)
hhmmss=$(echo "$timepart" | tr -d ':')
# Normalize extension to lowercase
ext_lower=${ext,,}
# Build destination folder structure
output_dir="$cam_sort_dir/$ext_lower/$yyyy/$mm/$dd"
mkdir -p "$output_dir" || { log "mkdir failed: $output_dir" error; return 1; }
# Construct new filename
new_filename="${hhmmss}_${uid}.${ext_lower}"
new_file="$output_dir/$new_filename"
log "Moving $img -> $new_file"
# Move and increment index
if mv "$img" "$new_file"; then
index=$((index + 1))
else
log "Failed to move $img" error
fi
done
# Update index file after processing each extension type
printf '%s\n' "$index" > "$cam_index_file"
done
STATUS="DONE"
log "$STATUS"
return 0
}
# ============================================================
# Main loop — runs forever
# ============================================================
log "Camera script started"
while :; do
# If camera detected and not already downloading/sorting
if is_camera_connected && [ "$STATUS" != "WAITING" ] && [ "$STATUS" != "SORTING" ]; then
download_images || log "Download step failed" error
# If camera disconnected, begin sorting
elif [ "$STATUS" = "DISCONNECTED" ]; then
sort_images || log "Sort step failed" error
fi
# Avoid hammering system; check every 2 seconds
sleep 2
done

View File

@@ -16,6 +16,13 @@ $cache_file = $CONFIG['cache_file'];
// Serve cached gallery JSON if fresh // Serve cached gallery JSON if fresh
if (file_exists($cache_file)) { if (file_exists($cache_file)) {
$data = json_decode(file_get_contents($cache_file), true); $data = json_decode(file_get_contents($cache_file), true);
$count = count_all_images($CONFIG);
if ($count !== $data['countall']) {
unlink($cache_file);
$data = buildGalleryCache($CONFIG);
if (!is_dir(dirname($cache_file))) @mkdir(dirname($cache_file), 0775, true);
file_put_contents($cache_file, json_encode($data, JSON_PRETTY_PRINT));
}
} else { } else {
$data = buildGalleryCache($CONFIG); $data = buildGalleryCache($CONFIG);
if (!is_dir(dirname($cache_file))) @mkdir(dirname($cache_file), 0775, true); if (!is_dir(dirname($cache_file))) @mkdir(dirname($cache_file), 0775, true);
@@ -37,6 +44,53 @@ echo json_encode([
// --- FUNCTIONS --- // --- FUNCTIONS ---
function count_all_images($CONFIG) {
$dir = $CONFIG['sorted_img_dir'];
if (!is_dir($dir)) {
return 0;
}
$count = 0;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->isFile()) {
$ext = strtolower($file->getExtension());
if ($ext === 'arw' || $ext === 'raw' ||
$ext === 'jpg' || $ext === 'png' ) {
$count++;
}
}
}
return $count;
}
function count_raw_files($CONFIG) {
$dir = $CONFIG['sorted_img_dir'].'/arw';
if (!is_dir($dir)) {
return 0;
}
$count = 0;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->isFile()) {
$ext = strtolower($file->getExtension());
if ($ext === 'arw' || $ext === 'raw') {
$count++;
}
}
}
return $count;
}
function buildGalleryCache($CONFIG) { function buildGalleryCache($CONFIG) {
$dir = $CONFIG['gallery_dir']; $dir = $CONFIG['gallery_dir'];
$rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)); $rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
@@ -74,11 +128,13 @@ function buildGalleryCache($CONFIG) {
} }
} }
return [ return [
'generated' => date('c'), 'generated' => date('c'),
'count' => count($images), 'countjpg' => count($images),
'countraw' => count_raw_files($CONFIG),
'countall' => count_all_images($CONFIG),
'images' => $images 'images' => $images
]; ];
} }

View File

@@ -25,7 +25,15 @@ $CONFIG = [
'blacklist_commands' => ['rm', 'shutdown', 'reboot', 'passwd', 'dd', ':(){'], 'blacklist_commands' => ['rm', 'shutdown', 'reboot', 'passwd', 'dd', ':(){'],
'default_dir' => '/home', 'default_dir' => '/home',
// === ShutterLinkSort bash file config ===
'camera_mac' => '00:00:00:00:00:00',
'camera_uuid' => 'e4:ec:89:a3:0c:b5:4d:1d:cb:e3:a8:e1:ff:68:48:ed',
'camera_xml_url' => 'http://ip:1900/DeviceDescription.xml',
'camera_ext_formats' => 'ARW JPG',
'gphoto2_config_file' => '/root/.gphoto/settings',
'gphoto2_config_port' => 'gphoto2=port=ptpip:',
'gphoto2_config_model' => 'gphoto2=model=PTP/IP Camera',
'gphoto2_config_guid' => 'ptp2_ip=guid=',
]; ];
@@ -52,6 +60,7 @@ $CONFIG_JS = [
$CONFIG += [ $CONFIG += [
// === Paths === // === Paths ===
'base_dir' => $ROOT_DIR, // Location of root directory for the web server usually (Define at the top $ROOT_DIR) 'base_dir' => $ROOT_DIR, // Location of root directory for the web server usually (Define at the top $ROOT_DIR)
'sorted_img_dir' => $ROOT_DIR.'/img/sorted',
'gallery_dir' => $ROOT_DIR.'/img/sorted/jpg', // Location of the images 'gallery_dir' => $ROOT_DIR.'/img/sorted/jpg', // Location of the images
'upload_dir' => $ROOT_DIR.'/img/uploads', // Location for uploaded images 'upload_dir' => $ROOT_DIR.'/img/uploads', // Location for uploaded images
'index_file' => $ROOT_DIR.'/img/.index', // Location of the index file for image naming 'index_file' => $ROOT_DIR.'/img/.index', // Location of the index file for image naming

View File

@@ -45,14 +45,17 @@ $disk0_free = disk_free_space('/home');
$disk0_used = $disk0_total - $disk0_free; $disk0_used = $disk0_total - $disk0_free;
$disk0_usage = round(($disk0_used / $disk0_total) * 100, 1); $disk0_usage = round(($disk0_used / $disk0_total) * 100, 1);
// --- Gallery image count --- // --- Gallery image countjpg ---
// --- Image count from cache/gallery.json --- // --- Image countjpg from cache/gallery.json ---
$jsonFile = __DIR__ . '/../cache/gallery.json'; $jsonFile = __DIR__ . '/../cache/gallery.json';
if (file_exists($jsonFile)) { if (file_exists($jsonFile)) {
$gallery = json_decode(file_get_contents($jsonFile), true); $gallery = json_decode(file_get_contents($jsonFile), true);
$count = $gallery['count'] ?? 0; $countjpg = $gallery['countjpg'] ?? 0;
$countraw = $gallery['countraw'] ?? 0;
} else { } else {
$count = 0; // generate the file
$countjpg = 0;
$countraw = 0;
} }
@@ -70,5 +73,6 @@ echo json_encode([
'disk0' => $disk0_usage, 'disk0' => $disk0_usage,
'disk0_total' => $disk0_total, 'disk0_total' => $disk0_total,
'disk0_used' => $disk0_used, 'disk0_used' => $disk0_used,
'images' => $count, 'imagesjpg' => $countjpg,
'imagesraw' => $countraw,
]); ]);

View File

@@ -1,7 +1,7 @@
document.addEventListener('page-loaded', e => { document.addEventListener('page-loaded', e => {
if (e.detail.page !== "home") return; if (e.detail.page !== "home") return;
const stats = ['Cpu','Ram','Zram','Swap','Disk','Disk0','Images']; const stats = ['Cpu','Ram','Zram','Swap','Disk','Disk0','Imagesjpg','Imagesraw'];
const elements = {}; const elements = {};
const bars = {}; const bars = {};
const prev = {}; const prev = {};
@@ -32,7 +32,7 @@ document.addEventListener('page-loaded', e => {
const numVal = Number(val) || 0; const numVal = Number(val) || 0;
// Animate number // Animate number
if (key === 'Images') { if (key === 'Imagesjpg' || key === 'Imagesraw') {
// Raw number // Raw number
animateValue(elements[key], prev[key], numVal, 400, false); animateValue(elements[key], prev[key], numVal, 400, false);
} else { } else {
@@ -55,7 +55,7 @@ document.addEventListener('page-loaded', e => {
// Progress bar // Progress bar
if (bars[key]) { if (bars[key]) {
let pct = key === 'Images' ? Math.min(numVal / 100 * 100, 100) : numVal; let pct = key === 'Imagesjpg' || key === 'Imagesraw' ? Math.min(numVal / 100 * 100, 100) : numVal;
bars[key].style.width = pct + '%'; bars[key].style.width = pct + '%';
} }

View File

@@ -38,14 +38,14 @@
<div class="progress-bar"><div id="barDisk0"></div></div> <div class="progress-bar"><div id="barDisk0"></div></div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<h3>Images 🖼️</h3> <h3>Images JPG 🖼️</h3>
<p id="statImages"></p> <p id="statImagesjpg"></p>
<div class="progress-bar"><div id="barImages"></div></div> <div class="progress-bar"><div id="barImagesjpg"></div></div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<h3>Blank Card ✏</h3> <h3>Images RAW 🖼</h3>
<p id="statImages"></p> <p id="statImagesraw"></p>
<div class="progress-bar"><div id="barnone"></div></div> <div class="progress-bar"><div id="barImagesraw"></div></div>
</div> </div>
</div> </div>
</section> </section>