#!/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()