#!/usr/bin/env python3 """ Generate RSS feeds from Pebble Search API data. Requires: feedgen (pip install feedgen) """ import requests import json import os from datetime import datetime, timezone from feedgen.feed import FeedGenerator # Define your feeds here FEEDS = [ { 'url': 'https://repebblesearch.com/api/v1/apps?sort=release_date&order=desc&new_only=true&limit=50&platform=basalt', 'name': 'New Apps', 'description': 'Latest new Pebble Time (basalt) apps' }, { 'url': 'https://repebblesearch.com/api/v1/apps?sort=release_date&order=desc&recently_updated=true&limit=50&platform=basalt', 'name': 'Updated Apps', 'description': 'Recently updated Pebble Time (basalt) apps' } ] # Output folder for feeds FEEDS_FOLDER = 'feeds' def fetch_apps_data(url): """Fetch apps data from the API endpoint.""" try: response = requests.get(url) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"Error fetching data: {e}") return None def fetch_app_description(app_id): """Fetch the description for a specific app from its detail endpoint.""" detail_url = f"https://repebblesearch.com/api/v1/apps/{app_id}" try: response = requests.get(detail_url) response.raise_for_status() # Try parsing as JSON first try: data = response.json() return data.get('description', '') except json.JSONDecodeError: # If not JSON, try to extract from response text print(f"Warning: Detail endpoint returned non-JSON for {app_id}") return "" except requests.exceptions.RequestException as e: print(f"Error fetching description for {app_id}: {e}") return "" def format_description(text): """Convert plain text description to formatted HTML with proper line breaks and lists.""" if not text: return "" # Normalize line breaks (handle both \r\n and \n) text = text.replace('\r\n', '\n') lines = text.split('\n') formatted_lines = [] in_list = False for line in lines: stripped = line.strip() # Check if this is a bullet point if stripped and stripped[0] in ('-', '*', '•'): if not in_list: formatted_lines.append('') in_list = False # Add non-empty paragraphs if stripped: formatted_lines.append(f'

{stripped}

') # Close any open list if in_list: formatted_lines.append('') return '\n'.join(formatted_lines) def build_screenshots_html(app): """Build HTML for screenshots on a single line.""" screenshots_html = "" # Check for new-style screenshots array screenshots = app.get("screenshots", []) # Fallback to old-style screenshot_url if no array if not screenshots and 'screenshot_url' in app and app['screenshot_url']: screenshots = [{'url': app['screenshot_url']}] if screenshots: screenshots_html = '
' for idx, screenshot in enumerate(screenshots, 1): url = screenshot.get("url", "") width = screenshot.get("width", "auto") height = screenshot.get("height", "auto") hardware = screenshot.get("hardware", "") label = f"Screenshot {idx}" if hardware: label += f" ({hardware})" if url: screenshots_html += f'{label}' screenshots_html += '
' return screenshots_html def build_changelog_html(app): """Build HTML for changelog entries.""" changelog_html = "" changelog = app.get("changelog", []) if changelog: for entry in changelog: version = entry.get("version", "Unknown") published_date = entry.get("published_date", "") release_notes = entry.get("release_notes", "") # Format the date if published_date: try: date_obj = datetime.fromisoformat(published_date.replace('Z', '+00:00')) formatted_date = date_obj.strftime("%Y-%m-%d %H:%M UTC") except: formatted_date = published_date else: formatted_date = "Unknown" changelog_html += f"

Version {version}

