Skip to content

About

RSS feed for personal favourites and bookmarks

Resources

Stars

4 stars

Watchers

1 watching

Forks

Repository files navigation

Star Collector

A tool to generate an RSS feed combining your starred/favorite content from Mastodon favorites and/or bookmarks, LinkedIn reactions and any personalized RSS feed you may have (for example Feedbin or Linkding or even GitHub). A GitHub Action rebuilds and publishes the feed every hour.

Features

  • Combines multiple star sources into a single RSS feed
  • Configurable number of items per feed
  • Support for Mastodon media attachments in feed items
  • Hashtags of Mastodon posts become feed categories (tags)
  • Environment variable support for sensitive tokens
  • Detailed logging options
  • Short headlines for Mastodon and LinkedIn posts, generated via OpenRouter and cached
  • LinkedIn reactions (except plain likes) as a source, see below

Installation

  1. Clone this repository
  2. Install dependencies with uv sync
  3. Copy .env.sample to .env and configure your tokens (including OPENROUTER_API_KEY for headlines)
  4. Create a sc_config.yaml (see below) and configure your settings

Configuration

Create a sc_config.yaml file with your settings:

mastodon:
  access_token: "your-token-here"  # Can also be set via MASTODON_ACCESS_TOKEN env var
  mastodon_instance: "https://your.instance"
  mastodon_username: "yourusername"
  import_tags: true  # use the hashtags of a post as feed categories (default)

# Optional RSS configuration
rss:
  urls:
    - url: https://feedbin.com/starred/${FEEDBIN_ID}.xml
      tag: feedbin
    - url: https://bookmarks.ping13.net/feeds/${LINKDING_ID}/all
      tag: linkding

# Optional: headline generation, see "Headline generation"
titles:
  model: anthropic/claude-haiku-5.5

# Optional: LinkedIn reactions, see "LinkedIn reactions"
linkedin:
  enabled: true
  exclude_reactions: [LIKE]

Usage

# Basic usage (outputs to stdout)
uv run rss.py

# With options
uv run rss.py --config sc_config.yaml --output feed.xml --limit 10 --debug

Options:
  -c, --config TEXT                 Path to configuration file (default: sc_config.yaml)
  --debug / --no-debug              Enable debug output
  -o, --output TEXT                 Output file (optional, defaults to stdout)
  -l, --limit INTEGER               Number of feed items to include (default: 5)
  -L, --log-level [DEBUG|INFO|WARNING|ERROR|CRITICAL]
                                    Set logging level (default: ERROR)
  --help                            Show this message and exit

Ideas

  • Spotify
  • GitHub stars
  • ...

Limitations

  • Currently, there is no way to use the creation date of a like in Mastodon, see this issue.

LinkedIn reactions (optional)

Reactions on LinkedIn posts (except plain likes) and your reposts can be added as a star source through LinkedIn's Member Data Portability API (EEA/Switzerland members only):

  1. Create a LinkedIn developer app with the Member Data Portability API (Member) product and add http://localhost:8799/callback as redirect URL.
  2. Put LINKEDIN_CLIENT_ID and LINKEDIN_CLIENT_SECRET into .env, then run make linkedin-token (once a year). Use make linkedin-token-ci to also update the GitHub secret LINKEDIN_ACCESS_TOKEN and the variable LINKEDIN_TOKEN_EXPIRES.
  3. Enable the linkedin: block in sc_config.yaml. exclude_reactions takes reaction types (LIKE, PRAISE, EMPATHY, ...) and REPOST.

The API only returns the last 28 days, so reactions are accumulated in linkedin-state.json, which is published next to the feed and re-read on the next run. The state also remembers the last processed event, so a normal run asks LinkedIn only for new events (one request, which keeps well below the daily API quota); once a week, or when the state is missing, the whole 28-day window is read again. Post text is read from the public post page and turned into a headline. If the token expires, the feed still builds from the stored items and a warning is logged.

Token renewal. The token is valid for about a year. When fewer than 30 days remain, the feed job opens a GitHub issue ("LinkedIn token expires soon") and you get the usual GitHub notification. Renew with make linkedin-token-ci and close the issue. The expiry date is kept in the repository variable LINKEDIN_TOKEN_EXPIRES.

Headline generation

Mastodon posts have no title, so one is generated with an LLM through OpenRouter (needs OPENROUTER_API_KEY; without it, the start of the post is used). Configure it in sc_config.yaml:

titles:
  model: anthropic/claude-haiku-5.5      # default
  fallback_model: qwen/qwen3.7-flash     # used if the primary fails
  max_new_titles_per_run: 100            # cost guard

Titles are cached in title-generator.cache (keyed by model, prompt and text), so only new posts cost money: typically well under USD 0.10 per month. The GitHub Action persists the cache between runs. Compare models with make title-bakeoff.

Blog favorites page

The feed is rebuilt every hour. When its items changed, the job starts the favorites.yaml workflow in ping13/blog, which rebuilds and uploads only the blog's favorites page. This needs the secret BLOG_DISPATCH_TOKEN: a fine-grained token for ping13/blog with Actions: Read and write.

Token renewal. When fewer than 30 days remain until the date in the repository variable BLOG_DISPATCH_TOKEN_EXPIRES, the feed job opens a GitHub issue ("Blog dispatch token expires soon") with the renewal steps.

Bookmark summaries (optional)

linkding fills a bookmark's description from the page's meta description when it is saved. Some sites answer with a consent page, a login wall or an app shell instead (for example m.youtube.com links get "Auf YouTube findest du ..."), so the feed shows no description or a generic one. bookmark_summaries.py replaces those descriptions with a 2–3 sentence summary: Kagi's Extract API fetches the page as Markdown (on Kagi's servers, so CI is not served bot-check pages) and the OpenRouter model of the titles: block writes the summary. For a Hacker News story the linked article is summarized. Error pages, walls and junk text are recognized by the model and skipped.

A description counts as generic if it is empty, the text of an error page ("404 Fehler"), shared by bookmarks with different URLs, or the same as the site's homepage description (unless the bookmark is that homepage). Anything else, short notes included, is treated as written by hand and left alone. Bookmarks with a tag from rss.exclude_categories are never sent anywhere.

  1. Put LINKDING_API_TOKEN (linkding: Settings → Integrations) and KAGI_API_TOKEN (a v1 key from the Kagi API dashboard, with prepaid credit) into .env, next to OPENROUTER_API_KEY.
  2. make bookmarks lists the candidates of the last 7 days and why they were picked; nothing paid is called. make bookmarks ARGS="--days 300" looks further back.
  3. make bookmarks-preview adds the summaries they would get, without writing them. make bookmarks-apply writes them.

The feed workflow upload.yaml runs every hour and starts with --apply, with the three keys as GitHub secrets; the feed is built afterwards, also when summarizing failed. Hourly runs only look at bookmarks of the last 3 hours, so a page that cannot be summarized is not retried every hour; the run at 02:04 UTC covers the last week. bookmarks.yaml runs the same step on its own, for manual runs. Settings are in the bookmarks: block of sc_config.yaml. A summary costs about one cent ($0.004 for Kagi Extract plus the model); max_summaries_per_run caps a run.

Development

make help   # all targets, grouped by purpose
make test   # run tests (no network)
make lint   # ruff

Architecture diagrams live in docs/diagrams/.

About

RSS feed for personal favourites and bookmarks

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages