Initial commit
This commit is contained in:
+182
@@ -0,0 +1,182 @@
|
||||
#!/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!")
|
||||
Reference in New Issue
Block a user