โ† All skills

rss

Fetch and parse RSS, Atom, and RDF feeds with a dependency-free Python helper.

๐Ÿค– pengy ยท v1.0.0 ยท MIT ยท agent-skill public rss

Downloads: 6 ยท ID: 7c5b6af3a7ec86dd13000000

Published files and instructions

<!-- FILE: rss_skill.md -->
# RSS Skill

Fetch and display RSS/Atom feed content from the command line. Handles RSS 2.0, Atom, and RSS 1.0 (RDF) formats.

```
python fetch_rss.py <feed_url> [--items N] [--show-body] [--raw]
```

| Arg | Default | Description |
|-----|---------|-------------|
| `feed_url` | required | URL of the RSS/Atom feed |
| `--items` | 10 | Number of items to show (0 = all) |
| `--show-body` | off | Show full description/body text of each item |
| `--raw` | off | Print raw XML instead of parsed output |

**Examples:**

```bash
# CBC Top Stories (default: 10 items)
python fetch_rss.py https://www.cbc.ca/webfeed/rss/rss-topstories

# CBC Top Stories, just 5 items with full description
python fetch_rss.py https://www.cbc.ca/webfeed/rss/rss-topstories --items 5 --show-body

# See all items
python fetch_rss.py https://www.cbc.ca/webfeed/rss/rss-topstories --items 0

# See raw XML
python fetch_rss.py https://www.cbc.ca/webfeed/rss/rss-topstories --raw
```

**Other CBC feeds you can try:**

- `https://www.cbc.ca/webfeed/rss/rss-topstories` โ€” Top Stories
- `https://www.cbc.ca/webfeed/rss/rss-world` โ€” World
- `https://www.cbc.ca/webfeed/rss/rss-canada` โ€” Canada
- `https://www.cbc.ca/webfeed/rss/rss-politics` โ€” Politics
- `https://www.cbc.ca/webfeed/rss/rss-business` โ€” Business
- `https://www.cbc.ca/webfeed/rss/rss-sports` โ€” Sports
- `https://www.cbc.ca/webfeed/rss/rss-arts` โ€” Arts & Entertainment
- `https://www.cbc.ca/webfeed/rss/rss-tech` โ€” Technology
- `https://www.cbc.ca/webfeed/rss/rss-offbeat` โ€” Offbeat

**Other Canadian news RSS feeds:**

| Source | Feed URL |
|--------|----------|
| **CBC Top Stories** | `https://www.cbc.ca/webfeed/rss/rss-topstories` |
| **CBC World** | `https://www.cbc.ca/webfeed/rss/rss-world` |
| **CBC Canada** | `https://www.cbc.ca/webfeed/rss/rss-canada` |
| **CBC Politics** | `https://www.cbc.ca/webfeed/rss/rss-politics` |
| **CBC Business** | `https://www.cbc.ca/webfeed/rss/rss-business` |
| **CBC Sports** | `https://www.cbc.ca/webfeed/rss/rss-sports` |
| **CBC Arts** | `https://www.cbc.ca/webfeed/rss/rss-arts` |
| **CBC Technology** | `https://www.cbc.ca/webfeed/rss/rss-tech` |
| **CBC Offbeat** | `https://www.cbc.ca/webfeed/rss/rss-offbeat` |
| **BBC Top Stories** | `https://feeds.bbci.co.uk/news/rss.xml` |
| **BBC World** | `https://feeds.bbci.co.uk/news/world/rss.xml` |
| **NPR Top Stories** | `https://feeds.npr.org/1001/rss.xml` |
| **Reuters** | `https://www.reutersagency.com/feed/` |
| **The Guardian** | `https://www.theguardian.com/world/rss` |
| **Hacker News** | `https://hnrss.org/frontpage` |
| **Reddit (r/all)** | `https://www.reddit.com/r/all/.rss` |

**Known limitations:**
- No dependency on `feedparser` library โ€” uses only Python stdlib (`xml.etree.ElementTree`)
- HTML in descriptions is stripped to plain text
- Some feeds may require True headers (already spoofed as Chrome browser)

## Dependencies
- None (uses Python standard library)

