📻 addictune-sdk
Async Python SDK for the AudioAddict radio platform
DI.FM · RadioTunes · Rock Radio · Jazz Radio · Classical Radio · Zen Radio
Features
- Fully async — built on
httpxwithasync/awaitthroughout - 6 networks — DI.FM, RadioTunes, Rock Radio, Jazz Radio, Classical Radio, Zen Radio 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 — per-network sessions,
SecretStr-guarded internal storage - Minimal dependencies —
httpx,pydanticandplatformdirs - Zero-config — sensible defaults, override via the
AddictuneConfigconstructor
API coverage
| Domain | What you can do |
|---|---|
| Auth | Per-network login, restore sessions, 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 |
| Playlists | Browse by popularity/newest, get tracks, followed playlists, record listen history |
| Mix Shows | Browse shows, iterate episodes, upcoming events, follow/unfollow shows |
| Search | Search channels, shows, playlists, and tracks within a network |
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:
...
Sessions are per network. Log in to a specific network, or restore persisted ones:
await client.login("[EMAIL_REDACTED]", "your-password", network="rockradio")
client.set_session("di", "saved-session-key", listen_key="saved-listen-key")
client.has_session("di") # True
await client.ensure_session("radiotunes") # mints one from the stored credentials
A wrong username/password raises AddictuneAuthError.
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")
stream_url = await di.channels.resolve_stream_url(url)
# Set this client's listen key on any track/stream URL (fills the bare
# `?listen_key` placeholder, replaces a stale key)
playable = client.with_listen_key(track.url)
# Favorites
await di.channels.add_favorite(user_id, channel_id)
favs = await di.channels.get_favorites(user_id)
Public artwork can be downloaded without passing API session headers to the CDN:
raw_image = await client.assets.get_bytes(channel.images.url("compact", size=200))
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)
Playlists
# 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)
# Browse the website's popular ordering (active shows, then follow count)
async for show in di.mixshows.iter_popular():
print(show.name, show.follows_count)
# 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)
# Follow or unfollow a show
await di.mixshows.follow(user_id, show_id)
await di.mixshows.unfollow(user_id, show_id)
Search
results = await di.search.query("state of trance")
for show in results.shows.items:
print(show.name)
Built-in networks
| Slug | Name |
|---|---|
di |
DI.FM |
radiotunes |
RadioTunes |
rockradio |
Rock Radio |
jazzradio |
Jazz Radio |
classicalradio |
Classical Radio |
zenradio |
Zen Radio |
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 from your application's own settings layer; the SDK never reads files or environment variables.
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.
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 |
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 | — | — | — |
| 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
Versioning
Semantic versioning. While the SDK is below 1.0, breaking changes bump the minor version (0.3 → 0.4) and are listed in the GitHub release notes; patch releases are backwards compatible. Pin accordingly, e.g. addictune-sdk>=0.4,<0.5.
License
MIT © ukw2d
Disclaimer
This project is not affiliated with, endorsed by, or connected to AudioAddict Inc., Digitally Imported (DI.FM), RadioTunes, Rock Radio, Jazz Radio, Classical Radio, or Zen Radio. All trademarks belong to their respective owners.
Metadata
Release files for addictune-sdk 0.4.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| addictune_sdk-0.4.1.tar.gz | 43.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| addictune_sdk-0.4.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 83.0 kB
Release files / addictune_sdk-0.4.1.tar.gz
| Download URL | addictune_sdk-0.4.1.tar.gz |
|---|---|
| Size | 43.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9bc131ab3ec02b6188e20be479b7add9aba84b4e29755419745f5e7c199303d9
|
|
BLAKE2b-256 checksum How to use checksums |
715bbd9a8bda1a5b4bf9dfd3ac3a7cf401fb48858d3d7921f04b4f54eae465c2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / addictune_sdk-0.4.1-py3-none-any.whl
| Download URL | addictune_sdk-0.4.1-py3-none-any.whl |
|---|---|
| Size | 39.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
aa0ad4c0b9762c015006f2b9e5572759ad3836f54de6030d6472b3a8071d35a2
|
|
BLAKE2b-256 checksum How to use checksums |
23b8054c8668cde639b126ad866a37219ea4b5ce4fbc044a363958ba010fb39e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|