soundcloud-ma-provider
September 1, 2026 · View on GitHub
This is a Music Assistant provider for SoundCloud. It searches and streams tracks, and browses artist profiles and playlists or sets, without a SoundCloud account. It uses nuvem_de_som to talk to SoundCloud.
| Provider domain | Content |
|---|---|
soundcloud_free | Tracks, artists, playlists/sets |
Stream URLs are resolved at play-time from SoundCloud's public stream endpoint. No credentials, no client ID registration, no OAuth.
Table of contents
- Quick start
- What you get
- How it works
- Provider reference
- Architecture deep-dive
- Development guide
- Troubleshooting
Quick start
1. Install
pip install soundcloud-ma-provider
Dependencies pulled in automatically:
| Package | Role |
|---|---|
music-assistant-plugin-manager | Registers the provider with MA at startup |
nuvem_de_som | SoundCloud search, user/playlist resolution, stream URL extraction |
2. Launch Music Assistant through the plugin manager
mass-pm
3. Enable the provider
In Music Assistant: Settings → Providers → SoundCloud (no login) → +
No configuration fields are required.
What you get
Search: tracks, artists (SoundCloud "people"), and playlists/sets by keyword.
Artist profiles: navigate to an artist from any search result. The artist profile shows their uploaded tracks.
Playlists / Sets: browse and play any public SoundCloud set.
Browse: a "Popular searches" shortcut runs a curated search for popular tracks. It is not SoundCloud's real trending feed — nuvem_de_som has no endpoint for that.
What is not available: private tracks, and tracks behind a SoundCloud Go+ paywall. Only publicly streamable tracks play. The stream format is AAC, delivered as HLS or plain HTTP depending on the track and on SoundCloud's CDN routing.
How it works
User searches "Burial"
│
└─ SoundCloudProvider.search()
├─ client.search_tracks("Burial", limit=10) ← nuvem_de_som
├─ client.search_people("Burial", limit=10)
└─ client.search_sets("Burial", limit=10)
User follows artist "Burial"
│
└─ get_artist_toptracks("https://soundcloud.com/burial")
└─ client.get_tracks("https://soundcloud.com/burial")
└─ list of track dicts → Track objects
User presses Play
│
└─ get_stream_details("https://soundcloud.com/burial/archangel")
└─ client.resolve_stream("https://soundcloud.com/burial/archangel")
└─ SoundCloud stream URL (HLS .m3u8 or plain HTTP)
└─ StreamDetails(HLS or HTTP, AAC)
└─ MA fetches and plays
The stream type is detected automatically: if the resolved URL contains m3u8, StreamType.HLS is used; otherwise StreamType.HTTP. MA handles both.
Provider reference
Source: soundcloud_ma_provider/__init__.py
Domain: soundcloud_free
Audio format: AAC (stream type: HLS or HTTP, detected at resolution time)
Supported features
| Feature | Description |
|---|---|
SEARCH | Tracks, artists, playlists/sets |
ARTIST_TOPTRACKS | Artist's uploaded tracks |
BROWSE | "Popular searches" curated-search shortcut |
Media type mapping
| MA type | SoundCloud concept | item_id format |
|---|---|---|
Track | Track | https://soundcloud.com/<user>/<track-slug> |
Artist | User / profile | https://soundcloud.com/<user> |
Playlist | Set / playlist | https://soundcloud.com/<user>/sets/<set-slug> |
Item IDs are always full SoundCloud page URLs. nuvem_de_som accepts these directly in all resolution calls — no ID-to-URL conversion is needed.
Methods
| Method | What it does |
|---|---|
search(query, media_types, limit) | Searches SoundCloud for tracks, people, and/or sets |
get_artist(prov_artist_id) | Resolves user profile by URL; falls back to a name stub on failure |
get_artist_toptracks(prov_artist_id) | Returns the artist's uploaded tracks |
get_playlist(prov_playlist_id) | Returns a minimal playlist stub for set URLs (title derived from the slug); raises MediaNotFoundError otherwise |
get_playlist_tracks(prov_playlist_id, page) | Fetches set track listing via client.get_tracks() |
get_track(prov_track_id) | Returns a stub track (metadata resolved lazily at stream time) |
browse(path) | Returns a "Popular searches" folder at root; curated-search tracks one level deep |
get_stream_details(item_id, media_type) | Resolves SoundCloud page URL to audio stream |
get_playlist() behaviour
nuvem_de_som has no endpoint to resolve a set/playlist page, so get_playlist() cannot fetch a real title, artwork, or owner. For a URL that looks like a SoundCloud set (.../sets/<slug>), it returns a minimal Playlist with the title derived from the slug — consistent with the slug fallback get_artist() and get_track() already use — so a playlist surfaced by search() stays openable. For anything else, it raises MediaNotFoundError. get_playlist_tracks() fetches the full track listing directly and works independently of get_playlist().
get_track() stub behaviour
get_track() also returns a stub Track, with the title derived from the URL slug. The real metadata (title, artwork, duration) would need an extra page fetch, and that latency is not worth paying. MA calls get_stream_details() right after, and the player does not need rich metadata to start playback.
Stream type detection
is_hls = "m3u8" in stream_url
StreamDetails(
stream_type=StreamType.HLS if is_hls else StreamType.HTTP,
...
)
SoundCloud serves some tracks as plain HTTP AAC and others as HLS (.m3u8) playlists. The choice depends on the track's upload format and on SoundCloud's CDN routing. MA's player handles both formats.
Artist resolution fallback
get_artist() calls client.resolve_user(url). If the resolution fails (network error, unknown user), it returns a stub Artist with the name derived from the URL slug instead of raising an error. This lets a stale library entry show a name instead of breaking.
Architecture deep-dive
Discovery
[project.entry-points."music_assistant.provider"]
soundcloud_free = "soundcloud_ma_provider"
mass-pm reads this entry point at startup, through music-assistant-plugin-manager, and injects the soundcloud_free domain into MA's provider registry before MA's own startup code runs. See TigreGotico/plugin-managers for the full mechanism.
nuvem_de_som client lifecycle
async def handle_async_init(self) -> None:
from nuvem_de_som import SoundCloudAPI
self._client = SoundCloudAPI()
The provider creates one SoundCloudAPI() instance per provider instance. This instance holds internal state across calls, for example the SoundCloud client ID extracted from the page. The import happens inside handle_async_init(), so a missing dependency raises ProviderUnavailableError cleanly instead of crashing at module import time.
All network calls run in a thread
Every nuvem_de_som call is synchronous. All are wrapped in asyncio.to_thread to avoid blocking the MA event loop:
items = await asyncio.to_thread(
lambda: list(self._client.search_tracks(search_query, limit=limit))
)
Data shape from nuvem_de_som
nuvem_de_som returns plain Python dicts. The conversion helpers (_to_track, _to_artist, _to_playlist) extract fields by key and fall back to safe defaults:
def _to_track(item: dict, domain: str, instance_id: str) -> Track:
page_url = item.get("url", "")
artist_name = item.get("artist") or "Unknown"
artist_url = item.get("artist_url") or ""
...
When artist_url is present it is used as the artist's item_id, so clicking an artist from a track result navigates correctly to their profile page.
Development guide
Set up
git clone https://github.com/TigreGotico/soundcloud-ma-provider
cd soundcloud-ma-provider
pip install -e .
Also install nuvem_de_som from source if needed:
git clone https://github.com/TigreGotico/nuvem_de_som
pip install -e ../nuvem_de_som
Explore the nuvem_de_som API
from nuvem_de_som import SoundCloudAPI
sc = SoundCloudAPI()
# Search
for t in sc.search_tracks("burial", limit=5):
print(t["title"], t["url"], t["duration"])
for u in sc.search_people("burial", limit=5):
print(u["artist"], u["artist_url"])
for s in sc.search_sets("burial", limit=5):
print(s["title"], s["url"])
# Artist profile
user = sc.resolve_user("https://soundcloud.com/burial-official")
print(user)
# Artist tracks
for t in sc.get_tracks("https://soundcloud.com/burial-official"):
print(t["title"], t["url"])
# Set tracks
for t in sc.get_tracks("https://soundcloud.com/burial-official/sets/untrue"):
print(t["title"])
# Stream URL
url = sc.resolve_stream("https://soundcloud.com/burial-official/archangel")
print(url) # direct stream URL (HTTP or HLS .m3u8)
Adding rich playlist metadata
get_playlist() returns a slug-derived stub because nuvem_de_som has no endpoint to resolve a set page. To return real playlists, add a resolve_set() call once nuvem_de_som exposes one:
async def get_playlist(self, prov_playlist_id: str) -> Playlist:
data = await asyncio.to_thread(self._client.resolve_set, prov_playlist_id)
if data:
return _to_playlist(data, self.domain, self.instance_id)
if "/sets/" not in prov_playlist_id:
raise MediaNotFoundError(f"No playlist available for: {prov_playlist_id}")
slug = prov_playlist_id.rstrip("/").split("/")[-1].replace("-", " ").title()
return Playlist(item_id=prov_playlist_id, provider=self.domain, name=slug, ...)
If nuvem_de_som does not have resolve_set, open a feature request on that repo.
Adding more browse content
The browse method surfaces one "Popular searches" folder. To add genre folders:
GENRES = ["electronic", "hip-hop", "ambient", "jazz"]
async def browse(self, path):
parts = ...
if not parts:
return [BrowseFolder(..., name=g.title()) for g in GENRES]
genre = parts[0]
items = await asyncio.to_thread(
lambda: list(self._client.search_tracks(f"{genre}", limit=20))
)
return [_to_track(i, self.domain, self.instance_id) for i in items]
Troubleshooting
"No stream available" error
get_stream_details raises MediaNotFoundError when client.resolve_stream() returns None. Possible causes:
- Track is private or behind SoundCloud Go+.
- SoundCloud updated its stream endpoint and
nuvem_de_somneeds updating. - Regional restriction.
Update first:
pip install -U nuvem_de_som
Search returns no results
nuvem_de_som extracts SoundCloud's client ID from the page at startup. If SoundCloud updates its page structure and extraction fails, all search calls return nothing. Update nuvem_de_som and, if the problem persists, open an issue on its repository.
Artist tracks list is empty
get_tracks(artist_url) scrapes the artist's profile page. If the artist has no public uploads, or SoundCloud's layout changed, this returns empty. Check for nuvem_de_som updates.
HLS playback is choppy or fails
Some MA audio backends have partial HLS support. MPV generally handles HLS well, but simpler backends may not. If you see problems, check your MA audio backend configuration.
Provider not appearing in MA
python -c "
from music_assistant_plugin_manager import find_providers
print(find_providers())
"
# expected: {"soundcloud_free": "soundcloud_ma_provider", ...}
If missing: verify pip install soundcloud-ma-provider succeeded and that you are running mass-pm, not music-assistant directly.