A Windows background app that detects currently playing music and posts now-playing data to a custom API. Named for Euterpe, the Greek muse of music.
It's loosely tied to Stream Delta, my Stream Management & overlay application.
- Detects music from any app registered with Windows Media Session (SMTC): Spotify, Apple Music, browsers, Windows Media Player, etc.
- Detects game audio via WASAPI loopback + ACRCloud fingerprinting — useful for games like Final Fantasy XIV whose OSTs are on streaming platforms but don't register with SMTC.
- Posts now-playing data to a configurable API endpoint.
- Lives in the system tray with a settings UI.
- Windows 10/11
- Python 3.10+ (managed via Poetry)
- ACRCloud account (for game audio fingerprinting)
cd app
poetry install
poetry run python main.pyOn first launch, the settings window opens automatically. Enter your ACRCloud credentials and API endpoint.
Config is stored at %LOCALAPPDATA%\euterpium\euterpium.ini.
- Release builds now target a Windows installer built from PyInstaller output.
- GitHub Releases should publish both a portable zip and an installer
.exe. - The app can check GitHub Releases for a newer installer and offer it from the tray menu.
| File | Purpose |
|---|---|
app/main.py |
Entry point — wires tracker, tray, window together |
app/tracker.py |
Background detection loop |
app/smtc.py |
Windows Media Session (SMTC) detection via winsdk |
app/fingerprint.py |
ACRCloud audio fingerprinting |
app/audio_capture.py |
WASAPI loopback capture + audio change detection |
app/game_detector.py |
Detects known game processes via psutil |
app/api_client.py |
Posts to the active API profile endpoint |
app/config.py |
INI config read/write with multi-profile API support |
app/ui/tray.py |
System tray icon (pystray) — swaps icon when fingerprinting |
app/ui/window.py |
Main tkinter window |
app/ui/settings_window.py |
Tabbed settings dialog |
app/ui/notifications.py |
Windows toast notifications (win11toast) |
- Check for known game processes (psutil).
- Game running → WASAPI loopback capture → ACRCloud fingerprint.
- No game → poll Windows Media Session (SMTC) every N seconds.
- Game takes priority over SMTC. When a known game process is detected, the tracker uses WASAPI + ACRCloud exclusively — SMTC is not polled. Music playing in Spotify alongside a game will not be reported.
- Only the current SMTC session is checked. Windows exposes one "current" session at a time (the most recently active one). If Chrome is playing a video and Spotify is also running, only the current session is seen — the ignore list will filter Chrome out, but Spotify won't be detected as a fallback. If this matters,
smtc.pywould need to iteratesessions.get_sessions()instead.
Python's default ProactorEventLoop (IOCP) conflicts with WinRT async callbacks, causing RPC_E_CALL_CANCELED. SMTC calls use asyncio.SelectorEventLoop explicitly.
Apple Music for Windows has two SMTC quirks handled in smtc.py:
- Reports playback status as
PAUSEDeven when actively playing — treated as playing if track metadata is present. - Packs
"Artist — Album"into the artist field and leavesalbum_titleempty — split on the em dash.
Euterpium supports multiple named API profiles (dev/stage/live, or any name you choose), switchable from the Settings window without editing the config file.
Profiles are stored as [api:name] sections in the INI file:
[api]
active = live
[api:dev]
url = http://localhost:8000/webhooks/euterpium
key =
[api:live]
url = https://example.com/webhooks/euterpium
key = your-secret-keyIf you have an older config with a single [api] url/key, it is automatically migrated to [api:dev] on first launch.
Euterpium POSTs JSON to the active profile's endpoint on every track change.
{
"source": "smtc",
"title": "It Overtakes Me",
"artist": "The Flaming Lips",
"album": "At War with the Mystics (Deluxe Version)"
}When the track is identified via ACRCloud (game audio), additional fields are included:
{
"source": "acrcloud",
"title": "Answers",
"artist": "Susan Calloway",
"album": "Final Fantasy XIV: A Realm Reborn",
"release_date": "2013-09-10",
"acrid": "abc123",
"streaming_links": { ... }
}When a known game process is running, a game object is added regardless of source:
{
"source": "acrcloud",
"title": "...",
"game": {
"process": "ffxiv_dx11.exe",
"display_name": "Final Fantasy XIV"
}
}If an API key is set in the active profile, requests include an Authorization: Bearer <key> header.
Euterpium includes a Playnite generic plugin that automatically tells Euterpium when a game starts and stops. When a game is launched via Playnite, the plugin writes a small JSON file; Euterpium picks it up on the next poll cycle and switches to game audio fingerprinting mode. Works for Steam, GOG, Epic, emulators — any source Playnite can launch.
See docs/playnite-plugin.md for build, install, and auto-update instructions.
cd app
poetry install
poetry run pytest # run tests
poetry run ruff check . # lint