Skip to main content

📻 addictune-sdk

Async Python SDK for the AudioAddict radio platform

DI.FM · RadioTunes · Rock Radio · Jazz Radio · Classical Radio · Zen Radio

PyPI version Python License CI


Features

  • Fully async — built on httpx with async/await throughout
  • 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 / 304 handling backed by SQLite
  • Auto-pagination — async for iterators 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, pydantic and platformdirs
  • Zero-config — sensible defaults, override via the AddictuneConfig constructor

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)
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)

Source distribution for addictune-sdk 0.4.1
File Size Uploaded
addictune_sdk-0.4.1.tar.gz 43.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for addictune-sdk 0.4.1
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

This release

0.4.1 This release

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.9

2 release files

0.2.8

2 release files

0.2.7

2 release files

0.2.6

2 release files

0.2.5

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page