Central, platform-independent archive of Munich Maker Lab's social media posts
(Mastodon, Tumblr, Instagram, Twitter/X, Bluesky, ...). Goal: preserve the
history independently of the external platforms and serve it as
posts.json/RSS.
| Mastodon | Bluesky | Tumblr | Twitter/X | ||
|---|---|---|---|---|---|
| Status | ✅ | ✅ | ❌ | ⛔ | ⛔ |
| Last successful sync (UTC) | 2026-09-13 08:19:21 UTC | 2026-09-13 08:19:21 UTC | 2026-09-12 07:55:36 UTC | never | never |
| New posts (last run) | +0 | +0 | +0 | – | – |
| Known posts | 21 | 21 | 20 | – | – |
A source erroring or going quiet never deletes previously-archived posts -- importers only ever write raw files, never remove them -- and the "Last successful sync" timestamp above keeps showing the last time a source actually worked even while it's currently failing.
First end-to-end pipeline for the three sources that work without login (Mastodon, Bluesky, Tumblr):
external platform -> archive/importers/*.py -> raw/<platform>/*.json
|
archive/normalize.py
|
archive/deduplicate.py
v
data/posts.json (canonical, deduped)
|
archive/build.py -> public/{posts,latest}.json, feed.xml
uv run python -m archive sync # fetch raw posts from each source into raw/
uv run python -m archive build # normalize raw/ -> data/posts.json + public/*Instagram and Twitter/X don't have an importer yet: the PoC showed that no
real posts are reachable without login/session on either platform right now
(see FINDINGS.md).
The sync.yml GitHub Actions workflow has been triggered manually against the
real repo and confirmed working (fetch, normalize, build, auto-commit).
Known gaps at this stage:
- Cross-platform deduplication (
archive/deduplicate.py) merges posts that were cross-posted within a 6-hour window and score >=80 on normalized text similarity (rapidfuzztoken-set ratio), preferring Mastodon as the canonical source, then Bluesky, then Tumblr. A post reworded very differently per platform, or one with no matching text at all (e.g. an image-only crosspost), won't be caught -- that would need image-hash or semantic matching, not implemented yet. data/state.jsonnow tracks per-source sync health (see "Source status" above), but there's still nolast_seen_id-based incremental fetch:syncre-fetches the latest 20 posts per platform every time.- Images are mirrored into
public/media/<post-id>/;media.urlpoints at the repo-hosted copy andmedia.original_urlkeeps the platform's CDN URL. Video is not mirrored yet (Bluesky serves it as an HLS playlist, Mastodon as a direct MP4 -- both still just link out to the original URL).
archive/
├── importers/{mastodon,bluesky,tumblr}.py # fetch -> raw/<platform>/*.json
├── models.py # canonical Post schema (pydantic)
├── normalize.py # raw/*.json -> Post
├── deduplicate.py # merges cross-platform duplicate Posts (Mastodon > Bluesky > Tumblr)
├── media.py # mirrors post images into public/media/<post-id>/
├── feed.py # RSS 2.0 generator
└── build.py # normalize_all() -> deduplicate() -> mirror_images() -> data/posts.json + public/*
raw/<platform>/ # unmodified raw data per platform
data/posts.json # generated, canonical data set
public/ # static output (posts.json, latest.json, feed.xml, media/)
poc/ # frozen PoC experiment, see FINDINGS.md
Dependencies and the .venv are managed with uv
(pyproject.toml + uv.lock).
uv sync # creates/updates .venv from uv.lock
uv run <script.py> # runs a script inside the .venv
uv add <package> # add a new dependency