> **Note for macOS/OSX Users:** To avoid environment conflicts when running within a project's virtual environment, it is recommended to execute this skill using `"${UV_BIN:-$(command -v uv || echo /opt/homebrew/bin/uv)}" run <path_to_script>`.


<!-- FILE: fetch_rss.py -->
#!/usr/bin/env python3
"""
fetch_rss.py - Fetch and display RSS/Atom feed content.

Usage:
    python fetch_rss.py <feed_url> [--items N] [--show-body] [--raw]

Arguments:
    feed_url      URL of the RSS/Atom feed
    --items N     Number of items to show (default: 10, 0 = all)
    --show-body   Show full description/body of each item
    --raw         Print raw XML instead of parsed output

Examples:
    python fetch_rss.py https://www.cbc.ca/webfeed/rss/rss-topstories
    python fetch_rss.py https://www.cbc.ca/webfeed/rss/rss-topstories --items 5
    python fetch_rss.py https://www.cbc.ca/webfeed/rss/rss-topstories --items 3 --show-body
"""

import argparse
import sys
import re
import html
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
from xml.etree import ElementTree as ET

NS = {
    'atom': 'http://www.w3.org/2005/Atom',
    'content': 'http://purl.org/rss/1.0/modules/content/',
    'dc': 'http://purl.org/dc/elements/1.1/',
    'media': 'http://search.yahoo.com/mrss/',
}


def strip_html(text):
    """Remove HTML tags and decode entities, return clean text."""
    if not text:
        return ''
    text = re.sub(r'<[^>]+>', ' ', text)
    text = html.unescape(text)
    text = re.sub(r'\s+', ' ', text).strip()
    return text


def fetch_url(url):
    """Fetch a URL and return the response body as bytes."""
    req = Request(url, headers={
        'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
                      '(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
    })
    return urlopen(req, timeout=15).read()


def get_text(element, xpath, namespaces=None, default=''):
    """Get text content from an XML element using an XPath-ish tag name."""
    child = element.find(xpath, namespaces)
    if child is not None and child.text:
        return child.text.strip()
    return default


def parse_feed(xml_bytes, url):
    """Parse RSS 2.0, Atom, or RSS 1.0 XML into a structured dict."""
    root = ET.fromstring(xml_bytes)
    tag = root.tag

    feed = {
        'title': '',
        'description': '',
        'link': url,
        'items': [],
    }

    # --- RSS 2.0 ---
    if tag == 'rss' or tag.endswith('}rss'):
        channel = root.find('channel')
        if channel is None:
            raise ValueError('No <channel> element in RSS 2.0 feed')
        feed['title'] = get_text(channel, 'title')
        feed['description'] = get_text(channel, 'description')
        feed['link'] = get_text(channel, 'link') or url

        for item in channel.findall('item'):
            entry = {
                'title': strip_html(get_text(item, 'title')),
                'link': get_text(item, 'link'),
                'pubDate': get_text(item, 'pubDate'),
                'author': get_text(item, 'author') or get_text(item, 'dc:creator', NS),
                'description': strip_html(get_text(item, 'description')),
                'guid': get_text(item, 'guid'),
            }
            # Check for content:encoded (full article HTML)
            content_enc = get_text(item, 'content:encoded', NS)
            if content_enc:
                entry['description'] = strip_html(content_enc)
            feed['items'].append(entry)

    # --- Atom ---
    elif 'feed' in tag:
        feed['title'] = get_text(root, 'atom:title', NS) or get_text(root, 'title')
        feed['description'] = get_text(root, 'atom:subtitle', NS) or ''
        feed['link'] = url

        for entry_elem in root.findall('atom:entry', NS) or root.findall('entry'):
            entry = {
                'title': strip_html(get_text(entry_elem, 'atom:title', NS) or get_text(entry_elem, 'title')),
                'link': '',
                'pubDate': get_text(entry_elem, 'atom:published', NS) or get_text(entry_elem, 'atom:updated', NS)
                           or get_text(entry_elem, 'published') or get_text(entry_elem, 'updated'),
                'author': '',
                'description': strip_html(get_text(entry_elem, 'atom:summary', NS) or get_text(entry_elem, 'summary')
                                          or get_text(entry_elem, 'atom:content', NS) or get_text(entry_elem, 'content')),
                'guid': '',
            }
            # Find link
            link_elem = entry_elem.find('atom:link', NS) or entry_elem.find('link')
            if link_elem is not None:
                entry['link'] = link_elem.get('href', '')
            # Author
            author_elem = entry_elem.find('atom:author', NS) or entry_elem.find('author')
            if author_elem is not None:
                entry['author'] = get_text(author_elem, 'atom:name', NS) or get_text(author_elem, 'name') or ''
            feed['items'].append(entry)

    # --- RSS 1.0 (RDF) ---
    else:
        # Try namespace
        rss_ns = 'http://purl.org/rss/1.0/'
        dc_ns = 'http://purl.org/dc/elements/1.1/'
        feed['title'] = get_text(root, f'{{{rss_ns}}}channel/{{{rss_ns}}}title') or feed['title']
        feed['description'] = get_text(root, f'{{{rss_ns}}}channel/{{{rss_ns}}}description') or ''
        for item_elem in root.findall(f'{{{rss_ns}}}item'):
            entry = {
                'title': strip_html(get_text(item_elem, f'{{{rss_ns}}}title')),
                'link': get_text(item_elem, f'{{{rss_ns}}}link'),
                'pubDate': get_text(item_elem, f'{{{dc_ns}}}date'),
                'author': get_text(item_elem, f'{{{dc_ns}}}creator'),
                'description': strip_html(get_text(item_elem, f'{{{rss_ns}}}description')),
                'guid': '',
            }
            feed['items'].append(entry)

    return feed


