From 1cf51eaa26ec56d27053f5cd71c18eea0fb1c111 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Apr 2026 14:09:27 +0000 Subject: [PATCH] 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 --- sky-cam.conf | 3 + sunrise2mm.py | 242 +++++++++++++++++++++++++++++--------------------- 2 files changed, 142 insertions(+), 103 deletions(-) diff --git a/sky-cam.conf b/sky-cam.conf index aeab692..1d3add9 100644 --- a/sky-cam.conf +++ b/sky-cam.conf @@ -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 18–30. diff --git a/sunrise2mm.py b/sunrise2mm.py index af2357e..5163771 100755 --- a/sunrise2mm.py +++ b/sunrise2mm.py @@ -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')