Files
fredagsbar-meeting-generator/generator.py
2026-01-30 12:30:51 +01:00

184 lines
6.7 KiB
Python

#!/usr/bin/env python3
"""
fredagsbar_ics_generator_v3.py
Genererer .ics invitationer baseret på JSON stories.
Understøtter nu 'hints' i filnavne og headers.
"""
import json
import argparse
import sys
from ics import Calendar, Event
from datetime import datetime, date, time, timedelta
from zoneinfo import ZoneInfo
import uuid
import os
import re
# ====== STANDARD INDSTILLINGER ======
START_DATE = date(2026, 1, 29)
START_TIME = time(15, 00)
DURATION_MINUTES = 60
TIMEZONE = ZoneInfo("Europe/Copenhagen")
OUTPUT_BASE_DIR = "fredagsbar_output"
REPO = "https://gitea.weircon.dk/agw/fredagsbar-meeting-generator";
# ====================================
def load_story(filepath):
"""Indlæser JSON fil og validerer strukturen."""
try:
with open(filepath, 'r', encoding='utf-8') as f:
data = json.load(f)
return data
except Exception as e:
print(f"Fejl ved indlæsning af {filepath}: {e}")
sys.exit(1)
def next_or_same_friday(d: date) -> date:
return d + timedelta(days=(4 - d.weekday()) % 7)
def sanitize_filename(s: str) -> str:
s = re.sub(r"[^\w\s-]", "", s, flags=re.UNICODE)
s = re.sub(r"\s+", "_", s.strip())
return s[:120]
def strip_html_tags(text: str) -> str:
clean = re.compile('<.*?>')
return re.sub(clean, '', text)
def inject_outlook_lines(ical_text: str, html_description: str) -> str:
"""Indsætter Outlook-specifikke linjer og HTML beskrivelse."""
injection_status = "TRANSP:OPAQUE\r\nX-MICROSOFT-CDO-BUSYSTATUS:BUSY\r\n"
# Outlook kræver often at HTML er på én linje eller foldet korrekt
html_oneline = html_description.replace("\n", "")
injection_html = f"X-ALT-DESC;FMTTYPE=text/html:<!DOCTYPE html><html><body>{html_oneline}</body></html>\r\n"
def replacer(match):
vevent = match.group(0)
if "X-MICROSOFT-CDO-BUSYSTATUS" not in vevent:
vevent = vevent.replace("\r\nEND:VEVENT", "\r\n" + injection_status + "END:VEVENT")
# Overskriv eller indsæt X-ALT-DESC
if "X-ALT-DESC" in vevent:
# Simpel håndtering: Hvis vi allerede har det, ignorer (eller implementer regex replace af X-ALT-DESC)
pass
else:
vevent = vevent.replace("\r\nEND:VEVENT", "\r\n" + injection_html + "END:VEVENT")
return vevent
text_crlf = ical_text.replace("\n", "\r\n")
vevent_pattern = re.compile(r"BEGIN:VEVENT[\s\S]*?END:VEVENT", flags=re.IGNORECASE)
return vevent_pattern.sub(replacer, text_crlf)
def create_event_ics_file(event_data, meta, log_index, total_logs, start_dt, end_dt, out_path):
title = event_data["title"]
story_text = event_data["story"]
hint = event_data.get("hint", "") # Hent hint hvis det findes
cal = Calendar()
event = Event()
event.uid = str(uuid.uuid4())
# Hvis der er et hint, sæt det evt. ind i titlen eller behold titlen ren corporate
event.name = title
event.begin = start_dt
event.end = end_dt
event.status = "CONFIRMED"
event.classification = "PUBLIC"
# Meta styling
font = meta.get("font", "Arial, sans-serif")
bg_color = meta.get("bg_color", "#f0f0f0")
text_color = meta.get("text_color", "#000000")
theme_color = meta.get("theme_color", "#000000")
log_prefix = meta.get("log_prefix", "LOG")
# Byg Header strengen
header_text = f"// {log_prefix} {log_index:02d}/{total_logs} // SUBJECT: {title.upper()}"
if hint:
header_text += f" // CODE: {hint}"
# --- HTML Version (Outlook) ---
html_desc = (
f"<div style='font-family: {font}; color: {text_color};'>"
f"<b style='color:{theme_color}'>{header_text}</b><br><br>"
f"<div style='background-color: {bg_color}; padding: 15px; border-left: 5px solid {theme_color};'>"
f"{story_text}" # Her indsætter vi story direkte, da den nu indeholder HTML tags fra JSON
f"</div><br>"
"<hr>"
"<b>OBS: Dette er en invitation til en fredagsbar.</b><br>"
"Dette event er automatisk genereret til Fredagsbar, med en title der giver Nicolaj mulighed for at deltage.<br>"
"Ingen forberedelse nødvendig.<br><br>"
"<span style='font-size: 10px; color: #666;'>"
"Vibecoded sourcecode til generering kan findes her: <a href='"+ repo + "'> " + repo + "</a>"
"</span>"
"</div>"
)
# --- Plain Text Version ---
# Vi erstatter <br> med \n for læsbarhed i plain text
plain_story = story_text.replace("<br>", "\n").replace("<b>", "").replace("</b>", "").replace("<i>", "").replace("</i>", "")
plain_desc = (
f"{header_text}\n\n"
f"{plain_story}\n\n"
"--------------------------------------------------\n"
"OBS: Dette er en invitation til en fredagsbar.\n"
"Ingen forberedelse nødvendig.\n"
+ repo
)
event.description = plain_desc
cal.events.add(event)
serialized = cal.serialize()
processed = inject_outlook_lines(serialized, html_desc)
with open(out_path, "w", encoding="utf-8", newline="\r\n") as f:
f.write(processed)
print(f"[{log_index}/{total_logs}] '{title}' -> {out_path}")
def generate_series(json_path):
data = load_story(json_path)
meta = data["meta"]
events = data["events"]
story_slug = sanitize_filename(meta.get("name", "story"))
output_dir = os.path.join(OUTPUT_BASE_DIR, story_slug)
os.makedirs(output_dir, exist_ok=True)
print(f"--- Starter generering af: {meta.get('name')} ---")
first_friday = next_or_same_friday(START_DATE)
total_logs = len(events)
for idx, event_entry in enumerate(events):
event_date = first_friday + timedelta(weeks=idx)
start_dt = datetime.combine(event_date, START_TIME).replace(tzinfo=TIMEZONE)
end_dt = start_dt + timedelta(minutes=DURATION_MINUTES)
log_num = idx + 1
# Bruger hint i filnavnet hvis det findes
hint_part = ""
if "hint" in event_entry:
hint_part = f"_{sanitize_filename(event_entry['hint'])}"
fname = f"{event_date.strftime('%Y%m%d')}_Log{log_num}{hint_part}_{sanitize_filename(event_entry['title'])}.ics"
out_path = os.path.join(output_dir, fname)
create_event_ics_file(event_entry, meta, log_num, total_logs, start_dt, end_dt, out_path)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generer Fredagsbar ICS filer fra JSON historie.")
parser.add_argument("story_file", help="Sti til JSON filen med historien (f.eks. stories/story_scp.json)")
args = parser.parse_args()
if not os.path.exists(args.story_file):
print(f"Fejl: Filen '{args.story_file}' findes ikke.")
sys.exit(1)
generate_series(args.story_file)
print("\nFærdig!")