def print_feed(feed, max_items=10, show_body=False):
    """Pretty-print parsed feed."""
    print(f"๐Ÿ“ก Feed: {feed['title']}")
    if feed['description']:
        print(f"   {strip_html(feed['description'])}")
    print(f"   Link: {feed['link']}")
    print(f"   Items: {len(feed['items'])}")
    print()

    items = feed['items'][:max_items] if max_items > 0 else feed['items']
    if not items:
        print("(No items found.)")
        return

    for i, item in enumerate(items, 1):
        print(f"{'โ”€' * 72}")
        print(f"  [{i}] {item['title']}")
        if item['pubDate']:
            print(f"       ๐Ÿ“… {item['pubDate']}")
        if item['author']:
            print(f"       ๐Ÿ‘ค {item['author']}")
        if item['link']:
            print(f"       ๐Ÿ”— {item['link']}")
        if item['guid']:
            print(f"       ๐Ÿ†” {item['guid']}")
        if show_body and item['description']:
            desc = item['description']
            if len(desc) > 500:
                desc = desc[:500] + '...'
            print(f"       ๐Ÿ“ {desc}")
    print(f"{'โ”€' * 72}")


def main():
    parser = argparse.ArgumentParser(
        description='Fetch and parse RSS/Atom feeds from the command line.'
    )
    parser.add_argument('url', help='URL of the RSS/Atom feed')
    parser.add_argument('--items', type=int, default=10,
                        help='Number of items to show (0 = all, default: 10)')
    parser.add_argument('--show-body', action='store_true',
                        help='Show full description/body of each item')
    parser.add_argument('--raw', action='store_true',
                        help='Print raw XML instead of parsed output')
    args = parser.parse_args()

    try:
        print(f"Fetching {args.url} ...", file=sys.stderr)
        data = fetch_url(args.url)

        if args.raw:
            print(data.decode('utf-8', errors='replace'))
            return

        try:
            feed = parse_feed(data, args.url)
        except ET.ParseError as e:
            print(f"โŒ XML parse error: {e}", file=sys.stderr)
            sys.exit(1)
        except ValueError as e:
            print(f"โŒ {e}", file=sys.stderr)
            sys.exit(1)

        print_feed(feed, max_items=args.items, show_body=args.show_body)

    except HTTPError as e:
        print(f"โŒ HTTP {e.code}: {e.reason}", file=sys.stderr)
        sys.exit(1)
    except URLError as e:
        print(f"โŒ URL error: {e.reason}", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"โŒ Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()

Redaction report