radiosoma

August 2, 2026 ยท View on GitHub

Python client for the SomaFM public channels API, modelled with mediavocab as the canonical media vocabulary.

Install

pip install radiosoma

Modelling

SOMA FM channels follow mediavocab axiom 8:

  • Each channel is a Work with MediaType.RADIO (live-linear broadcast).
  • Each distinct stream encoding (130 kbps AAC, 256 kbps MP3, 64 kbps HE-AAC, 32 kbps HE-AAC) is a separate Release of the same Work, with StreamMode.CONTINUOUS.
  • The provider is audio-only (PlaybackType.AUDIO).
  • Each recently-played song from the recent-tracks feed surfaces as a MediaType.MUSIC Work; the play time is ephemeral runtime state in extra["played_at"], not catalogue identity (axiom A3).

Quick start

from radiosoma import get_stations

for station in get_stations():
    print(station.title, station.best_stream)
    for variant in station.stream_variants:
        print(" ", variant.format, variant.bitrate, variant.url)

Convert to mediavocab

One Release per stream variant

from radiosoma import get_stations
from radiosoma.converters import station_to_releases

jazz = next(s for s in get_stations() if s.station_id == "groovesalad")

for release in station_to_releases(jazz):
    print(release.codec, release.bitrate, release.uri)
    # e.g.  aac    130  https://somafm.com/groovesalad130.pls
    #       mp3    256  https://somafm.com/groovesalad256.pls
    #       he-aac  64  https://somafm.com/groovesalad64.pls
    #       he-aac  32  https://somafm.com/groovesalad32.pls

All releases share the same underlying Work so consumers can deduplicate by identity. The Work carries broadcaster_country="US" (the RADIO country slot; read it via work.country), language="en", media_type=RADIO, and content_genres resolved against mediavocab.taxonomy.genre.GENRE_* constants where possible.

Highest-quality release only

from radiosoma.converters import station_to_release

release = station_to_release(jazz)
print(release.work.title)             # "Groove Salad"
print(release.codec, release.bitrate) # "aac" "130"
print(release.audio_channels)         # "stereo"
print(release.work.content_genres)    # [GENRE_AMBIENT]

Now-playing / recent tracks

from radiosoma import get_recent_tracks, get_stations
from radiosoma.converters import recent_tracks_to_works

jazz = next(s for s in get_stations() if s.station_id == "groovesalad")
songs = get_recent_tracks("groovesalad")

for work in recent_tracks_to_works(songs, jazz)[:3]:
    artist = work.credits[0].entity.name if work.credits else ""
    print(work.extra["played_at"], f"{artist} โ€” {work.title}")
    print(work.extra.get("album", ""))

Provider modality axis

from radiosoma.converters import MODALITY
from mediavocab import PlaybackType

assert MODALITY == {PlaybackType.AUDIO}

HTTP transport

radiosoma uses requests by default, but the HTTP session is pluggable for consistency with sibling API clients in the family.

You can inject your own session:

import requests
from radiosoma import get_stations, get_recent_tracks

sess = requests.Session()
sess.headers.update({"User-Agent": "my-app/1.0"})

for station in get_stations(session=sess):
    ...

tracks = get_recent_tracks("groovesalad", session=sess)

SomaFmStation(raw, session=...) likewise accepts an injected session.

To opt in to a curl_cffi browser-impersonating session, install the optional extra and set the env var:

pip install radiosoma[stealth]
export RADIOSOMA_TRANSPORT=curl_cffi

SomaFM is an open API and does not need stealth transport. This option exists only for parity across the api_clients family.

Docs

Examples

  • mediavocab: the media vocabulary that defines Work, Release, and the other types this library converts SOMA FM data into.

License

Apache-2.0. See LICENSE.