Auto-delete old unpinned sunrise posts from Mattermost after each upload

sunrise2mm.py: after a successful upload, scan the channel and delete any
post older than MM_SUNRISE_RETENTION_DAYS that is not pinned and has a file
attachment. Pinned posts are always preserved regardless of age.

sky-cam.conf: add MM_SUNRISE_RETENTION_DAYS=8 (independent of the local
disk SUNRISE_RETENTION_DAYS so the two retentions can differ).

Also modernises sunrise2mm.py: uses the same ${VAR:-default} aware
read_config as sunrise.py, consistent error handling, cleaner structure.

https://claude.ai/code/session_01C4jbd3waXG3eKZYbGUjLUQ
This commit is contained in:
Claude
2026-04-22 14:09:27 +00:00
parent 3c50e5dad2
commit 1cf51eaa26
2 changed files with 142 additions and 103 deletions
+3
View File
@@ -225,6 +225,9 @@ SUNRISE_OVERLAY_OPACITY=0.45
# Delete daily sunrise videos older than this many days (rolling window).
# Set 0 to keep forever.
SUNRISE_RETENTION_DAYS=10
# Delete unpinned sunrise posts from Mattermost older than this many days.
# Pinned posts are always kept regardless. Set 0 to disable Mattermost cleanup.
MM_SUNRISE_RETENTION_DAYS=8
# ── Video encoding quality ────────────────────────────────────────────────────
# FFmpeg CRF: lower = higher quality / larger files. Typical range 1830.
+139 -103
View File
@@ -1,127 +1,163 @@
#!/usr/bin/env python3
import requests
import os
import pathlib
import re
import subprocess
from datetime import datetime
import pathlib
import requests
# Locate sky-cam.conf next to this script (works regardless of cwd).
_here = pathlib.Path(__file__).resolve().parent
config_file_path = str(_here / 'sky-cam.conf')
# Function to read configuration from the config file
def read_config(config_file_path):
config = {}
def read_config(path):
conf = {}
try:
with open(config_file_path, 'r') as f:
with open(path) as f:
for line in f:
# Ignore comments and empty lines
if line.strip() and not line.startswith('#'):
key, value = line.strip().split('=', 1)
config[key.strip()] = value.strip()
line = line.strip()
if not line or line.startswith('#') or '=' not in line:
continue
k, v = line.split('=', 1)
k = k.strip()
v = v.strip().strip('"').strip("'")
m = re.match(r'^\$\{[^}]+:-([^}]*)\}$', v)
if m:
v = m.group(1).strip('"').strip("'")
conf[k] = v
except FileNotFoundError:
print(f"Error: The configuration file {config_file_path} was not found.")
exit(1)
except Exception as e:
print(f"Error reading the configuration file: {e}")
exit(1)
return config
pass
return conf
# Read configuration — sky-cam.conf first, then .env overrides (credentials)
config = read_config(config_file_path)
_env_path = str(_here / '.env')
if os.path.isfile(_env_path):
config.update(read_config(_env_path))
# Get the values from the configuration file
mattermost_url = config.get('mattermost_url')
access_token = config.get('access_token')
channel_id = config.get('channel_id')
config = read_config(_here / 'sky-cam.conf')
config.update(read_config(_here / '.env'))
# Check if all required values are provided
if not mattermost_url or not access_token or not channel_id:
print("Error: Missing one or more configuration values.")
exit(1)
mattermost_url = config.get('mattermost_url', '').rstrip('/')
access_token = config.get('access_token', '')
channel_id = config.get('channel_id', '')
# Get today's date
today = datetime.now()
today_date_str = today.strftime("%Y-%m-%d") # e.g., "2024-11-05"
today_month_str = today.strftime("%Y-%m") # e.g., "2024-11"
if not all([mattermost_url, access_token, channel_id]):
print("Error: mattermost_url / access_token / channel_id not set in .env")
raise SystemExit(1)
# Set the full folder path for today's video
base_video_folder = os.path.join(config.get('MOVIES_DIR', ''), config.get('SUNRISE_CAM', 'sunrise'))
output_dir = os.path.join(base_video_folder, today_month_str, "sunrise-only")
headers = {'Authorization': f'Bearer {access_token}'}
# Check if the directory exists
if not os.path.isdir(output_dir):
print(f"Error: The directory {output_dir} does not exist. The video may not have been created yet.")
exit(1)
today_date_str = datetime.now().strftime('%Y-%m-%d')
today_month_str = datetime.now().strftime('%Y-%m')
final_video_file = os.path.join(output_dir, f"{today_date_str}-daily-sunrise.mp4")
movies_dir = config.get('MOVIES_DIR', '')
sunrise_cam = config.get('SUNRISE_CAM', 'east')
output_dir = os.path.join(movies_dir, sunrise_cam, today_month_str, 'sunrise-only')
video_file = os.path.join(output_dir, f'{today_date_str}-daily-sunrise.mp4')
# Debugging: print the full path of the video file to make sure it's correct
print(f"Looking for video file: {final_video_file}")
print(f"Looking for video file: {video_file}")
# Check if the video file exists
if os.path.isfile(final_video_file):
# Upload the video file
with open(final_video_file, 'rb') as f:
response = requests.post(
f"{mattermost_url}/api/v4/files",
headers={'Authorization': f'Bearer {access_token}'},
files={'files': f},
data={'channel_id': channel_id}
)
if response.status_code == 201:
print(f"Successfully uploaded {final_video_file}")
# Get the file ID from the response
file_id = response.json().get('file_infos', [{}])[0].get('id')
# Send a message with the file reference
message_data = {
'channel_id': channel_id,
'message': f"Here is a video of the sunrise for {today_date_str}",
'file_ids': [file_id] # Attach the uploaded file
}
message_response = requests.post(
f"{mattermost_url}/api/v4/posts",
headers={'Authorization': f'Bearer {access_token}'},
json=message_data
)
if message_response.status_code == 201:
print("Message posted successfully.")
subprocess.run(
[str(_here / 'notify.sh'),
f"Sunrise uploaded: {today_date_str}",
f"Video posted to Mattermost successfully"],
check=False
)
else:
print(f"Failed to post message: {message_response.text}")
subprocess.run(
[str(_here / 'notify.sh'),
f"FAILED: sunrise Mattermost post {today_date_str}",
f"File uploaded but post failed: {message_response.status_code}"],
check=False
)
else:
print(f"Failed to upload {final_video_file}: {response.text}")
subprocess.run(
[str(_here / 'notify.sh'),
f"FAILED: sunrise upload {today_date_str}",
f"Mattermost file upload failed: {response.status_code}"],
check=False
)
else:
print(f"No video file found at {final_video_file}.")
if not os.path.isfile(video_file):
print(f"No video file found at {video_file}.")
subprocess.run(
[str(_here / 'notify.sh'),
f"FAILED: sunrise upload {today_date_str}",
f"Video file not found: {final_video_file}"],
f'FAILED: sunrise upload {today_date_str}',
f'Video file not found: {video_file}'],
check=False
)
raise SystemExit(1)
# ── Upload ────────────────────────────────────────────────────────────────────
with open(video_file, 'rb') as f:
resp = requests.post(
f'{mattermost_url}/api/v4/files',
headers=headers,
files={'files': f},
data={'channel_id': channel_id}
)
if resp.status_code != 201:
print(f'Failed to upload: {resp.text}')
subprocess.run(
[str(_here / 'notify.sh'),
f'FAILED: sunrise upload {today_date_str}',
f'Mattermost file upload failed: {resp.status_code}'],
check=False
)
raise SystemExit(1)
print(f'Uploaded {video_file}')
file_id = resp.json()['file_infos'][0]['id']
post_resp = requests.post(
f'{mattermost_url}/api/v4/posts',
headers=headers,
json={
'channel_id': channel_id,
'message': f'Sunrise {today_date_str}',
'file_ids': [file_id],
}
)
if post_resp.status_code == 201:
print('Message posted successfully.')
subprocess.run(
[str(_here / 'notify.sh'),
f'Sunrise uploaded: {today_date_str}',
'Video posted to Mattermost successfully'],
check=False
)
else:
print(f'Failed to post message: {post_resp.text}')
subprocess.run(
[str(_here / 'notify.sh'),
f'FAILED: sunrise Mattermost post {today_date_str}',
f'File uploaded but post failed: {post_resp.status_code}'],
check=False
)
raise SystemExit(1)
# ── Cleanup old unpinned posts ─────────────────────────────────────────────────
retention_days = int(config.get('MM_SUNRISE_RETENTION_DAYS', 8))
if retention_days <= 0:
raise SystemExit(0)
cutoff_ms = (datetime.now().timestamp() - retention_days * 86400) * 1000
deleted = 0
page = 0
while True:
r = requests.get(
f'{mattermost_url}/api/v4/channels/{channel_id}/posts',
headers=headers,
params={'page': page, 'per_page': 200}
)
if r.status_code != 200:
print(f'Cleanup: could not fetch posts ({r.status_code}) — skipping')
break
data = r.json()
posts = data.get('posts', {})
order = data.get('order', [])
if not order:
break
for post_id in order:
post = posts[post_id]
if post.get('is_pinned'):
continue
if not post.get('file_ids'):
continue
if post['create_at'] >= cutoff_ms:
continue
del_r = requests.delete(
f'{mattermost_url}/api/v4/posts/{post_id}',
headers=headers
)
if del_r.status_code == 200:
deleted += 1
if len(order) < 200:
break
page += 1
if deleted:
print(f'Cleanup: removed {deleted} unpinned post(s) older than {retention_days} days')