182 lines
6.7 KiB
Python
182 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"
|
|
# ====================================
|
|
|
|
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='https://gitea.weircon.dk/{{repo}}'>gitea.weircon.dk/{{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"
|
|
"https://gitea.weircon.dk/{{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!") |