Add screenshots before the description

This commit is contained in:
2026-09-11 16:46:30 +02:00
committed by djib
parent 986d1e9cb8
commit e8783faef9
+150 -233
View File
@@ -1,281 +1,198 @@
#!/usr/bin/env python3
"""
Generate RSS feeds from Pebble Search API data.
Requires: feedgen (pip install feedgen)
"""
import requests import requests
import json
import os import os
from datetime import datetime, timezone from datetime import datetime
from feedgen.feed import FeedGenerator from feedgen.feed import FeedGenerator
from feedgen.entry import FeedEntry
# Define your feeds here API_BASE_URL = "https://repebblesearch.com/api/v1/apps"
FEEDS = [ OUTPUT_FOLDER = "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): 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: if not text:
return "" return ""
# Normalize line breaks (handle both \r\n and \n) html = ""
text = text.replace('\r\n', '\n') lines = text.split("\n")
lines = text.split('\n')
formatted_lines = []
in_list = False in_list = False
for line in lines: for line in lines:
stripped = line.strip() line = line.strip()
if not line:
# Check if this is a bullet point
if stripped and stripped[0] == '-':
if not in_list:
formatted_lines.append('<ul>')
in_list = True
# Remove the leading dash and clean up
item_text = stripped[1:].strip()
formatted_lines.append(f'<li>{item_text}</li>')
else:
# Close list if we were in one
if in_list: if in_list:
formatted_lines.append('</ul>') html += "</ul>"
in_list = False in_list = False
# Add non-empty paragraphs
if stripped:
formatted_lines.append(f'<p>{stripped}</p>')
# Close any open list
if in_list:
formatted_lines.append('</ul>')
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 continue
app_id = app.get('id', app['title']) # Check if line is a list item (starts with -, *, or •)
print(f" Processing app {i+1}/{len(apps)}: {app['title']}") if line.startswith(("-", "*", "•")):
if not in_list:
html += "<ul>"
in_list = True
item_text = line[1:].strip()
html += f"<li>{item_text}</li>"
else:
if in_list:
html += "</ul>"
in_list = False
html += f"<p>{line}</p>"
# Fetch description from detail endpoint if in_list:
description = fetch_app_description(app_id) html += "</ul>"
# Format the description with HTML
description = format_description(description) return html
# Ensure description is never None
if description is None: def create_description_with_screenshots(app_data):
"""Build description with screenshots on a single line, then release notes."""
description = "" description = ""
# Build description with sections # Add screenshots on a single line at the top
description_sections = [] screenshots = app_data.get("screenshots", [])
if screenshots:
screenshot_html = '<div style="display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: nowrap; overflow-x: auto;">'
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", "")
# Add main description label = f"Screenshot {idx}"
if description:
description_sections.append('<h3>Description</h3>')
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('<h3>Screenshots</h3>')
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: if hardware:
screenshots_html.append(f'<p><strong>Screenshot {idx} ({hardware})</strong></p>') label += f" ({hardware})"
else:
screenshots_html.append(f'<p><strong>Screenshot {idx}</strong></p>')
screenshots_html.append(f'<img src="{url}" alt="Screenshot {idx}" width="{width}" height="{height}" style="max-width: 100%; height: auto;" />') screenshot_html += f'<img src="{url}" alt="{label}" title="{label}" width="{width}" height="{height}" style="max-height: 300px; flex-shrink: 0;" />'
description_sections.append('\n'.join(screenshots_html)) screenshot_html += '</div>'
description += screenshot_html
# Add changelog if available # Add release notes below screenshots
if 'changelog' in app and isinstance(app['changelog'], list) and len(app['changelog']) > 0: changelog = app_data.get("changelog", [])
description_sections.append('<h3>Changelog</h3>') if changelog:
changelog_html = [] for entry in changelog:
for entry in app['changelog']: version = entry.get("version", "Unknown")
if isinstance(entry, dict): published_date = entry.get("published_date", "")
version = entry.get('version', 'Unknown') release_notes = entry.get("release_notes", "")
release_notes = entry.get('release_notes', '')
published_date = entry.get('published_date', '')
changelog_html.append(f'<h4>Version {version}</h4>') # Format the date
if published_date: if published_date:
try: try:
# Parse and format the date nicely date_obj = datetime.fromisoformat(published_date.replace('Z', '+00:00'))
pub_date = datetime.fromisoformat(published_date.replace('Z', '+00:00')) formatted_date = date_obj.strftime("%Y-%m-%d %H:%M UTC")
formatted_date = pub_date.strftime('%Y-%m-%d %H:%M UTC') except:
changelog_html.append(f'<p><em>Released: {formatted_date}</em></p>') formatted_date = published_date
except (ValueError, AttributeError):
changelog_html.append(f'<p><em>Released: {published_date}</em></p>')
if release_notes:
formatted_notes = format_description(release_notes)
changelog_html.append(formatted_notes)
else: else:
changelog_html.append('<p>No release notes provided.</p>') formatted_date = "Unknown"
description_sections.append('\n'.join(changelog_html)) description += f"<h4>Version {version}</h4>"
description += f"<em>Released: {formatted_date}</em><br/>"
# Add latest release notes if available (and changelog isn't already shown) description += format_description(release_notes)
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('<h3>Latest Release Notes</h3>')
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: else:
fe.description("No description available") # Fallback to latest_release_notes
latest_notes = app_data.get("latest_release_notes", "")
if latest_notes:
description += format_description(latest_notes)
# Add all screenshot URLs as enclosures return description
if 'screenshots' in app and isinstance(app['screenshots'], list):
for screenshot in app['screenshots']: def fetch_apps(query, platform="basalt"):
if isinstance(screenshot, dict) and 'url' in screenshot: """Fetch apps from the Pebble Search API."""
fe.link(href=str(screenshot['url']), rel='enclosure', type='image/png') params = {
"query": query,
"platform": platform,
"limit": 100
}
# Set publication date if available
pub_date = None
if 'release_date' in app and app['release_date']:
try: try:
pub_date = datetime.fromisoformat(app['release_date'].replace('Z', '+00:00')) response = requests.get(API_BASE_URL, params=params)
except (ValueError, AttributeError): response.raise_for_status()
pass data = response.json()
return data.get("apps", [])
except requests.exceptions.RequestException as e:
print(f"Error fetching apps for query '{query}': {e}")
return []
# If no valid date, use current time with UTC timezone def create_rss_feed(filename, title, description, apps):
if pub_date is None: """Create an RSS feed from app data."""
pub_date = datetime.now(timezone.utc) 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) fe.pubDate(pub_date)
# Add author if available # Ensure output folder exists
if 'author' in app and app['author']: os.makedirs(OUTPUT_FOLDER, exist_ok=True)
fe.author(name=str(app['author']))
# Write the RSS feed to file # Write RSS feed to file
try: output_path = os.path.join(OUTPUT_FOLDER, filename)
fg.rss_file(output_file, pretty=True) fg.rss_file(output_path, pretty=True)
print(f" RSS feed successfully created: {output_file}") print(f"Feed created: {output_path}")
return True
except Exception as e:
print(f" Error writing RSS file: {e}")
return False
def main(): def main():
# Create feeds folder if it doesn't exist """Main function to generate RSS feeds."""
if not os.path.exists(FEEDS_FOLDER): print("Fetching Pebble apps and generating RSS feeds...")
os.makedirs(FEEDS_FOLDER)
print(f"Created '{FEEDS_FOLDER}' folder\n")
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: # Fetch updated apps (you might want to adjust the query)
feed_name = feed_config['name'] print("Fetching updated apps...")
feed_url = feed_config['url'] updated_apps = fetch_apps("*", platform="basalt")
feed_description = feed_config['description']
output_file = os.path.join(FEEDS_FOLDER, f"{feed_name.lower().replace(' ', '_')}_feed.xml")
print(f"Processing: {feed_name}") # Create RSS feeds
print(f" Fetching data from API...") create_rss_feed(
apps_data = fetch_apps_data(feed_url) "new_apps.xml",
"New Pebble Apps",
"Recently released Pebble apps from the Pebble Search database",
new_apps[:20] # Limit to 20 most recent
)
if apps_data: create_rss_feed(
print(f" Creating RSS feed...") "updated_apps.xml",
if create_rss_feed(apps_data, feed_name, feed_description, output_file): "Updated Pebble Apps",
print(f" ✓ Done!\n") "Recently updated Pebble apps from the Pebble Search database",
else: updated_apps[:20] # Limit to 20 most recent
print(f" ✗ Failed to create RSS feed\n") )
else:
print(f" ✗ Failed to fetch apps data\n")
print("All feeds generated!") print("Done!")
if __name__ == "__main__": if __name__ == "__main__":
main() main()