feat: add owncloud photo organizer script and cron setup
This commit is contained in:
@@ -0,0 +1,76 @@
|
|||||||
|
# ownCloud Photo Organizer
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
ownCloud's Android app does not support automatic year/month subfolder creation
|
||||||
|
during instant upload (unlike Nextcloud). This script compensates by running
|
||||||
|
nightly and sorting all uploaded photos and videos into a `YEAR/YEAR-MM/`
|
||||||
|
folder structure.
|
||||||
|
|
||||||
|
## Script
|
||||||
|
|
||||||
|
**Location on server:** `/usr/local/bin/organize_photos.sh`
|
||||||
|
|
||||||
|
**Source folder watched:**
|
||||||
|
`/mnt/INTEL-SSD/ownCloud - Joe Gitta@cloud.jgitta.com/Personal/InstantUpload`
|
||||||
|
|
||||||
|
**Subfolders processed:** `Camera`, `BlueIris`, `Facebook`, `scans`, `EufyVideoDir`
|
||||||
|
|
||||||
|
**Supported file types:** jpg, jpeg, png, gif, heic, webp, bmp, tiff, mp4, mov, pdf
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
The script reads the date from each filename using these patterns (in order):
|
||||||
|
|
||||||
|
1. `YYYYMMDD` embedded in the filename (e.g. `20231022_082107.jpg`)
|
||||||
|
2. `YYYY-MM-DD` or `YYYY_MM_DD` format
|
||||||
|
3. 13-digit Unix timestamp in milliseconds (e.g. `FB_IMG_1572815868839.jpg`)
|
||||||
|
4. `MM_DD_YYYY` prefix (scans folder format, e.g. `06_11_2026_7_28_16_AM.pdf`)
|
||||||
|
|
||||||
|
Files that don't match any pattern are moved to `_unsorted/` for manual review.
|
||||||
|
|
||||||
|
## Output Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
InstantUpload/
|
||||||
|
└── Camera/
|
||||||
|
├── 2023/
|
||||||
|
│ ├── 2023-06/
|
||||||
|
│ └── 2023-12/
|
||||||
|
├── 2024/
|
||||||
|
│ └── 2024-03/
|
||||||
|
└── _unsorted/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cron Job
|
||||||
|
|
||||||
|
Runs nightly at 2:00 AM as root:
|
||||||
|
|
||||||
|
```
|
||||||
|
0 2 * * * /usr/local/bin/organize_photos.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
To view or edit: `sudo crontab -e`
|
||||||
|
|
||||||
|
## Log File
|
||||||
|
|
||||||
|
Each run appends to `/var/log/organize_photos.log`:
|
||||||
|
|
||||||
|
```
|
||||||
|
=== 2026-06-14 02:00:01 Starting ===
|
||||||
|
Moved: 12 Unsorted: 0 Skipped: 0 Errors: 0
|
||||||
|
=== Done ===
|
||||||
|
```
|
||||||
|
|
||||||
|
Check the log anytime with:
|
||||||
|
```bash
|
||||||
|
cat /var/log/organize_photos.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo cp organize_photos.sh /usr/local/bin/organize_photos.sh
|
||||||
|
sudo chmod +x /usr/local/bin/organize_photos.sh
|
||||||
|
(sudo crontab -l; echo "0 2 * * * /usr/local/bin/organize_photos.sh") | sudo crontab -
|
||||||
|
```
|
||||||
Executable
+72
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
BASE="/mnt/INTEL-SSD/ownCloud - Joe Gitta@cloud.jgitta.com/Personal/InstantUpload"
|
||||||
|
UNSORTED="$BASE/_unsorted"
|
||||||
|
LOG="/var/log/organize_photos.log"
|
||||||
|
moved=0; skipped=0; unsorted=0; errors=0
|
||||||
|
|
||||||
|
echo "=== $(date +%Y-%m-%d\ %H:%M:%S) Starting ===" >> "$LOG"
|
||||||
|
|
||||||
|
ts_to_ym() {
|
||||||
|
local ts_ms="$1"
|
||||||
|
local ts_s=$(( ts_ms / 1000 ))
|
||||||
|
date -d "@$ts_s" "+%Y %m" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
get_ym() {
|
||||||
|
local f="$1"
|
||||||
|
local b
|
||||||
|
b=$(basename "$f")
|
||||||
|
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
|
||||||
|
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
|
||||||
|
if [[ "$b" =~ (^|[^0-9])([0-9]{13})([^0-9]|$) ]]; then
|
||||||
|
ts_to_ym "${BASH_REMATCH[2]}"; return
|
||||||
|
fi
|
||||||
|
if [[ "$b" =~ ^([0-9]{2})_([0-9]{2})_([0-9]{4})_ ]]; then
|
||||||
|
echo "${BASH_REMATCH[3]} ${BASH_REMATCH[1]}"; return
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
move_file() {
|
||||||
|
local src="$1"
|
||||||
|
local root="$2"
|
||||||
|
local fname
|
||||||
|
fname=$(basename "$src")
|
||||||
|
local ym
|
||||||
|
ym=$(get_ym "$src")
|
||||||
|
if [[ -z "$ym" ]]; then
|
||||||
|
mkdir -p "$UNSORTED"
|
||||||
|
if [[ ! -e "$UNSORTED/$fname" ]]; then
|
||||||
|
mv "$src" "$UNSORTED/$fname" && ((unsorted++)) || ((errors++))
|
||||||
|
else
|
||||||
|
((skipped++))
|
||||||
|
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="$dest_dir/$fname"
|
||||||
|
mkdir -p "$dest_dir"
|
||||||
|
if [[ -e "$dest" ]]; then
|
||||||
|
((skipped++)); return
|
||||||
|
fi
|
||||||
|
mv "$src" "$dest" && ((moved++)) || { echo " ERROR: $fname" >> "$LOG"; ((errors++)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
for SUBDIR in Camera BlueIris Facebook scans EufyVideoDir; do
|
||||||
|
SUBPATH="$BASE/$SUBDIR"
|
||||||
|
[[ -d "$SUBPATH" ]] || continue
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
move_file "$f" "$SUBPATH"
|
||||||
|
done < <(find "$SUBPATH" -maxdepth 1 -type f -print0)
|
||||||
|
done
|
||||||
|
|
||||||
|
echo " Moved: $moved Unsorted: $unsorted Skipped: $skipped Errors: $errors" >> "$LOG"
|
||||||
|
echo "=== Done ===" >> "$LOG"
|
||||||
Reference in New Issue
Block a user