Async Python SDK for the AudioAddict radio platform (DI.FM, RadioTunes, RockRadio, JazzRadio, ClassicalRadio, ZenRadio)
Project description
📻 addictune-sdk
Async Python SDK for the AudioAddict radio platform
DI.FM · RadioTunes · RockRadio · JazzRadio · ClassicalRadio · ZenRadio
Features
- Fully async — built on
httpxwithasync/awaitthroughout - 6 networks — DI.FM, RadioTunes, RockRadio, JazzRadio, ClassicalRadio, ZenRadio out of the box
- Typed models — Pydantic v2 models for every API response, with IDE autocomplete and validation
- ETag caching — automatic HTTP
If-None-Match/304handling backed by SQLite - Auto-pagination —
async foriterators that transparently walk pages - Resilient transport — retry with exponential backoff + jitter, circuit breaker
- Auth helpers — session and direct login,
SecretStr-guarded internal storage - Minimal dependencies — only
httpxandpydantic - Zero-config — sensible defaults, override via constructor, JSON file, or auto-discovery
API coverage
| Domain | What you can do |
|---|---|
| Auth | Login (session or direct), retrieve API key + listen key |
| Channels | Browse all channels, get by ID, track history, now playing, stream URLs, favorites |
| Tracks | Get by ID, liked tracks, vote up/down/delete, skip events, audio quality prefs |
| Playlists | Featured playlists, browse by popularity/newest, get tracks, follow, listen history |
| Mix Shows | Browse shows, iterate episodes, upcoming events, followed shows |
| User | Ping API, check premium status, payment methods |
Installation
pip install addictune-sdk
Or with other package managers:
# uv
uv add addictune-sdk
# poetry
poetry add addictune-sdk
# pipx (for scripts)
pipx inject my-tool addictune-sdk
Requires Python 3.12+.
Quick start
import asyncio
from addictune_sdk import Client
async def main():
async with Client() as client:
di = client.network("di")
channels = await di.channels.get_all()
for ch in channels:
print(ch.name)
asyncio.run(main())
Authentication
from addictune_sdk import Client
async with Client() as client:
auth = await client.login("you@example.com", "your-password")
print(f"Logged in as user {auth.user_id}")
Or pass a pre-existing session key:
async with Client(session_key="your-session-key") as client:
...
Network-scoped APIs
Every network is accessed via client.network(slug) and exposes namespaced APIs:
di = client.network("di")
Channels
# List all channels
channels = await di.channels.get_all()
# Single channel by ID
channel = await di.channels.get_by_id(123)
# What's playing right now across all channels
now = await di.channels.get_currently_playing()
# Build a direct stream URL
url = di.channels.get_stream_url("trance", "your-listen-key", quality="hi")
# Favorites
await di.channels.add_favorite(user_id, channel_id)
favs = await di.channels.get_favorites(user_id)
Tracks
# Fetch a track
track = await di.tracks.get_by_id(12345)
# Like / unlike
await di.tracks.vote(12345, direction="up")
await di.tracks.vote(12345, direction="delete")
# Iterate all liked tracks (auto-paginated)
async for track in di.tracks.iter_liked_tracks(user_id):
print(track.title)
# Audio quality
qualities = await di.tracks.get_qualities()
await di.tracks.set_preferred_quality(user_id, quality_id=3)
Playlists
# Featured playlists
featured = await di.playlists.get_featured()
# Browse with auto-pagination
async for pl in di.playlists.iter_playlists(order_by="newest"):
print(pl.name)
# Get playable tracks for a playlist
content = await di.playlists.get_content(playlist_id)
# Followed playlists
async for pl in di.playlists.iter_followed(user_id):
print(pl.name)
Mix Shows
# Browse shows (auto-paginated)
async for show in di.mixshows.iter_shows(active=True):
print(show.name)
# Episodes for a specific show
async for ep in di.mixshows.iter_episodes(show_id):
print(ep.name)
# Upcoming events
upcoming = await di.mixshows.get_upcoming(limit=10)
User
# Health check
ping = await client.user.ping()
print(f"API v{ping.api_version} — {ping.country}")
# Premium status for a network
status = await client.user.check_premium_status("di")
print(status.listener_type, status.skips_remaining)
Built-in networks
| Slug | Name |
|---|---|
di |
DI.FM |
radiotunes |
RadioTunes |
rockradio |
RockRadio |
jazzradio |
JazzRadio |
classicalradio |
ClassicalRadio |
zenradio |
ZenRadio |
Add custom networks via the custom_networks parameter on Client.
Configuration
The SDK uses a frozen dataclass (AddictuneConfig) with sensible defaults. Configuration is explicit and controlled entirely by the host application.
AddictuneConfig is a plain Python frozen=True dataclass — every field has a default, so it works out of the box with zero setup. Override only what you need, using whichever approach fits your application.
There are four ways to configure the SDK, in order of precedence:
| Approach | When to use |
|---|---|
| No config | Scripts, prototypes — defaults are production-ready |
| Programmatic | Desktop apps with their own settings layer (QSettings, NSUserDefaults, etc.) |
| JSON file | File-based settings, shared configs, deployment overrides |
| Auto-discovery | Let the SDK find a config file in standard OS locations automatically |
Defaults only
No config object needed — every field ships with a sensible default:
from addictune_sdk import Client
async with Client() as client:
# Uses AddictuneConfig() under the hood:
# api_base = "https://api.audioaddict.com/v1"
# network = "di"
# timeout = 30.0
# retry = RetryConfig() (3 attempts, exponential backoff)
# circuit = CircuitConfig() (5 failures → open, 60s recovery)
di = client.network("di")
channels = await di.channels.get_all()
Programmatic override
Override top-level fields
Pass an AddictuneConfig to the Client constructor with just the fields you want to change:
from addictune_sdk import Client, AddictuneConfig
config = AddictuneConfig(
network="radiotunes", # default to RadioTunes instead of DI.FM
timeout=15.0, # shorter timeout for latency-sensitive apps
)
async with Client(config=config) as client:
# client.login() will authenticate against the "radiotunes" network
auth = await client.login("you@example.com", "password")
Override retry and circuit-breaker settings
AddictuneConfig has two nested dataclasses — RetryConfig and CircuitConfig — that control resilient transport behaviour:
from addictune_sdk import AddictuneConfig, RetryConfig, CircuitConfig
config = AddictuneConfig(
retry=RetryConfig(
max_attempts=5, # retry up to 5 times before giving up
wait_min=1.0, # wait at least 1s between retries
wait_max=30.0, # cap backoff at 30s
wait_jitter=2.0, # add up to 2s random jitter
),
circuit=CircuitConfig(
failure_threshold=10, # tolerate more failures before tripping
recovery_timeout=30.0, # recover faster (30s instead of 60s)
),
)
How retry works: on each failed attempt the delay is wait_multiplier × 2^(attempt-1), clamped to [wait_min, wait_max], then a random jitter in [0, wait_jitter] is added. With defaults (multiplier 1.0, min 2.0, max 10.0) the delays are approximately 2s → 4s → 8s plus jitter.
How the circuit breaker works: consecutive failures are tracked. Once they reach failure_threshold, the circuit opens and all requests are immediately rejected. After recovery_timeout seconds the circuit closes and new requests are allowed through.
Use dataclasses.replace for small tweaks
If you only need to change one or two fields, use dataclasses.replace on the default instance:
from dataclasses import replace
from addictune_sdk import AddictuneConfig
config = replace(AddictuneConfig(), timeout=10.0, network="jazzradio")
This is equivalent to AddictuneConfig(timeout=10.0, network="jazzradio") but reads more naturally when you're overriding a value you already have.
JSON config file
Load config from a JSON file when your application prefers file-based settings:
from addictune_sdk import Client, AddictuneConfig
config = AddictuneConfig.from_json("~/.config/myapp/addictune.json")
async with Client(config=config) as client:
...
All fields are optional — missing keys fall back to their defaults, so your JSON only needs the overrides:
{
"network": "di",
"timeout": 15.0
}
Full example with every field:
{
"api_base": "https://api.audioaddict.com/v1",
"network": "di",
"timeout": 30.0,
"retry": {
"max_attempts": 3,
"wait_multiplier": 1.0,
"wait_min": 2.0,
"wait_max": 10.0,
"wait_jitter": 1.0
},
"circuit": {
"failure_threshold": 5,
"recovery_timeout": 60.0
}
}
Write a config file from code
Persist settings for later use:
from addictune_sdk import AddictuneConfig
config = AddictuneConfig(timeout=15.0, network="rockradio")
config.to_json("path/to/config.json")
to_json creates parent directories automatically if they don't exist.
Round-trip: read → modify → write
from addictune_sdk import AddictuneConfig
# Load existing config
config = AddictuneConfig.from_json("config.json")
# Modify with dataclasses.replace
from dataclasses import replace
config = replace(config, timeout=20.0)
# Save back
config.to_json("config.json")
Auto-discovery
load_config() searches standard OS config locations in order and returns the first file it finds. If nothing exists, it returns a default AddictuneConfig() — so your code never needs to handle "no config found" as a special case.
| Platform | Search paths (in order) |
|---|---|
| Linux / macOS | $XDG_CONFIG_HOME/addictune/config.json, ~/.addictune/config.json |
| Windows | %APPDATA%\addictune\config.json |
from addictune_sdk import Client, load_config
# Searches standard paths; falls back to defaults if no file exists
config = load_config()
async with Client(config=config) as client:
...
Pass an explicit path to skip auto-discovery:
config = load_config("/etc/myapp/addictune.json")
Configuration reference
AddictuneConfig
| Field | Type | Default | Description |
|---|---|---|---|
api_base |
str |
https://api.audioaddict.com/v1 |
API base URL |
network |
str |
di |
Default network slug used by Client.login() |
timeout |
float |
30.0 |
HTTP request timeout (seconds) |
retry |
RetryConfig |
RetryConfig() |
Retry behaviour (see below) |
circuit |
CircuitConfig |
CircuitConfig() |
Circuit-breaker behaviour (see below) |
RetryConfig
Controls automatic retry with exponential backoff + jitter.
| Field | Type | Default | Description |
|---|---|---|---|
max_attempts |
int |
3 |
Max attempts per request (including initial). Set to 1 to disable retries. |
wait_multiplier |
float |
1.0 |
Exponential backoff multiplier |
wait_min |
float |
2.0 |
Minimum delay between retries (seconds) |
wait_max |
float |
10.0 |
Maximum delay between retries (seconds) |
wait_jitter |
float |
1.0 |
Upper bound of random jitter added to each delay (seconds) |
CircuitConfig
Controls the circuit-breaker that protects against cascading failures.
| Field | Type | Default | Description |
|---|---|---|---|
failure_threshold |
int |
5 |
Consecutive failures before the circuit opens |
recovery_timeout |
float |
60.0 |
Seconds before a tripped circuit allows a retry |
name |
str | None |
None |
Optional label for logging / metrics |
Logging
The SDK uses Python's standard logging library under the addictune_sdk namespace. It does not configure handlers or formatters — that's the host application's responsibility. By default only WARNING and above is visible.
Quick setup
The simplest way to see SDK log output:
import logging
logging.basicConfig(level=logging.DEBUG)
Target just the SDK
To control SDK logging independently of the rest of your application:
import logging
logging.getLogger("addictune_sdk").setLevel(logging.DEBUG)
Or use a dedicated handler with a custom format:
import logging
handler = logging.StreamHandler()
handler.setFormatter(
logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
)
sdk_logger = logging.getLogger("addictune_sdk")
sdk_logger.setLevel(logging.DEBUG)
sdk_logger.addHandler(handler)
Log levels by component
| Component | DEBUG |
INFO |
WARNING |
ERROR |
|---|---|---|---|---|
| Transport (retry / circuit breaker) | Each retry attempt with wait time | Retry succeeded; circuit recovered | Circuit tripped open; request rejected by circuit | All attempts exhausted |
| Cache (ETag / SQLite) | Cache hit, miss, expired, stored, indexed | — | — | — |
| Client | Init, connection close | Successful login | — | — |
Recommended levels:
- Production:
WARNING(default) — only circuit-breaker trips and exhausted retries - Development:
INFO— adds login events and retry recoveries - Debugging:
DEBUG— full visibility into cache behaviour and every retry attempt
License
MIT © ukw2d
Disclaimer
This project is not affiliated with, endorsed by, or connected to AudioAddict Inc., Digitally Imported (DI.FM), RadioTunes, RockRadio, JazzRadio, ClassicalRadio, or ZenRadio. All trademarks belong to their respective owners.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file addictune_sdk-0.1.4.tar.gz.
File metadata
- Download URL: addictune_sdk-0.1.4.tar.gz
- Upload date:
- Size: 60.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Fedora Linux","version":"44","id":"","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
924d3f450b63a9c421944ba1c6a8f6b28a82c964da387d39080cf7deb4eb0a0a
|
|
| MD5 |
e8fcbfdf1108ce19fecbf299192cd697
|
|
| BLAKE2b-256 |
16b1cf1ca87cfbba36aa291c11659b8f25a26778be8ccf54c12c05b8810874fb
|
File details
Details for the file addictune_sdk-0.1.4-py3-none-any.whl.
File metadata
- Download URL: addictune_sdk-0.1.4-py3-none-any.whl
- Upload date:
- Size: 40.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Fedora Linux","version":"44","id":"","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3ac93556b094fc19f25425e45f5dcb9a4626b7b5c8eabd91c32f712cc0a518d1
|
|
| MD5 |
6e3da82c92ede4c251a138ab93ea5873
|
|
| BLAKE2b-256 |
0d17152c60e810b3e64c04e76b68bb2a6fe2baf02e675d49cd75419e1963e9a0
|