owncloud: document and fix organize_photos.sh folder structure
- Add full header doc block explaining purpose, patterns, and scheduling - Inline comments on all functions and both processing passes - Fix folder format: YYYY/YYYY-MM/ → YYYY/MM/ (year was redundant in subfolder) - Add pass 2: migrate existing YYYY/YYYY-MM/ folders to YYYY/MM/ - Pass 1 now also handles Android-app pre-sorted YYYY/MM/ videos correctly Also commit pending doc updates: - README.md: remove Kopia stack, add ntfy entry, fix infrastructure.caddy note - authentik/SETUP-GUIDE.md: add Mealie OIDC setup notes - homelab-overview.md: remove Kopia refs, add Vaultwarden, fix TrueNAS/PBS wording
This commit is contained in:
@@ -1,4 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# organize_photos.sh
|
||||
# =============================================================================
|
||||
# Organizes files in the ownCloud Instant Upload folders into a consistent
|
||||
# date-based folder structure: SUBDIR/YYYY/MM/filename
|
||||
#
|
||||
# The ownCloud Android app pre-sorts videos into YYYY/MM/ subfolders
|
||||
# automatically, but uses YYYY/YYYY-MM/ for images. This script:
|
||||
# 1. Moves any new files dropped in the SUBDIR root into YYYY/MM/
|
||||
# 2. Migrates old YYYY/YYYY-MM/ folders → YYYY/MM/ (removes redundant year)
|
||||
#
|
||||
# Processed subdirectories (under BASE/):
|
||||
# Camera – phone photos and videos (instant upload)
|
||||
# BlueIris – BlueIris NVR clip exports
|
||||
# Facebook – Facebook photo downloads
|
||||
# scans – scanned documents
|
||||
# EufyVideoDir – Eufy security camera recordings
|
||||
#
|
||||
# Files with no recognisable date go to BASE/_unsorted/ unchanged.
|
||||
#
|
||||
# Date extraction tries these patterns in order (first match wins):
|
||||
# 1. YYYYMMDD embedded anywhere in the filename (e.g. PXL_20260802_...)
|
||||
# 2. YYYY-MM-DD or YYYY_MM_DD separator format (e.g. 2026-08-02)
|
||||
# 3. 13-digit Unix millisecond timestamp (e.g. 1722614400000)
|
||||
# 4. MM_DD_YYYY_ prefix (e.g. 08_02_2026_clip)
|
||||
#
|
||||
# Scheduling: run via cron or systemd timer. Example cron (every 15 min):
|
||||
# */15 * * * * /path/to/organize_photos.sh
|
||||
#
|
||||
# Log file: /var/log/organize_photos.log
|
||||
# =============================================================================
|
||||
|
||||
BASE="/mnt/INTEL-SSD/ownCloud - Joe Gitta@cloud.jgitta.com/Personal/InstantUpload"
|
||||
UNSORTED="$BASE/_unsorted"
|
||||
@@ -7,31 +38,54 @@ moved=0; skipped=0; unsorted=0; errors=0
|
||||
|
||||
echo "=== $(date +%Y-%m-%d\ %H:%M:%S) Starting ===" >> "$LOG"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ts_to_ym TIMESTAMP_MS
|
||||
# Converts a 13-digit Unix millisecond timestamp to "YYYY MM".
|
||||
# -----------------------------------------------------------------------------
|
||||
ts_to_ym() {
|
||||
local ts_ms="$1"
|
||||
local ts_s=$(( ts_ms / 1000 ))
|
||||
date -d "@$ts_s" "+%Y %m" 2>/dev/null
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# get_ym FILEPATH
|
||||
# Extracts "YYYY MM" from a filename using the patterns described in the header.
|
||||
# Prints "YYYY MM" on success, empty string if no date found.
|
||||
# -----------------------------------------------------------------------------
|
||||
get_ym() {
|
||||
local f="$1"
|
||||
local b
|
||||
b=$(basename "$f")
|
||||
|
||||
# Pattern 1: YYYYMMDD anywhere in filename (e.g. PXL_20260802_171610514.mp4)
|
||||
if [[ "$b" =~ (^|[^0-9])([0-9]{4})(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|3[01]) ]]; then
|
||||
echo "${BASH_REMATCH[2]} ${BASH_REMATCH[3]}"; return
|
||||
fi
|
||||
|
||||
# Pattern 2: YYYY-MM-DD or YYYY_MM_DD (e.g. 2026-08-02_clip.mp4)
|
||||
if [[ "$b" =~ ([0-9]{4})[-_](0[1-9]|1[0-2])[-_](0[1-9]|[12][0-9]|3[01]) ]]; then
|
||||
echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]}"; return
|
||||
fi
|
||||
|
||||
# Pattern 3: 13-digit Unix ms timestamp (e.g. 1722614400000_eufy.mp4)
|
||||
if [[ "$b" =~ (^|[^0-9])([0-9]{13})([^0-9]|$) ]]; then
|
||||
ts_to_ym "${BASH_REMATCH[2]}"; return
|
||||
fi
|
||||
|
||||
# Pattern 4: MM_DD_YYYY_ prefix (e.g. 08_02_2026_doorbell.mp4)
|
||||
if [[ "$b" =~ ^([0-9]{2})_([0-9]{2})_([0-9]{4})_ ]]; then
|
||||
echo "${BASH_REMATCH[3]} ${BASH_REMATCH[1]}"; return
|
||||
fi
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# move_file SRC ROOT
|
||||
# Moves SRC into ROOT/YYYY/MM/filename using the date extracted from the
|
||||
# filename. Sends undatable files to UNSORTED. Skips if destination exists.
|
||||
# -----------------------------------------------------------------------------
|
||||
move_file() {
|
||||
local src="$1"
|
||||
local root="$2"
|
||||
@@ -39,7 +93,9 @@ move_file() {
|
||||
fname=$(basename "$src")
|
||||
local ym
|
||||
ym=$(get_ym "$src")
|
||||
|
||||
if [[ -z "$ym" ]]; then
|
||||
# No date found — move to _unsorted for manual review
|
||||
mkdir -p "$UNSORTED"
|
||||
if [[ ! -e "$UNSORTED/$fname" ]]; then
|
||||
mv "$src" "$UNSORTED/$fname" && ((unsorted++)) || ((errors++))
|
||||
@@ -48,10 +104,11 @@ move_file() {
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
local year month dest_dir dest
|
||||
year=$(echo "$ym" | cut -d' ' -f1)
|
||||
month=$(echo "$ym" | cut -d' ' -f2)
|
||||
dest_dir="$root/$year/$year-$month"
|
||||
dest_dir="$root/$year/$month"
|
||||
dest="$dest_dir/$fname"
|
||||
mkdir -p "$dest_dir"
|
||||
if [[ -e "$dest" ]]; then
|
||||
@@ -60,12 +117,43 @@ move_file() {
|
||||
mv "$src" "$dest" && ((moved++)) || { echo " ERROR: $fname" >> "$LOG"; ((errors++)); }
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Main loop — process each instant-upload subdirectory
|
||||
# =============================================================================
|
||||
for SUBDIR in Camera BlueIris Facebook scans EufyVideoDir; do
|
||||
SUBPATH="$BASE/$SUBDIR"
|
||||
[[ -d "$SUBPATH" ]] || continue
|
||||
|
||||
# --- Pass 1: files dropped directly into the subdir root ----------------
|
||||
# These are new uploads not yet organised by the Android app.
|
||||
while IFS= read -r -d '' f; do
|
||||
move_file "$f" "$SUBPATH"
|
||||
done < <(find "$SUBPATH" -maxdepth 1 -type f -print0)
|
||||
|
||||
# --- Pass 2: migrate old YYYY/YYYY-MM/ folders → YYYY/MM/ ---------------
|
||||
# Older versions of this script (and some ownCloud client versions) created
|
||||
# YYYY/YYYY-MM/ subfolders, which redundantly repeat the year. This pass
|
||||
# flattens them to YYYY/MM/ and removes the now-empty YYYY-MM directory.
|
||||
for year_dir in "$SUBPATH"/[0-9][0-9][0-9][0-9]; do
|
||||
[[ -d "$year_dir" ]] || continue
|
||||
year=$(basename "$year_dir")
|
||||
for old_dir in "$year_dir"/"$year"-[0-9][0-9]; do
|
||||
[[ -d "$old_dir" ]] || continue
|
||||
month="${old_dir##*-}" # extract MM from YYYY-MM folder name
|
||||
dest_dir="$SUBPATH/$year/$month"
|
||||
mkdir -p "$dest_dir"
|
||||
while IFS= read -r -d '' f; do
|
||||
fname=$(basename "$f")
|
||||
dest="$dest_dir/$fname"
|
||||
if [[ ! -e "$dest" ]]; then
|
||||
mv "$f" "$dest" && ((moved++)) || { echo " ERROR: $fname" >> "$LOG"; ((errors++)); }
|
||||
else
|
||||
((skipped++))
|
||||
fi
|
||||
done < <(find "$old_dir" -maxdepth 1 -type f -print0)
|
||||
rmdir "$old_dir" 2>/dev/null # Remove if now empty
|
||||
done
|
||||
done
|
||||
done
|
||||
|
||||
echo " Moved: $moved Unsorted: $unsorted Skipped: $skipped Errors: $errors" >> "$LOG"
|
||||
|
||||
Reference in New Issue
Block a user