Restructure find loop to iterate known CAMERAS+sunrise in the main shell
so cam_name is visible in the while body. Previously the for loop ran in
a process substitution subshell, making cam_name unavailable and causing
top-level year dirs (e.g. 2026/) to be treated as camera names.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Moves Season/MvtN mp4 files from flat camera dirs into
astro-YEAR/<cam>/Season/MvtN/ using season_info.py for correct
year assignment. Handles cross-year mixing in old backup structure.
Dry-run support; never deletes files.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Re-processes *-temp.mp4 files from a backup location using season_info.py
to compute the correct target duration per clip (music_duration / DAYS_IN_MVT),
producing properly-sized *-final.mp4 files without touching the backup source.
Skips finals that already exist. Fixes drift caused by old pipeline sizing clips
to the wrong movement structure.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
- sky-cam.conf: add ENCODE_PRESET=slow (applied to all encodes);
split CRF_MONTAGE (intermediates, H.264) from CRF_MONTAGE_FINAL=22
(archive montage, H.265); add SUNRISE_RETENTION_DAYS=10; improve
CAPTURE_INTERVAL comment with storage/density table
- montage-mvt.sh: steps 1+2 use preset; step 3 final switches to
libx265 + CRF_MONTAGE_FINAL + -tag:v hvc1 for broad compatibility
- 4-seasons.sh: both encode steps use ENCODE_PRESET
- daily_sunrise_video.sh: all encode steps use ENCODE_PRESET; rolling
retention deletes sunrise videos older than SUNRISE_RETENTION_DAYS
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
- verify-mvt.sh: remove dead first _info= line (used MVT_START before
defined); fix JPEG size loop to accumulate bytes correctly and display
human-readable total; show size in delete confirmation
- montage-mvt.sh: grep -c uses || echo 0 instead of || true to be
unambiguous about the no-match value under set -e
- capture.sh, capture-watchdog.sh: require camera name arg, remove
silent SUNRISE_CAM fallback
- migrate-seasons.sh: require --cam arg, error if missing
- README.md, sky-cam.conf: remove stale fullday-video.sh references;
README pipeline diagram updated to show verify-mvt.sh
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
No script defaults to SUNRISE_CAM (east) for the seasons pipeline.
Every direction is treated equally — camera name is a required arg,
not an optional override.
- 4-seasons.sh: $1 required, exits with usage if missing
- montage-mvt.sh: arg order changed to <cam> [date]; $1 required
- year-end-join.sh: $2 (cam) required
- verify-mvt.sh: $4 (cam) required
- migrate-seasons.sh: example commands updated to new arg order
- README.md: montage-mvt.sh examples updated
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
- sky-cam.conf: CAMERAS now includes east, north, south with staggered
SCHEDULE_SEASONS times (01:00, 01:30, 02:00); fullday schedules and
tuning removed
- install.sh: fullday service/timer generation removed entirely
- fullday-video.sh + systemd units: deleted
- montage-mvt.sh: post-build technical checks (duration drift, video/audio
stream presence) added; notification now includes check results and the
exact verify-mvt.sh command to run
- verify-mvt.sh: new interactive CLI for montage review — shows checks,
plays video, deletes source JPEG folders on approval, or re-runs montage
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
--cam NAME overrides SUNRISE_CAM so the script can be used for any
camera (e.g. north). Summer is now included in the find targets so
any ASTRO_YEAR=2025 Summer clips (Summer 2026) are also migrated.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
The old pattern *Mvt*3* matched .mp3 in every MP3 filename, so sort|head-1
always picked track 01 (Mvt1). Fix by finding all files for the season
then filtering with grep -iE "Mvt[^0-9]*N[^0-9]", which requires the
movement number to be surrounded by non-digit characters (space, dot,
underscore) and cannot match digits embedded in .mp3 or track prefixes.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
All *-final.mp4 clips are valid footage regardless of what speed factor
the old script used — montage-mvt.sh re-adjusts the full concatenated
video to match music duration anyway, so per-clip speed is irrelevant.
Move every ASTRO_YEAR=2025 clip to the correct new movement directory
based on its date. Spring Mvt1 now gets all 30 clips (Mar 21–Apr 19).
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
- Filter to ASTRO_YEAR=2025 (Winter 2025-26 onward); skip Autumn 2025 and earlier
- Move files instead of creating symlinks
- Quarantine clips matching Day<N>of92 (Spring clips from broken script with
wrong 92-day movement total) to quarantine-old-scripts/ for review + deletion
- Preserve clips with Dayof92 (no day number, old legacy format) — these are
migrated normally since montage-mvt.sh re-adjusts speed regardless
- Summary shows delete commands for temp files and quarantine folder
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
migrate-seasons.sh uses season_info.py to re-classify every old
*-final.mp4 clip by its YYYY-MM-DD filename prefix, placing a symlink
in the correct new-system directory regardless of how the old system
labelled movements. Handles the boundary mismatches between old and
new movement splits automatically.
Coverage after migration (based on available clips):
Spring 2026 Mvt1 30/31 days (missing Day 1 only — excellent)
Winter Mvt1 ~18/29 days
Winter Mvt2 ~24/29 days
Winter Mvt3 ~20/31 days
Autumn Mvt1 ~24/30 days
Autumn Mvt3 ~18/30 days
Autumn Mvt2 absent — year-end join will not fire for 2024 cycle
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
capture-watchdog.sh: long-running script that checks the newest JPEG mtime
in each camera's output directory every CAPTURE_STALE_SECS/3 seconds (min 5s).
Sends a stall notification via notify.sh if no new frame has arrived within
CAPTURE_STALE_SECS. Sends a recovery notification once new frames resume.
Only alerts if the camera was previously working (avoids false positives on
first start or overnight before the first frame of the day).
sky-cam.conf: CAPTURE_STALE_SECS=30 added next to CAPTURE_INTERVAL with a
note that it should be at least 2x the interval.
install.sh: generates sky-cam-watchdog-<cam>.service for every camera that
has a RTSP URL configured. The watchdog service starts after and alongside
the capture service, restarts on failure, and is enabled/started with the
same loop as the capture service.
README.md: note about watchdog and journalctl command for its logs.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
montage-mvt.sh, year-end-join.sh: CAM_NAME fallback referenced undefined
\$CAM_NAME — crashes under set -euo pipefail on manual runs without args.
Fixed to fall back to \$SUNRISE_CAM (same fix applied earlier to other scripts).
notify.sh: Mattermost notification guard only checked MM_NOTIFY_CHANNEL_ID,
not mattermost_url or access_token. If those are unset, curl would send a
malformed request silently. Added guards for all three required values.
install.sh: seasons/fullday service units had literal \n in After=/Wants=
lines because bash does not interpret \n in heredoc variable expansions.
Changed to \$'...' syntax so actual newlines are written, making valid
systemd unit files. Also: audio capture timer now uses \$SCHEDULE_SUNRISE
instead of hardcoded 03:00, keeping it in sync if the user changes the
schedule.
sunrise-audio-capture.sh: record_start_sec could theoretically go negative
(sunrise very early + large SUNRISE_TARGET_SECS). Added floor-at-zero guard.
daily_sunrise_video.sh: added comment explaining why audio_offset is always 0.
README.md: fix CAM_RTSP_sunrise example to CAM_RTSP_east.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Cameras are now named by direction (east, north, south, west) throughout
config defaults, examples, and documentation. The config key SUNRISE_CAM
is unchanged — it still means 'which camera faces east and gets the sunrise
job'. Only the camera instance name value changes from 'sunrise' to 'east'.
sky-cam.conf: CAMERAS=(east), SUNRISE_CAM=east, SCHEDULE_*_east,
example comments updated to show all four compass points.
README.md: all command examples updated (4-seasons.sh east, journalctl
sky-cam-capture-east, etc.) — phenomenon references ('sunrise window',
'sunrise time') left unchanged.
bootstrap.sh: example updated to show compass directions.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
- Step 1 is now dependency installation (apt + pip3) before anything else
- SCRIPT_DIR removed from config table (auto-detected)
- RTSP/Mattermost credentials moved to .env section (step 4)
- install.sh now shown as generating .env.example before credentials are filled in
- Schedule settings explained: SEASONS/FULLDAY process yesterday; SUNRISE waits internally
- How it works: audio capture described as SUNRISE_TARGET_SECS centred on sunrise
- daily_sunrise_video.sh pipeline updated: steps 3a/3b shown separately
- Resilience section replaced with four-tier audio/overlay matrix table
- Camera audio section updated: exact duration, centred split, odd-second rule
- Manual operations: examples use explicit camera name argument
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
4-seasons.sh, fullday-video.sh: CAM_NAME fallback referenced undefined
\$CAM_NAME — under set -euo pipefail this crashes if called manually without
an argument. Changed to fall back to \$SUNRISE_CAM (matches capture.sh).
daily_sunrise_video.sh: add log message when the capture window has already
closed (manual re-run after the fact). Behaviour is unchanged — processing
proceeds immediately — but the operator now gets a clear confirmation rather
than silence.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Previously sunrise-audio-capture.sh recorded the entire capture window
(70+ minutes) and daily_sunrise_video.sh sought to the right position with
a complex offset calculation.
Now: record floor(TARGET/2) seconds before sunrise and ceil(TARGET/2) after —
odd second goes to post-sunrise. The file is exactly SUNRISE_TARGET_SECS long
and already centred, so no offset is needed when mixing.
AUDIO_PRE_BUFFER_MIN removed — it was part of the old whole-window scheme
and is no longer meaningful.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
sky-cam.conf:
- SCRIPT_DIR auto-detects from its own location via BASH_SOURCE — no manual
path entry needed after bootstrap; override still works via environment.
- BASE_DIR defaults to \$SCRIPT_DIR/data; MOVIES_DIR defaults to \$BASE_DIR/movies.
Override either to point at a drive/mount as before.
- SCHEDULE_SUNRISE changed to 03:00 — the script now waits internally until
the capture window closes before touching images, so the schedule just needs
to be early enough, not timed exactly to sunrise.
- Schedule section rewritten to explain what each job does and what day's data
it processes (SEASONS/FULLDAY always process yesterday).
- SUNRISE_PRE_MIN/POST_MIN comments explain the window and its relationship to
SUNRISE_TARGET_SECS.
- SUNRISE_OVERLAY_ENABLED=true added; set false to skip the timestamp entirely.
daily_sunrise_video.sh:
- After calculating the capture window, sleep until end_sec+30s if we fired
before the window finished — guarantees all images are present.
- DT (drawtext filter) is only built when SUNRISE_OVERLAY_ENABLED=true.
- Step 3b skips ffmpeg entirely when overlay is disabled, moving work_video
directly to final_video (faster, no re-encode).
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
daily_sunrise_video.sh: use \$MOVIES_DIR instead of \$BASE_DIR/movies so the
override in sky-cam.conf is respected when videos live on a separate drive.
capture.sh: default camera name falls back to \$SUNRISE_CAM (not the
undefined \$CAM_NAME) when no argument is passed.
sunrise2mm.py: also loads .env after sky-cam.conf so Mattermost credentials
moved to .env are actually visible to the upload script.
sunrise.py: raise a clear error message when LATITUDE/LONGITUDE/TIMEZONE
are missing from sky-cam.conf instead of an opaque KeyError.
4-seasons.sh, montage-mvt.sh: capture season_info.py output before eval so
a Python failure exits cleanly with a diagnostic message rather than
silently continuing with undefined variables.
bootstrap.sh: add system package install step (ffmpeg bc fonts-dejavu) and
show the .env setup step after install.sh generates .env.example.
README.md: correct Python package names and add fonts-dejavu dependency.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
sky-cam.conf now sources .env from the same directory at the bottom of the
file, so RTSP URLs, Mattermost tokens, and notification credentials can live
outside version control. Plaintext credential placeholders removed from conf.
install.sh generates .env.example on every run with the correct CAM_RTSP_*
variable names derived from the CAMERAS list, so users always know exactly
what to put in .env for their specific camera setup.
.gitignore: add .env and sunrise-sounds/ (downloaded audio library).
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
daily_sunrise_video.sh: derive CAM_NAME from $1 (defaulting to SUNRISE_CAM)
instead of an undefined variable; this was causing empty image/output paths.
install.sh: pass $SUNRISE_CAM as argument to daily_sunrise_video.sh so it
matches the pattern used by every other per-camera script.
sunrise2mm.py: use SUNRISE_CAM config key (not the undefined CAM_NAME) to
locate the output directory; fixes silent path mismatch on non-default names.
4-seasons.sh: remove stray `set -x` that was flooding systemd logs with
shell trace output.
README.md: correct Python dependencies from `ephem` to `suntime pytz` to
match what sunrise.py actually imports.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Each stage can now fail independently, giving four upload paths:
audio+overlay, audio-only, overlay-only, or speed-only video.
Step 3a copies the video stream (no re-encode) when mixing audio,
so step 3b failure still leaves a clean audio-mixed file to promote.
Overlay failure notification now reports which quality was uploaded.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
daily_sunrise_video.sh: priority chain is camera mic → library random MP3 →
overlay-only → sped video promotion. If audio mixing fails a warning
notification fires and the script retries with overlay-only so a video is
always produced and uploaded.
download-sunrise-sounds.py: downloads CC-licensed ambient sounds from
freesound.org into 11 weather/season category folders (~275 files total at
25 per category), writing a manifest.json with attribution data.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Removes dependency on MotionEye or any NVR software. Everything is now
CLI/ffmpeg configured entirely from sky-cam.conf.
capture.sh
Long-running systemd service (one per camera) that pulls frames from the
camera's RTSP stream at CAPTURE_INTERVAL seconds, writing HH-MM-SS.jpg
into BASE_DIR/<cam>/YYYY-MM-DD/. Restarts at midnight for the new date
directory; auto-reconnects on camera disconnect.
sunrise-audio-capture.sh
One-shot service triggered at 03:00. Waits until (sunrise minus
SUNRISE_PRE_MIN minus AUDIO_PRE_BUFFER_MIN), then records the RTSP audio
stream for the full sunrise window. Output deleted after mixing.
daily_sunrise_video.sh
Step 3 now mixes in sunrise-audio.m4a when AUDIO_ENABLED=true and the
file exists. Audio is centred on actual sunrise time at natural speed
while the video plays as the sped-up timelapse. Uses filter_complex
when audio is present (can't combine -vf with -filter_complex).
install.sh
Generates sky-cam-capture-<cam>.service for each camera with CAM_RTSP_<cam>
set, and sky-cam-audio-capture.{service,timer} when AUDIO_ENABLED=true.
sky-cam.conf
New settings: CAM_RTSP_<cam>, CAPTURE_INTERVAL, AUDIO_ENABLED,
AUDIO_PRE_BUFFER_MIN, CAPTURE_AUDIO_BITRATE.
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
Sunrise:
- Overlay failure no longer exits 1; the speed-only video is renamed to the
expected final path so OnSuccess still fires and the upload still happens
- sunrise2mm.py now notifies on every upload outcome (success, post failure,
file-not-found) via notify.sh so the user always knows whether the video
reached Mattermost
Montage:
- Music+overlay failure no longer exits 1; the sped video is promoted to the
*-Montage.mp4 path so year-end-join can still include this movement (as a
silent, no-overlay clip) and the user is notified of the degraded output
- Year-end join trigger is now outside the success/failure branch so it fires
in both cases
https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