#!/usr/bin/env python3
"""
FAQ RSS Generator
Generates FAQ.RSS from YAML game documentation files.
Produces clean HTML-escaped content without CDATA, using
for line breaks.
"""
import os
import sys
import yaml
import html
import xml.etree.ElementTree as ET
from datetime import datetime
import shutil
from pathlib import Path
class FAQRSSGenerator:
def __init__(self, data_dir="data/games", output_file="FAQ.RSS"):
self.data_dir = Path(data_dir)
self.output_file = Path(output_file)
self.games = []
def load_games(self):
"""Load all YAML game files from data directory"""
self.games = []
if not self.data_dir.exists():
print(f"Error: Data directory {self.data_dir} does not exist")
return False
yaml_files = list(self.data_dir.glob("*.yml")) + list(self.data_dir.glob("*.yaml"))
if not yaml_files:
print(f"Warning: No YAML files found in {self.data_dir}")
return True
for yaml_file in yaml_files:
try:
with open(yaml_file, 'r', encoding='utf-8') as f:
game_data = yaml.safe_load(f)
if self.validate_game_data(game_data, yaml_file):
self.games.append(game_data)
print(f"Loaded: {game_data['name']}")
except Exception as e:
print(f"Error loading {yaml_file}: {e}")
return True
def validate_game_data(self, data, filename):
"""Validate game YAML data structure"""
required_fields = ['name', 'supports_workshop', 'startup', 'configs', 'troubleshooting']
for field in required_fields:
if field not in data:
print(f"Error in {filename}: Missing required field '{field}'")
return False
# Validate startup section
startup = data['startup']
if 'default_command' not in startup:
print(f"Error in {filename}: Missing 'default_command' in startup section")
return False
# Validate workshop section if supported
if data['supports_workshop'] and 'workshop' not in data:
print(f"Error in {filename}: supports_workshop is true but 'workshop' section missing")
return False
return True
def escape_html_content(self, text):
"""Escape HTML content and convert newlines to
tags"""
if not text:
return ""
# Escape HTML entities
escaped = html.escape(str(text))
# Convert newlines to
tags
escaped = escaped.replace('\n', '<br>')
return escaped
def generate_config_files_content(self, game):
"""Generate Config Files section content"""
content = "<strong>Configuration Files</strong><br>"
for config in game['configs']:
file_name = config['file']
paths = config.get('paths', [])
desc = config.get('desc', '')
content += "<br>- " + self.escape_html_content(file_name)
if paths:
path_str = ", ".join(paths)
content += " — " + self.escape_html_content(desc) + ". Paths: " + self.escape_html_content(path_str)
elif desc:
content += " — " + self.escape_html_content(desc)
content += "<br>"
return content
def generate_startup_parameters_content(self, game):
"""Generate Startup Parameters section content"""
startup = game['startup']
content = "<strong>Default Command Line</strong><br>"
content += "<br>" + self.escape_html_content(startup['default_command']) + "<br>"
# Port scheme
if 'ports' in startup and startup['ports']:
content += "<br><strong>Port Scheme</strong><br>"
for port in startup['ports']:
label = port.get('label', '')
port_num = port.get('port', '')
proto = port.get('proto', '')
relative = port.get('relative', '')
content += "<br>- " + self.escape_html_content(f"{label} ({proto}) — {relative} (default {port_num})") + "<br>"
# Command line flags
if 'flags' in startup and startup['flags']:
content += "<br><strong>Command Line Flags</strong><br>"
for flag in startup['flags']:
flag_name = flag.get('flag', '')
default = flag.get('default', '')
flag_type = flag.get('type', '')
desc = flag.get('desc', '')
content += "<br>" + self.escape_html_content(f"{flag_name} — {desc}. Type: {flag_type}, Default: {default}") + "<br>"
return content
def generate_troubleshooting_content(self, game):
"""Generate Troubleshooting section content"""
content = "<strong>Common Issues and Solutions</strong><br>"
for issue in game['troubleshooting']:
content += "<br>- " + self.escape_html_content(issue) + "<br>"
return content
def generate_workshop_content(self, game):
"""Generate Steam Workshop section content"""
if not game.get('supports_workshop', False):
return None
workshop = game.get('workshop', {})
content = "<strong>Steam Workshop Configuration</strong><br>"
notes = workshop.get('notes', [])
for note in notes:
content += "<br>- " + self.escape_html_content(note) + "<br>"
return content
def create_rss_item(self, title, category, content):
"""Create RSS item element"""
item = ET.Element('item')
title_elem = ET.SubElement(item, 'title')
title_elem.text = title
category_elem = ET.SubElement(item, 'category')
category_elem.text = category
# Handle namespaced element properly
content_elem = ET.SubElement(item, '{http://purl.org/rss/1.0/modules/content/}encoded')
content_elem.text = content
return item
def generate_rss(self):
"""Generate the complete RSS file"""
# Create backup if file exists
if self.output_file.exists():
backup_file = self.output_file.with_suffix('.bak')
shutil.copy2(self.output_file, backup_file)
print(f"Created backup: {backup_file}")
# Create RSS root
rss = ET.Element('rss', version='2.0')
rss.set('xmlns:content', 'http://purl.org/rss/1.0/modules/content/')
rss.set('xmlns:dc', 'http://purl.org/dc/elements/1.1/')
channel = ET.SubElement(rss, 'channel')
# Channel metadata
title = ET.SubElement(channel, 'title')
title.text = 'Game Server FAQ'
link = ET.SubElement(channel, 'link')
link.text = 'https://gameservers.world/faq'
description = ET.SubElement(channel, 'description')
description.text = 'Comprehensive game server configuration and troubleshooting guide'
language = ET.SubElement(channel, 'dc:language')
language.text = 'en'
pubdate = ET.SubElement(channel, 'pubDate')
# Fix datetime deprecation warning
pubdate.text = datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT')
# Sort games alphabetically by name, then items by section order
sorted_games = sorted(self.games, key=lambda x: x['name'])
# Define section order
section_order = ['Config Files', 'Startup Parameters', 'Troubleshooting', 'Steam Workshop']
# Generate items for each game
all_items = []
for game in sorted_games:
game_name = game['name']
# Required sections in order
sections = [
('Config Files', self.generate_config_files_content(game)),
('Startup Parameters', self.generate_startup_parameters_content(game)),
('Troubleshooting', self.generate_troubleshooting_content(game))
]
# Optional workshop section
workshop_content = self.generate_workshop_content(game)
if workshop_content:
sections.append(('Steam Workshop', workshop_content))
# Create RSS items
for title, content in sections:
item = self.create_rss_item(title, game_name, content)
all_items.append((game_name, title, item))
# Sort all items by category (game name) then by section order
all_items.sort(key=lambda x: (x[0], section_order.index(x[1]) if x[1] in section_order else 999))
# Add all items to channel (no longer needed since we're writing manually)
# for game_name, title, item in all_items:
# channel.append(item)
# Write RSS file manually to avoid namespace issues
with open(self.output_file, 'w', encoding='utf-8') as f:
f.write('\n')
f.write('\n')
f.write(' \n')
f.write(' Game Server FAQ\n')
f.write(' https://gameservers.world/faq\n')
f.write(' Comprehensive game server configuration and troubleshooting guide\n')
f.write(' en\n')
f.write(f' {datetime.now().strftime("%a, %d %b %Y %H:%M:%S GMT")}\n')
# Write all items
for game_name, title, item in all_items:
title_text = item.find('title').text
category_text = item.find('category').text
content_elem = item.find('{http://purl.org/rss/1.0/modules/content/}encoded')
content = content_elem.text if content_elem is not None else ""
f.write(' - \n')
f.write(f' {html.escape(title_text)}\n')
f.write(f' {html.escape(category_text)}\n')
f.write(f' {content}\n')
f.write('
\n')
f.write(' \n')
f.write('\n')
print(f"Generated RSS with {len(all_items)} items for {len(sorted_games)} games")
print(f"Output: {self.output_file}")
def run(self):
"""Main execution method"""
print("FAQ RSS Generator")
print("================")
if not self.load_games():
return False
if not self.games:
print("No valid games found. RSS file will be empty.")
self.generate_rss()
return True
def main():
if len(sys.argv) > 1:
data_dir = sys.argv[1]
else:
data_dir = "data/games"
if len(sys.argv) > 2:
output_file = sys.argv[2]
else:
output_file = "FAQ.RSS"
generator = FAQRSSGenerator(data_dir, output_file)
success = generator.run()
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()