feat: add an unattended wrapper, and document running this without an agent

gdl-sync.py always needed a fistful of paths on the command line, and the
2026-08-20 run published to a local directory and then rsynced to the NAS by
hand -- fine interactively, impossible from cron. gdl-cron.sh takes one
argument (stories|full|sweep) and does the whole thing.

Staging is wiped every run deliberately: what we already hold is decided by
the skip-archive, never by what happens to be sitting in staging, so starting
empty is correct and keeps the publish to just the new files.

Documents the three cadences and what each is for -- notably that only `sweep`
notices carousels edited after we archived them, and that only `stories` loses
content if it is skipped, since stories cannot be backfilled.

Cron notes the two things that would silently break it: cron runs /bin/sh so
$RANDOM does not exist (use shuf), and `%` in a crontab line means newline.
systemd is offered as the better option because RandomizedDelaySec and
Persistent=true are built in -- a host asleep at 09:00 silently skips a cron
job, and a skipped stories run is content gone for good.

The publish step is still blocked on mattellite having no key on the NAS; that
is recorded under Outstanding as the one thing between this and unattended.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXfdJu7QhSJLr47K7koTDF
This commit is contained in:
2026-08-20 19:59:57 -04:00
co-authored by Claude Opus 5
parent ff0d0f9f78
commit ef99389cf4
2 changed files with 138 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
#!/bin/sh
# Unattended wrapper around gdl-sync.py. One argument: the run mode.
#
# stories daily ~6 requests; the only surface that cannot be backfilled
# full monthly every surface, --abort 50 (stops at already-held content)
# sweep quarterly every surface, no abort -- the only run that notices
# carousels edited after we archived them
#
# Exits non-zero if the sync does, so cron mails you. Everything is logged.
set -eu
MODE="${1:?usage: gdl-cron.sh stories|full|sweep}"
GDL_HOME="${GDL_HOME:-$HOME/gdl}"
INDEX="${GDL_INDEX:-https://instaarchive.ergosteur.com}"
PUBLISH="${GDL_PUBLISH:-agentapi@10.20.28.200:/volume1/rslsync/sync/Instagram-archive/archives/}"
STAGING="$GDL_HOME/staging-$MODE"
LOG="$GDL_HOME/logs/$MODE-$(date +%Y%m%d-%H%M%S).log"
PATH="$HOME/.local/bin:$PATH"; export PATH
case "$MODE" in
stories) ARGS="--only stories" ;;
full) ARGS="--only posts,reels,stories,highlights --abort 50" ;;
sweep) ARGS="--only posts,reels,stories,highlights" ;;
*) echo "unknown mode: $MODE" >&2; exit 2 ;;
esac
mkdir -p "$GDL_HOME/logs"
# Staging is wiped every run ON PURPOSE. What we already hold is decided by the
# skip-archive (--download-archive), never by which files happen to be sitting
# in staging, so starting empty is correct -- and it keeps the publish rsync
# to just the new files instead of re-walking gigabytes each time.
rm -rf "$STAGING"
{
echo "=== $MODE run $(date -Is) ==="
# shellcheck disable=SC2086
"$GDL_HOME/gdl-sync.py" \
--index "$INDEX" \
--staging "$STAGING" \
--publish "$PUBLISH" \
--archive-db "$GDL_HOME/artms.db" \
--urls-file "$GDL_HOME/artms_account_links.txt" \
$ARGS --execute
status=$?
echo "=== exit $status at $(date -Is) ==="
exit $status
} 2>&1 | tee -a "$LOG"
# Keep the log directory from growing without bound.
ls -1t "$GDL_HOME/logs" | tail -n +30 | while read -r old; do
rm -f "$GDL_HOME/logs/$old"
done