" changelog_html += f"Released: {formatted_date}
" changelog_html += format_description(release_notes) return changelog_html def create_rss_feed(apps_data, feed_name, feed_description, output_file): """Create an RSS feed from apps data using feedgen.""" # Extract the apps list from 'items' key if not isinstance(apps_data, dict) or 'items' not in apps_data: print(f" Unexpected data structure: 'items' key not found") return False apps = apps_data['items'] if not isinstance(apps, list): print(f" Error: 'items' is not a list") return False print(f" Found {len(apps)} apps to process") # Initialize the feed generator fg = FeedGenerator() fg.id(f'https://repebblesearch.com/api/v1/apps') fg.title(feed_name) fg.link(href='https://repebblesearch.com', rel='alternate') fg.description(feed_description) fg.language('en') # Process each app for i, app in enumerate(apps): # Skip entries without required fields if not isinstance(app, dict) or 'title' not in app: continue app_id = app.get('id', app['title']) print(f" Processing app {i+1}/{len(apps)}: {app['title']}") # Fetch description from detail endpoint description = fetch_app_description(app_id) # Format the description with HTML description = format_description(description) # Ensure description is never None if description is None: description = "" # Build description with sections description_sections = [] # Add screenshots on a single line at the top screenshots_html = build_screenshots_html(app) if screenshots_html: description_sections.append(screenshots_html) # Add main description if description: description_sections.append('

Description

') description_sections.append(description) # Add changelog if available changelog_html = build_changelog_html(app) if changelog_html: description_sections.append('

Changelog

') description_sections.append(changelog_html) # Fallback to latest release notes if no changelog if not changelog_html and 'latest_release_notes' in app and app['latest_release_notes']: release_notes = str(app['latest_release_notes']).strip() if release_notes: formatted_notes = format_description(release_notes) description_sections.append('

Latest Release Notes

') description_sections.append(formatted_notes) # Join all sections final_description = '\n'.join(description_sections) # Create a feed entry fe = fg.add_entry() fe.id(str(app_id)) # Ensure ID is a string fe.title(str(app['title'])) # Ensure title is a string fe.link(href=f'https://repebblesearch.com/apps/{app_id}', rel='alternate') # Set description (now includes all sections with headings) if final_description: fe.description(final_description) else: fe.description("No description available") # Add screenshot URLs as enclosures (for readers that support them) screenshots = app.get("screenshots", []) if not screenshots and 'screenshot_url' in app and app['screenshot_url']: screenshots = [{'url': app['screenshot_url']}] for screenshot in screenshots: url = screenshot.get("url", "") if url: fe.enclosure(url=url, type="image/png") # Set publication date if available pub_date = None # Try changelog first changelog = app.get("changelog", []) if changelog and len(changelog) > 0: pub_date_str = changelog[0].get("published_date", "") if pub_date_str: try: pub_date = datetime.fromisoformat(pub_date_str.replace('Z', '+00:00')) except: pass # Fallback to release_date if pub_date is None and 'release_date' in app and app['release_date']: try: pub_date = datetime.fromisoformat(app['release_date'].replace('Z', '+00:00')) except (ValueError, AttributeError): pass # If no valid date, use current time with UTC timezone if pub_date is None: pub_date = datetime.now(timezone.utc) fe.pubDate(pub_date) # Add author if available if 'author' in app and app['author']: fe.author(name=str(app['author'])) # Write the RSS feed to file try: fg.rss_file(output_file, pretty=True) print(f" RSS feed successfully created: {output_file}") return True except Exception as e: print(f" Error writing RSS file: {e}") return False def main(): # Create feeds folder if it doesn't exist if not os.path.exists(FEEDS_FOLDER): os.makedirs(FEEDS_FOLDER) print(f"Created '{FEEDS_FOLDER}' folder\n") print("Generating Pebble apps RSS feeds...\n") for feed_config in FEEDS: feed_name = feed_config['name'] feed_url = feed_config['url'] feed_description = feed_config['description'] output_file = os.path.join(FEEDS_FOLDER, f"{feed_name.lower().replace(' ', '_')}_feed.xml") print(f"Processing: {feed_name}") print(f" Fetching data from API...") apps_data = fetch_apps_data(feed_url) if apps_data: print(f" Creating RSS feed...") if create_rss_feed(apps_data, feed_name, feed_description, output_file): print(f" ✓ Done!\n") else: print(f" ✗ Failed to create RSS feed\n") else: print(f" ✗ Failed to fetch apps data\n") print("All feeds generated!") if __name__ == "__main__": main()