Python client for the soundvia.eu API
Project description
soundvia
Python client for the soundvia.eu API — zero dependencies, stdlib only.
Installation
pip install soundvia
Or from source:
git clone https://github.com/soundviadigital/soundvia-python
cd soundvia-python
pip install .
Two clients
| Class | Token type | Use for |
|---|---|---|
SoundviaClient |
App token (svapp_…) |
Public content — tracks, releases, artists, playlists, search |
OAuthClient |
User access token (svtok_…) |
User-specific data — library, playlists, history, follows, notifications, comments |
SoundviaClient — public API
Get an app token from soundvia.eu/developer.
from soundvia import SoundviaClient
client = SoundviaClient("svapp_...")
Status
status = client.status()
print(status.app.name)
print(status.app.tier) # "app" or "approved_app"
print(status.app.verification_status) # "none" | "pending" | "approved" | "declined"
print(status.app.limits.requests_per_minute)
Search
Searches tracks, releases, and artists in one call:
results = client.search("lofi beats")
for track in results.tracks:
print(track.title, track.artist_name, track.stream_count)
for release in results.releases:
print(release.title, release.release_type) # "single" | "ep" | "album"
for artist in results.artists:
print(artist.handle, artist.display_name)
Tracks
page = client.list_tracks(q="chill", limit=10)
for track in page.tracks:
print(track.title, track.genre, track.cover_art, track.stream_count)
track = client.get_track("TRACK_ID")
print(track.artist_name, track.artist_handle, track.release_id)
Releases
page = client.list_releases(q="ep", limit=5)
release = client.get_release("RELEASE_ID")
print(release.release_type) # "album", "ep", "single"
print(release.track_ids) # list of track ID strings
print(release.genre, release.cover_art)
Artists
profile = client.get_artist("rebzyyx")
print(profile.artist.display_name, profile.artist.bio, profile.artist.avatar)
for track in profile.tracks:
print(track.title)
for release in profile.releases:
print(release.title)
Playlists
page = client.list_playlists(q="woah", limit=20)
playlist = client.get_playlist("PLAYLIST_ID")
print(playlist.name, playlist.description, playlist.track_ids)
OAuthClient — user API
Step 1 — build the authorization URL
Redirect the user to this URL in their browser:
from soundvia import build_authorization_url
url = build_authorization_url(
client_id="svcli_...",
redirect_uri="https://myapp.com/callback",
scope="user.read library.read playlists.write",
state="random_csrf_token",
)
# redirect user's browser to `url`
Available scopes:
| Scope | Access |
|---|---|
user.read |
Username, display name, avatar, role |
user.email |
Email address |
library.read |
Saved releases, playlists, presaves |
library.write |
Save/remove library items |
playlists.read |
User's playlists |
playlists.write |
Create playlists, add/remove tracks |
listening.read |
Play history |
follows.read |
Following list |
follows.write |
Follow/unfollow users |
notifications.read |
Notification feed |
playback.control |
Control playback |
comments.write |
Post/delete comments |
Step 2 — exchange the code for a token
When the user approves, they're redirected to your redirect_uri with a code param:
from soundvia import OAuthClient
client = OAuthClient.from_code(
client_id="svcli_...",
client_secret="svsec_...",
code=request.args["code"],
redirect_uri="https://myapp.com/callback",
)
# persist these for later use:
print(client.token.access_token)
print(client.token.refresh_token)
print(client.token.expires_in)
print(client.token.scope)
Restore a saved token
client = OAuthClient.from_token(
"svtok_...",
refresh_token="svref_...",
client_id="svcli_...",
client_secret="svsec_...",
)
Token lifecycle
# refresh an expired access token (rotates both tokens)
new_token = client.refresh()
print(new_token.access_token, new_token.refresh_token)
# revoke the current token
client.revoke()
# inspect the current token
info = client.token_info()
User profile
me = client.me() # requires user.read
print(me.username, me.display_name, me.avatar, me.role)
me = client.me() # add user.email scope to also get:
print(me.email)
Library
library = client.get_library() # requires library.read
print(library.counts.total, library.counts.saved_releases)
for item in library.items:
print(item.item_type, item.name, item.track_count)
# item_type: "playlist" | "release" | "presave_release"
print(item.is_owner, item.is_collaborator, item.visibility)
# save / remove — requires library.write
client.save_to_library("release", "RELEASE_ID")
client.save_to_library("playlist", "PLAYLIST_ID")
client.save_to_library("presave_release", "RELEASE_ID")
client.remove_from_library("release", "RELEASE_ID")
Playlists
result = client.get_playlists() # requires playlists.read
for pl in result.playlists:
print(pl.name, pl.track_count, pl.is_owner, pl.visibility)
# create — requires playlists.write
pl = client.create_playlist("Late Night Jams", description="vibes only")
print(pl.id)
# add / remove tracks — requires playlists.write
client.add_track_to_playlist("PLAYLIST_ID", "TRACK_ID")
client.remove_track_from_playlist("PLAYLIST_ID", "TRACK_ID")
Listening history
history = client.get_listening_history() # requires listening.read
for entry in history.history:
print(entry.track_id, entry.played_at)
Follows
follows = client.get_follows() # requires follows.read
print(follows.following_ids)
client.follow("USER_ID") # requires follows.write
client.unfollow("USER_ID")
Notifications
notifs = client.get_notifications() # requires notifications.read
for n in notifs.notifications:
print(n.type, n.message, n.read, n.created_at)
Comments
comment_id = client.post_comment("TRACK_ID", "great track!") # requires comments.write
Error handling
from soundvia import (
AuthenticationError,
NotFoundError,
InsufficientScopeError,
RateLimitError,
APIError,
)
try:
track = client.get_track("bad-id")
except NotFoundError:
print("track doesn't exist")
except InsufficientScopeError:
print("token is missing a required scope")
except RateLimitError as e:
print(f"retry after {e.retry_after}s")
except AuthenticationError:
print("check your token")
except APIError as e:
print(e.status_code)
All exceptions inherit from SoundviaError.
| Exception | HTTP status |
|---|---|
AuthenticationError |
401 |
InsufficientScopeError |
403 |
NotFoundError |
404 |
RateLimitError |
429 (after retries) |
APIError |
5xx or network failure |
On 429 the client reads Retry-After and sleeps before retrying. On 5xx it backs off exponentially. Both give up after max_retries attempts (default 3).
Every response has a .raw field
track = client.get_track("TRACK_ID")
print(track.raw) # original response dict
Client options
SoundviaClient(
token,
base_url="https://soundvia.eu/api/v1",
timeout=15,
max_retries=3,
)
OAuthClient.from_token(
access_token,
client_id="",
client_secret="",
refresh_token="",
base_url="https://soundvia.eu",
timeout=15,
max_retries=3,
)
Context manager
with SoundviaClient("svapp_...") as client:
print(client.status().app.name)
with OAuthClient.from_token("svtok_...") as client:
print(client.me().username)
Tests
pip install pytest
pytest tests/
Project details
Release history Release notifications | RSS feed
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 soundvia-1.0.2.tar.gz.
File metadata
- Download URL: soundvia-1.0.2.tar.gz
- Upload date:
- Size: 17.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
10c950f57461810a6fa8b44cf5dd722e8039bfafd0b54804b16d4d0c8b5eb173
|
|
| MD5 |
4f47d8ca501aba013dd0e4f23d358c2b
|
|
| BLAKE2b-256 |
3f86f4aba4ebc986c292dde01476461b02db4a413352e95358c67d21001b7008
|
File details
Details for the file soundvia-1.0.2-py3-none-any.whl.
File metadata
- Download URL: soundvia-1.0.2-py3-none-any.whl
- Upload date:
- Size: 12.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6f4c925a62d8c0d129f2372948362aacf21393461f06ab088513ee93aa45b77f
|
|
| MD5 |
9e7fb2ac032157fd1b621ec144c942a0
|
|
| BLAKE2b-256 |
b9c84258f458a97c27ff3345c1278ce271e2c66a7d8f7318af45e00b511926d4
|