When Spotify released Wrapped for the first time, I was beaten in minutes listened to music by far by my friends.
But, I was actually listening to music on the radio all day long.
That left me wondering: How do I track my music listening record besides Spotify? I liked the idea of analyzing my listening behaviour, but I had heard of scrobblers and never felt the urge to connect to Last.fm.
Every year since, when a new Wrapped is published, I have to think again about my "problem".
timeline
title Project Development Journey
section Initial Idea
Spotify Wrapped Released : Idea born
: Get radio tracks to Spotify?
: Run songs on headless system?
: ...what a waste of energy π‘
section December 2025
Dec 23rd : Lying in bed googling stuff
: Wrapped haunting me again
: Musical age: 66 years
: Behind friends in minutes
: Losing street credibility π’
Dec 24th : Starting tests on Christmas Eve
: Working better than expected! π
: API understood
: Getting artist and title info
: First scrobble with pylast
: All work done by pylast library
Dec 25th : Kids playing with new toys
: Scrobbling works! π
: Minor flaws fixed
: Exception handling added
: Local work complete
: Next: Docker, then Unraid
Dec 26th-30th : Dockerfile creation
: Publishing to Docker Hub
: GitHub Actions setup
: Much harder than the code! π³
Alternative Timeline View:
- π Initial Idea β When was Wrapped released the first time!? Idea was born: Get Radio tracks to Spotify, run the Songs on a headless system...what a waste of energy.
- π Dec 23, 2025 β Lying in bed googling stuff...Wrapped is asking me again, since my "musical age" is 66. I am again far behind my friends in minutes spent with music, which is unfair, because I am listening to super good stuff all the time, but it is unrecognized, unrecorded...I am losing street credibility.
- π Dec 24, 2025 β Starting with some tests while everyone is getting ready for Christmas Eve, working better than expected! API is understood and workflow is getting artist and title information. First scrobble is placed with pylast library. No efforts for me, all the work done by pylast. Time saved.
- π Dec 25, 2025 β While the kids are playing with their new toys, scrobbling works with some minor (?) flaws. Code needs to wait here for a second and adding exceptions there is doing the trick. Locally my work is done, next step docker, then unraid.
- π³ Dec 26-30, 2025 β Dockerfile, publishing to Docker Hub, using DHI, getting GitHub Actions working properly, makes me forget how easy the first part was.
This installation guide is provided to you by github copilot:
This guide covers two methods to install and run the BEO Radio Scrobbler: using uv (modern Python package manager) or running as a Docker container.
- Python 3.11 or higher (for uv installation)
- Docker (for Docker installation)
- Last.fm account with API credentials
- Bang & Olufsen device on your network
uv is a fast Python package installer and resolver.
# On macOS and Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# On Windows
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"git clone https://github.com/CaptainPimpJr/beo_radio_scrobbler.git
cd beo_radio_scrobblerCreate a .env file in appdata/config/:
# . env file
LASTFM_API_KEY=your_lastfm_api_key
LASTFM_API_SECRET=your_lastfm_api_secret
LASTFM_USERNAME=your_lastfm_username
LASTFM_PASSWORD=your_lastfm_password
LOCAL_TIMEZONE=Europe/Berlin
RUN_MODE=production
BEO_IP=192.168.1.100
LOGLEVEL=INFOEnvironment Variables Explained:
LASTFM_API_KEY: Your Last.fm API key (get from https://www.last.fm/api/account/create)LASTFM_API_SECRET: Your Last.fm API secretLASTFM_USERNAME: Your Last.fm usernameLASTFM_PASSWORD: Your Last.fm passwordLOCAL_TIMEZONE: Your timezone (e.g.,UTC,Europe/Berlin,America/New_York)RUN_MODE: Operation mode (production,detect,detect_smpl, ornotify_me)BEO_IP: IP address of your Bang & Olufsen deviceLOGLEVEL: Logging level (DEBUG,INFO,WARNING,ERROR)
uv syncThis will create a virtual environment and install all dependencies.
# Activate the virtual environment
source .venv/bin/activate # On Linux/macOS
# or
. venv\Scripts\activate # On Windows
# Run the scrobbler
python -m beo_radio_scrobbler
# Or use the CLI entry point
beo-radio-scrobblerFor Linux systems with systemd, create a service file:
sudo nano /etc/systemd/system/beo-radio-scrobbler.serviceAdd the following content (adjust paths):
[Unit]
Description=BEO Radio Scrobbler
After=network.target
[Service]
Type=simple
User=your_username
WorkingDirectory=/path/to/beo_radio_scrobbler
Environment="PATH=/path/to/beo_radio_scrobbler/. venv/bin"
ExecStart=/path/to/beo_radio_scrobbler/.venv/bin/python -m beo_radio_scrobbler
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetEnable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable beo-radio-scrobbler
sudo systemctl start beo-radio-scrobbler
sudo systemctl status beo-radio-scrobblergit clone https://github.com/CaptainPimpJr/beo_radio_scrobbler.git
cd beo_radio_scrobblerdocker build -t beo-radio-scrobbler .docker run -d \
--name beo-radio-scrobbler \
--restart unless-stopped \
-e LASTFM_API_KEY=your_lastfm_api_key \
-e LASTFM_API_SECRET=your_lastfm_api_secret \
-e LASTFM_USERNAME=your_lastfm_username \
-e LASTFM_PASSWORD=your_lastfm_password \
-e LOCAL_TIMEZONE=Europe/Berlin \
-e RUN_MODE=production \
-e BEO_IP=192.168.1.100 \
-e LOGLEVEL=INFO \
-v $(pwd)/appdata:/app/appdata \
beo-radio-scrobblerCreate a docker-compose.yml file:
version: '3.8'
services:
beo-radio-scrobbler:
build: .
container_name: beo-radio-scrobbler
restart: unless-stopped
environment:
- LASTFM_API_KEY=your_lastfm_api_key
- LASTFM_API_SECRET=your_lastfm_api_secret
- LASTFM_USERNAME=your_lastfm_username
- LASTFM_PASSWORD=your_lastfm_password
- LOCAL_TIMEZONE=Europe/Berlin
- RUN_MODE=production
- BEO_IP=192.168.1.100
- LOGLEVEL=INFO
volumes:
- ./appdata:/app/appdata
network_mode: host # Required to access local network devicesCreate a .env file (instructions above) and place it inside appdata/config/. Where appdata/ is defined by '-v' of 'OPTION A' or volumes of 'OPTION B'
Then run:
docker-compose up -d# Docker run
docker logs -f beo-radio-scrobbler
# Docker Compose
docker-compose logs -fOr check logs folder.
# Docker run
docker stop beo-radio-scrobbler
# Docker Compose
docker-compose downThe RUN_MODE environment variable controls how the application behaves:
production: Continuously scrobbles tracks to Last.fm (recommended for 24/7 operation)detect: Detects one track and exits (useful for testing)notify_me: Notification mode for debugging
Logs are stored in the appdata/logs/ directory:
log_radio_scrobbler.log: Main application log (rotated daily, kept for 7 days)log_scrobbles.log: Scrobble history (production mode, kept for 5 years)log_detections.log: Detection log (detect modes, kept for 1 week)log_notifications.log: Notification log (notify_me mode, kept for 7 days)
- Ensure your device is powered on and connected to the network
- Verify the IP address in your environment variables
- Check that your Docker container has
network_mode: host(for Docker)
- Verify your API credentials are correct
- Check that you've created an API account at https://www.last.fm/api/account/create
- Check the logs in
appdata/logs/log_radio_scrobbler.log - Ensure all required environment variables are set
- Verify your
RUN_MODEsetting (useproductionfor continuous operation)
Docker installation on Unraid will be supported through Community Applications once the project is published.
For more detailed information, check out these documentation files:
- Run Modes - Different operation modes (production, detect, detect_smpl, notify_me)
- Feature Requests - Planned features and how to contribute
- How-Tos - Guides and tutorials for common tasks
- Initial proof of work written using
requestsandpylast
-
pylast - Python library for Last.fm scrobbling
-
BeoNetRemote Client API - Bang & Olufsen's API for third-party integration
-
uv - An extremely fast Python package installer and resolver
-
Astral UV Docker Example - Dockerfile template and basic setup
-
python-dotenv - loading .env file
-
Arrow (Apache 2.0) - Better dates and times for Python
-
Loguru (MIT) - Python logging made simple
-
Pydantic (MIT) - Data validation using Python type hints
-
Requests (Apache 2.0) - HTTP library for Python
-
PyYAML (MIT) - YAML parser and emitter for Python
-
AI Assistance:
- Static if-else cases improved with Claude Sonnet 4.5
- Project structure modernized with GitHub Copilot
- This README enhanced with GitHub Copilot π€
Copied from B&O documentation (B&O 3rd Party Integration):
Bang & Olufsen Audio Systems: Beosound Stage Beosound 35 Beosound Essence 2nd Gen Beosound Core Beosound Shape Beosound Edge Beosound 1 1st / 2nd Gen Beosound 2 1st / 2nd Gen Beolink Converter NL/ML Beoplay A9 2nd / 3rd / 4th Gen Beoplay A6 Beoplay M5 Beoplay M3
Bang & Olufsen Active Loudspeakers: BeoLab 90 BeoLab 50
I do not think I want to add a "Buy me a coffee new B&O Speaker"-Button.
But if you insist on supporting with a replacement for my Beosound 1, open an issue in github...
I will add Mozart API as soon as I have replaced my Beosound 1 (which is not planned), or I have access to a Mozart API supporting device.