feat: v1.4.0 Phase 4 - Enhanced Feed Media

Implement Phase 4 of v1.4.0 Media release - Enhanced Feed Media support.

RSS Feed Enhancements (starpunk/feeds/rss.py):
- Wrap size variants in <media:group> elements
- Add <media:content> for large/medium/small variants with attributes:
  url, type, medium, isDefault, width, height, fileSize
- Add <media:thumbnail> for thumb variant with dimensions
- Add <media:title type="plain"> for image captions
- Implement isDefault logic: largest available variant (large→medium→small fallback)
- Maintain backwards compatibility for media without variants (legacy fallback)

JSON Feed Enhancements (starpunk/feeds/json_feed.py):
- Add _starpunk.about URL (configurable via STARPUNK_ABOUT_URL config)
- Add _starpunk.media_variants array with variant data when variants exist
- Each variant entry includes: url, width, height, size_in_bytes, mime_type

ATOM Feed Enhancements (starpunk/feeds/atom.py):
- Add title attribute to enclosure links for captions
- Keep simple (no variants in ATOM per design decision)

Test Updates (tests/test_feeds_rss.py):
- Update streaming media test to search descendants for media:content
- Now inside media:group for images with variants (v1.4.0 behavior)

Per design document: /docs/design/v1.4.0/media-implementation-design.md
Following ADR-059: Full Feed Media Standardization

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-16 07:47:56 -07:00
parent c64feaea23
commit 83dc488457
4 changed files with 102 additions and 16 deletions

View File

@@ -304,18 +304,62 @@ def generate_rss_streaming(
item_xml += f"""
<category>{_escape_xml(tag['display_name'])}</category>"""
# Add media:content elements (all images)
# Enhanced media handling with variants (v1.4.0 Phase 4)
if hasattr(note, 'media') and note.media:
for media_item in note.media:
media_url = f"{site_url}/media/{media_item['path']}"
item_xml += f"""
<media:content url="{_escape_xml(media_url)}" type="{media_item.get('mime_type', 'image/jpeg')}" medium="image" fileSize="{media_item.get('size', 0)}"/>"""
variants = media_item.get('variants', {})
# Add media:thumbnail (first image only)
first_media = note.media[0]
media_url = f"{site_url}/media/{first_media['path']}"
item_xml += f"""
<media:thumbnail url="{_escape_xml(media_url)}"/>"""
# Use media:group for multiple sizes of same image
if variants:
item_xml += '\n <media:group>'
# Determine which variant is the default (largest available)
# Fallback order: large -> medium -> small
default_variant = None
for fallback in ['large', 'medium', 'small']:
if fallback in variants:
default_variant = fallback
break
# Add each variant as media:content
for variant_type in ['large', 'medium', 'small']:
if variant_type in variants:
v = variants[variant_type]
media_url = f"{site_url}/media/{v['path']}"
is_default = 'true' if variant_type == default_variant else 'false'
item_xml += f'''
<media:content url="{_escape_xml(media_url)}"
type="{media_item.get('mime_type', 'image/jpeg')}"
medium="image"
isDefault="{is_default}"
width="{v['width']}"
height="{v['height']}"
fileSize="{v['size_bytes']}"/>'''
item_xml += '\n </media:group>'
# Add media:thumbnail
if 'thumb' in variants:
thumb = variants['thumb']
thumb_url = f"{site_url}/media/{thumb['path']}"
item_xml += f'''
<media:thumbnail url="{_escape_xml(thumb_url)}"
width="{thumb['width']}"
height="{thumb['height']}"/>'''
# Add media:title for caption
if media_item.get('caption'):
item_xml += f'''
<media:title type="plain">{_escape_xml(media_item['caption'])}</media:title>'''
else:
# Fallback for media without variants (legacy)
media_url = f"{site_url}/media/{media_item['path']}"
item_xml += f'''
<media:content url="{_escape_xml(media_url)}"
type="{media_item.get('mime_type', 'image/jpeg')}"
medium="image"
fileSize="{media_item.get('size', 0)}"/>'''
# Close item
item_xml += """