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('
')
- in_list = True
- # Remove the leading dash and clean up
- item_text = stripped[1:].strip()
- formatted_lines.append(f'- {item_text}
')
- else:
- # Close list if we were in one
+ line = line.strip()
+ if not line:
if 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'
')
-
- 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 = True
+ item_text = line[1:].strip()
+ html += f"- {item_text}
"
else:
- fe.description("No description available")
-
- # Add all screenshot URLs as enclosures
- if 'screenshots' in app and isinstance(app['screenshots'], list):
- for screenshot in app['screenshots']:
- if isinstance(screenshot, dict) and 'url' in screenshot:
- fe.link(href=str(screenshot['url']), rel='enclosure', type='image/png')
-
- # Set publication date if available
- pub_date = None
- if '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']))
+ if 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'

'
+
+ 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()