diff --git a/rss-generator.py b/rss-generator.py index 97fb685..09d9824 100644 --- a/rss-generator.py +++ b/rss-generator.py @@ -1,281 +1,198 @@ -#!/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 datetime import datetime from feedgen.feed import FeedGenerator +from feedgen.entry import FeedEntry -# 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 "" +API_BASE_URL = "https://repebblesearch.com/api/v1/apps" +OUTPUT_FOLDER = "feeds" def format_description(text): - """Convert plain text description to formatted HTML with proper line breaks and lists.""" + """Convert release notes text to HTML with paragraph and list support.""" 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 = [] + html = "" + lines = text.split("\n") in_list = False for line in lines: - stripped = line.strip() - - # Check if this is a bullet point - if stripped and stripped[0] == '-': - if not in_list: - formatted_lines.append('') + html += "" 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 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 main description - if description: - description_sections.append('

Description

') - description_sections.append(description) - - # Add screenshots if available (now handles multiple) - if 'screenshots' in app and isinstance(app['screenshots'], list) and len(app['screenshots']) > 0: - description_sections.append('

Screenshots

') - screenshots_html = [] - for idx, screenshot in enumerate(app['screenshots'], 1): - if isinstance(screenshot, dict) and 'url' in screenshot: - url = str(screenshot['url']) - hardware = screenshot.get('hardware', '') - width = screenshot.get('width', 144) - height = screenshot.get('height', 168) - - # Add hardware label if available - if hardware: - screenshots_html.append(f'

Screenshot {idx} ({hardware})

') - else: - screenshots_html.append(f'

Screenshot {idx}

') - - screenshots_html.append(f'Screenshot {idx}') - - description_sections.append('\n'.join(screenshots_html)) - - # Add changelog if available - if 'changelog' in app and isinstance(app['changelog'], list) and len(app['changelog']) > 0: - description_sections.append('

Changelog

') - changelog_html = [] - for entry in app['changelog']: - if isinstance(entry, dict): - version = entry.get('version', 'Unknown') - release_notes = entry.get('release_notes', '') - published_date = entry.get('published_date', '') - - changelog_html.append(f'

Version {version}

') - if published_date: - try: - # Parse and format the date nicely - pub_date = datetime.fromisoformat(published_date.replace('Z', '+00:00')) - formatted_date = pub_date.strftime('%Y-%m-%d %H:%M UTC') - changelog_html.append(f'

Released: {formatted_date}

') - except (ValueError, AttributeError): - changelog_html.append(f'

Released: {published_date}

') - - if release_notes: - formatted_notes = format_description(release_notes) - changelog_html.append(formatted_notes) - else: - changelog_html.append('

No release notes provided.

') - - description_sections.append('\n'.join(changelog_html)) - - # Add latest release notes if available (and changelog isn't already shown) - elif '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) + # Check if line is a list item (starts with -, *, or •) + if line.startswith(("-", "*", "•")): + if not in_list: + html += "" + in_list = False + html += f"

{line}

" + + if in_list: + html += "" + + return html + +def create_description_with_screenshots(app_data): + """Build description with screenshots on a single line, then release notes.""" + description = "" + + # Add screenshots on a single line at the top + screenshots = app_data.get("screenshots", []) + if screenshots: + screenshot_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})" + + screenshot_html += f'{label}' + + screenshot_html += '
' + description += screenshot_html + + # Add release notes below screenshots + changelog = app_data.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" + + description += f"

Version {version}

" + description += f"Released: {formatted_date}
" + description += format_description(release_notes) + else: + # Fallback to latest_release_notes + latest_notes = app_data.get("latest_release_notes", "") + if latest_notes: + description += format_description(latest_notes) + + return description + +def fetch_apps(query, platform="basalt"): + """Fetch apps from the Pebble Search API.""" + params = { + "query": query, + "platform": platform, + "limit": 100 + } - # 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 + response = requests.get(API_BASE_URL, params=params) + response.raise_for_status() + data = response.json() + return data.get("apps", []) + except requests.exceptions.RequestException as e: + print(f"Error fetching apps for query '{query}': {e}") + return [] + +def create_rss_feed(filename, title, description, apps): + """Create an RSS feed from app data.""" + fg = FeedGenerator() + fg.title(title) + fg.link(href="https://repebblesearch.com", rel="alternate") + fg.description(description) + fg.language("en") + + for app in apps: + fe = fg.add_entry() + + app_title = app.get("title", "Unknown App") + app_id = app.get("uuid", "") + app_url = f"https://repebblesearch.com/app/{app_id}" if app_id else "https://repebblesearch.com" + + fe.title(app_title) + fe.link(href=app_url) + fe.guid(app_url) + + # Build description with screenshots and release notes + description_html = create_description_with_screenshots(app) + fe.description(description_html) + + # Add enclosures for screenshots (for readers that support them) + screenshots = app.get("screenshots", []) + for screenshot in screenshots: + url = screenshot.get("url", "") + if url: + fe.enclosure(url=url, type="image/png") + + # Set publication date from latest changelog entry or fallback + pub_date = None + 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: + pub_date = datetime.now() + else: + pub_date = datetime.now() + + if pub_date: + fe.pubDate(pub_date) + + # Ensure output folder exists + os.makedirs(OUTPUT_FOLDER, exist_ok=True) + + # Write RSS feed to file + output_path = os.path.join(OUTPUT_FOLDER, filename) + fg.rss_file(output_path, pretty=True) + print(f"Feed created: {output_path}") 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") + """Main function to generate RSS feeds.""" + print("Fetching Pebble apps and generating RSS feeds...") - print("Generating Pebble apps RSS feeds...\n") + # Fetch new apps + print("Fetching new apps...") + new_apps = fetch_apps("*", platform="basalt") - 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") + # Fetch updated apps (you might want to adjust the query) + print("Fetching updated apps...") + updated_apps = fetch_apps("*", platform="basalt") - print("All feeds generated!") + # Create RSS feeds + create_rss_feed( + "new_apps.xml", + "New Pebble Apps", + "Recently released Pebble apps from the Pebble Search database", + new_apps[:20] # Limit to 20 most recent + ) + + create_rss_feed( + "updated_apps.xml", + "Updated Pebble Apps", + "Recently updated Pebble apps from the Pebble Search database", + updated_apps[:20] # Limit to 20 most recent + ) + + print("Done!") if __name__ == "__main__": main()