Spotify Wrapped-style recap generator for ListenBrainz, Last.fm, Libre.fm, and Navidrome. Built with Flask.
Important
Rebranded to Make a Wrapped (was ListenBrainz Wrapped). Site's back up. if something's broken, open a GitHub issue.
New 2025 templates are out!
wrapped.devmatei.com - built by devmatei.com
- pulls from ListenBrainz, Last.fm, Libre.fm, Navidrome, MusicBrainz, Cover Art Archive, and Wikidata. all public, no tokens needed (except the optional Last.fm key you'd already have for artwork anyway)
- artist art tries Last.fm first, falls back to MusicBrainz/Wikidata, and if all else fails there's a built-in editor to upload/zoom/position your own image (saved in local storage, or temporarily on the server for 1 hour)
- a data-driven template system, so the built-in themes and community templates all render from the same format
- a template library to browse featured/newest/popular templates and use any of them, plus a visual editor so artists can design their own Wrapped poster without knowing the format - both still in beta
- rate limits are in so your server doesn't get nuked
- there's a live counter of total wraps ever generated (idk it seemed cool)
- officially listed on the ListenBrainz Enabled Applications page :D
put your top artist in your github profile / website. live shields-style SVG, cached server-side so it won't nuke anything:
options via query params:
type-artist(default),track,genre,minutesservice-listenbrainz(default),lastfm, orlibrefmrange- any period preset, e.g.this_month,this_year,all_timecolor- hex without the#, e.g.color=ff6b9d
navidrome badges work differently since your server credentials never touch this app: generate your wrapped, hit "copy readme badge", and the browser publishes just the final numbers (top artist/track/genre/minutes, nothing else) under a private key stored in your browser. the badge URL stays stable across regenerations, only you can update it, and snapshots expire after 60 days unless refreshed.
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txtcopy .env.example to .env and fill in the basics:
FLASK_ENV=production
SECRET_KEY=<something>
LASTFM_API_KEY=<your-lastfm-key>
# set HTTP_PROXY / HTTPS_PROXY if you tunnel through a proxy
run it:
gunicorn -w 4 -b 0.0.0.0:8000 wrapped-fm:appor locally:
./start.sh <host> <port>or with Docker (change the port in the compose file first):
sudo docker compose up -dLISTENBRAINZ_API=https://api.listenbrainz.org/1
MUSICBRAINZ_API, COVER_ART_API
LISTENBRAINZ_RANGE=this_year
AVERAGE_TRACK_LENGTH_MINUTES, COVER_ART_LOOKUP_LIMIT
LASTFM_API_KEY - required for Last.fm stats + better artist images
LASTFM_API, LASTFM_USER_AGENT
HTTP_TIMEOUT, HTTP_POOL_MAXSIZE, LISTENBRAINZ_CACHE_TTL, LISTENBRAINZ_CACHE_SIZE
APP_RATE_LIMIT, APP_STATS_RATE_LIMIT, APP_IMAGE_RATE_LIMIT, APP_MAX_TOP_RESULTS
APP_IMAGE_CONCURRENCY, APP_IMAGE_QUEUE_LIMIT, APP_IMAGE_QUEUE_TIMEOUT
TEMP_ARTWORK_TTL_SECONDS, TEMP_ARTWORK_MAX_BYTES
WRAPPED_COUNT_FILE (defaults to data/wrapped-count.txt)
WRAPPED_COUNT_SINCE - label for when you started counting wraps (ISO date string)
APP_RATE_LIMIT_SALT, APP_TRUST_PROXY_HEADERS
BADGE_CACHE_TTL (seconds, default 6h), BADGE_CACHE_SIZE
BADGE_SNAPSHOT_DIR, BADGE_SNAPSHOT_TTL_SECONDS (default 60 days), BADGE_SNAPSHOT_MAX_COUNT
templates live as little JSON documents in static/templates/ (official) and data/templates/ (approved community). the renderer consumes the same format, so anything the editor makes runs everywhere the built-in themes do. the marketplace, editor and submission flow are still beta - expect rough edges.
/templates- a page that explains what templates are and how to use them./marketplace- browse templates. search, filter by category, sort by featured/newest/popular, and hit "use this template" to jump straight into the generator. you can also export any template as JSON./editor- the visual template editor. position, resize and configure text, lists, stats, background and artwork on a live Wrapped canvas, then submit it for review. export/import templates as JSON to share them./admin- moderation. community submissions land in apending_reviewstate and only appear publicly after a maintainer approves them.
creators are attributed by a key held in their browser (the same kind of ownership mechanism the badge snapshots use), so the same artist can submit many templates without re-registering, and nobody can casually claim someone else's identity. to enable the review API set TEMPLATE_REVIEW_KEY (see .env.example); leave it empty to disable moderation entirely.
Note
libre.fm integration is marked beta/unstable. the service throttles hard per IP and its API is less predictable than last.fm, so wrapped generation can be slower or occasionally flaky. treat it as experimental.
I self-host my music library on Navidrome and don't use Spotify, but all my friends post their Wrapped every year and everyone's like "wait what's that?" when you show them a ListenBrainz stats page. so I built this. same vibe, works with open music platforms.
go flex your scrobbles at wrapped.devmatei.com :)
I'm Matei (DevMatei) - full-stack dev from Moldova. I build random web tools, run a homelab full of self-hosted stuff, and care way too much about my music library. Check out my other projects at devmatei.com or hit me up on socials if you wanna talk projects or self-hosting.
See CONTRIBUTING.md for setup, style notes, and the PR checklist. TL;DR: keep PRs focused, run python -m py_compile wrapped-fm.py, and attach screenshots for any UI changes.
- security improvements
- faster rendering (capped around 33s by API speeds, not much I can do)
- modular, readable code (kind of)
originally made for Last.fm only by jeff parla <3
AGPL-3.0 - share alike
not affiliated with Spotify, ListenBrainz, Last.fm, Navidrome, or MusicBrainz. just a fan thing built for fun.
