Add screenshots before the description
This commit is contained in:
+171
-254
@@ -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('<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
|
||||
line = line.strip()
|
||||
if not line:
|
||||
if in_list:
|
||||
formatted_lines.append('</ul>')
|
||||
html += "</ul>"
|
||||
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
|
||||
|
||||
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('<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:
|
||||
screenshots_html.append(f'<p><strong>Screenshot {idx} ({hardware})</strong></p>')
|
||||
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;" />')
|
||||
|
||||
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('<h3>Changelog</h3>')
|
||||
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'<h4>Version {version}</h4>')
|
||||
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'<p><em>Released: {formatted_date}</em></p>')
|
||||
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:
|
||||
changelog_html.append('<p>No release notes provided.</p>')
|
||||
|
||||
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('<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)
|
||||
# Check if line is a list item (starts with -, *, or •)
|
||||
if line.startswith(("-", "*", "•")):
|
||||
if not in_list:
|
||||
html += "<ul>"
|
||||
in_list = True
|
||||
item_text = line[1:].strip()
|
||||
html += f"<li>{item_text}</li>"
|
||||
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 += "</ul>"
|
||||
in_list = False
|
||||
html += f"<p>{line}</p>"
|
||||
|
||||
if in_list:
|
||||
html += "</ul>"
|
||||
|
||||
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 = '<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", "")
|
||||
|
||||
label = f"Screenshot {idx}"
|
||||
if hardware:
|
||||
label += f" ({hardware})"
|
||||
|
||||
screenshot_html += f'<img src="{url}" alt="{label}" title="{label}" width="{width}" height="{height}" style="max-height: 300px; flex-shrink: 0;" />'
|
||||
|
||||
screenshot_html += '</div>'
|
||||
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"<h4>Version {version}</h4>"
|
||||
description += f"<em>Released: {formatted_date}</em><br/>"
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user