A comprehensive Rust client library for the Tidal music streaming service API. This library provides async/await support, automatic token refresh, and a clean, type-safe interface for interacting with Tidal's music catalog and user data.
Note: This library targets Tidal's v1 API. For Tidal's v2 JSON:API, see tidalv2. Auth for v2 typically uses the same device flow, optionally via tidalrs.
- Complete Music API: Access tracks, albums, artists, and playlists
- Audio Streaming: Stream tracks in various quality levels via MPEG-DASH, with optional rodio playback
- Advanced Search: Search across all content types with filtering
- User Management: Manage favorites, playlists, and user data
- OAuth2 Authentication: Device flow, PKCE, and client-credentials grants with automatic token refresh
- Async/Await: Built on Tokio for high-performance async operations
- Type Safety: Comprehensive type definitions for all Tidal API responses
- Cross-Platform: Works on all platforms supported by Rust
Add this to your Cargo.toml:
[dependencies]
tidalrs = "0.6.2"
tokio = { version = "1.0", features = ["full"] }use tidalrs::{AudioQuality, TidalClient};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a client with your Tidal client ID and secret
let client = TidalClient::new("your_client_id".to_string())
.with_client_secret("your_client_secret");
// Authenticate using device flow
let device_auth = client.device_authorization().await?;
println!("Visit: {}", device_auth.url);
println!("Enter code: {}", device_auth.user_code);
let authz_token = client
.wait_for_authorization(
&device_auth.device_code,
device_auth.expires_in,
device_auth.interval,
)
.await?;
// Now you can use the authenticated client
let track = client.track(123456789).await?;
println!("Track: {} by {}", track.title, track.artists[0].name);
// Play the track
let track_stream = client.track_stream(123456789, AudioQuality::Lossless).await?;
# #[cfg(feature = "playback")]
let music_bytes_stream = track_stream.stream().await?;
Ok(())
}If you already have authentication tokens:
use tidalrs::{TidalClient, Authz};
let authz = Authz::new(
"access_token".to_string(),
Some("refresh_token".to_string()),
Some(12345),
Some("US".to_string()),
None,
);
let client = TidalClient::new("your_client_id".to_string())
.with_authz(authz);
# let _ = client;Application tokens (no user) can be created with with_access_token or client_credentials(). Those sessions can call catalog endpoints but user-specific methods such as favorites and playlists return Error::UserAuthenticationRequired.
The library supports Tidal's OAuth2 device flow, PKCE, and client-credentials grants:
use tidalrs::TidalClient;
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
let client = TidalClient::new("client_id".to_string())
.with_client_secret("client_secret");
// Start device authorization
let device_auth = client.device_authorization().await?;
// User visits the URL and enters the code
println!("Visit: {}", device_auth.url);
println!("Code: {}", device_auth.user_code);
// Poll until the user completes authorization
let authz_token = client
.wait_for_authorization(
&device_auth.device_code,
device_auth.expires_in,
device_auth.interval,
)
.await?;
# let _ = authz_token;
# Ok(())
# }PKCE is available via start_pkce / finish_pkce for browser redirect flows. authorize remains available for a single token exchange if you prefer to poll yourself.
Search across all content types:
use tidalrs::{SearchQuery, ResourceType};
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
# let client = tidalrs::TidalClient::new("client_id".to_string());
let mut query = SearchQuery::new("Radiohead");
query.limit = Some(10);
query.search_types = Some(vec![ResourceType::Artist, ResourceType::Album]);
let results = client.search(query).await?;
for artist in results.artists.items {
println!("Artist: {}", artist.name);
}
for album in results.albums.items {
println!("Album: {}", album.title);
}
# Ok(())
# }Get track information and stream audio:
# #[cfg(feature = "playback")]
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
# let client = tidalrs::TidalClient::new("client_id".to_string());
# use tidalrs::AudioQuality;
// Get track details
let track = client.track(123456789).await?;
println!("{} - {}", track.title, track.artists[0].name);
// Get streaming URL
let stream = client.track_stream(123456789, AudioQuality::Lossless).await?;
let audio_stream = stream.stream().await?;
// Add to favorites
client.add_favorite_track(123456789).await?;
# let _ = audio_stream;
# Ok(())
# }Work with albums and their tracks:
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
# let client = tidalrs::TidalClient::new("client_id".to_string());
// Get album information
let album = client.album(987654321).await?;
println!("Album: {}", album.title);
// Get album tracks
let tracks = client.album_tracks(987654321, Some(0), Some(50)).await?;
for track in tracks.items {
println!("Track {}: {}", track.track_number, track.title);
}
// Add album to favorites
client.add_favorite_album(987654321).await?;
# Ok(())
# }Explore artist information and their albums:
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
# let client = tidalrs::TidalClient::new("client_id".to_string());
// Get artist details
let artist = client.artist(456789123).await?;
println!("Artist: {}", artist.name);
// Get artist's albums
let albums = client.artist_albums(456789123, None, Some(0), Some(20)).await?;
for album in albums.items {
println!("Album: {}", album.title);
}
# Ok(())
# }Manage playlists and their contents:
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
# let client = tidalrs::TidalClient::new("client_id".to_string());
// Create a new playlist
let playlist = client.create_playlist("My Playlist", "A great playlist").await?;
println!("Created playlist: {}", playlist.title);
let etag = playlist.etag.clone().unwrap();
let track_ids = vec![123456789, 987654321];
client.add_tracks_to_playlist(&playlist.uuid, &etag, track_ids, false).await?;
let tracks = client.playlist_tracks(&playlist.uuid, Some(0), Some(100)).await?;
for track in tracks.items {
println!("Track: {}", track.title);
}
client.remove_track_from_playlist(&playlist.uuid, &etag, 123456789).await?;
# Ok(())
# }Manage user's favorite content:
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
# let client = tidalrs::TidalClient::new("client_id".to_string());
// Get favorite tracks
let favorite_tracks = client.favorite_tracks(Some(0), Some(50), None, None).await?;
for fav_track in favorite_tracks.items {
println!("Favorite: {}", fav_track.item.title);
}
// Get favorite albums
let favorite_albums = client.favorite_albums(Some(0), Some(20), None, None).await?;
for fav_album in favorite_albums.items {
println!("Favorite album: {}", fav_album.item.title);
}
// Get favorite artists
let favorite_artists = client.favorite_artists(Some(0), Some(20), None, None).await?;
for fav_artist in favorite_artists.items {
println!("Favorite artist: {}", fav_artist.item.name);
}
# Ok(())
# }The library supports all Tidal audio quality levels:
use tidalrs::AudioQuality;
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
# let client = tidalrs::TidalClient::new("client_id".to_string());
# let track_id = 123456789;
// Available quality levels:
// - AudioQuality::Low
// - AudioQuality::High
// - AudioQuality::Lossless
// - AudioQuality::HiResLossless
let stream = client.track_stream(track_id, AudioQuality::Lossless).await?;
# let _ = stream;
# Ok(())
# }Tidal serves its highest qualities, including Hi-Res Lossless, over MPEG-DASH. Two Cargo features handle these streams:
| Feature | Default | Provides |
|---|---|---|
playback |
Yes | TrackDashPlaybackInfo::stream, a seekable Read + Seek stream of fragmented MP4 (FLAC or AAC) for the demuxer, decoder and audio output of your choice, and TrackStream::stream for progressive URL streams |
rodio |
No | TidalClient::track_rodio_source and Track::rodio_source, ready-to-play rodio sources that fall back across qualities, plus TrackDashPlaybackInfo::source. Includes the symphonia decoders Tidal's codecs need |
# #[cfg(feature = "playback")]
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
# let client = tidalrs::TidalClient::new("client_id".to_string());
let dash_info = client
.track_dash_playback_info(123456789, tidalrs::AudioQuality::HiResLossless)
.await?;
// Media fragments download in the background; reads block until the bytes
// they need arrive, so read from a blocking context.
let stream = dash_info.stream().await?;
# let _ = stream;
# Ok(())
# }Enable the rodio feature, and add rodio itself for audio output:
[dependencies]
tidalrs = { version = "0.6", features = ["rodio"] }
rodio = "0.22"track_rodio_source opens a track as a rodio source. It plays the requested quality when it can. Otherwise it falls back to the other lossless quality (for Lossless or HiResLossless) or lossy quality (for Low or High), then to lower qualities, then higher ones, trying MPEG-DASH before a progressive URL at each quality. A lossless request tries both lossless qualities over MPEG-DASH before any URL:
# #[cfg(feature = "rodio")]
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
# let client = tidalrs::TidalClient::new("client_id".to_string());
let source = client
.track_rodio_source(123456789, tidalrs::AudioQuality::HiResLossless)
.await?;
println!("Playing {:?} over {:?}", source.audio_quality(), source.transport());
// Report download progress (DASH streams only); take the handle before
// handing the source to rodio.
if let Some(buffer) = source.buffer() {
buffer.on_change(|status| println!("{:.0}% downloaded", status.downloaded_percent()));
}
// Rodio's player blocks, so keep it off the async runtime.
tokio::task::spawn_blocking(move || {
let handle = rodio::DeviceSinkBuilder::open_default_sink().expect("open audio device");
let player = rodio::Player::connect_new(handle.mixer());
player.append(source);
player.sleep_until_end();
})
.await?;
# Ok(())
# }If you already have a Track, call track.rodio_source(&client, quality) to skip fetching it again. To play a specific DASH stream without any fallback, append dash_info.source().await? to a rodio Player.
Configure the client for different regions and locales:
use tidalrs::{DeviceType, TidalClient};
let client = TidalClient::new("client_id".to_string())
.with_country_code("GB".to_string()) // United Kingdom
.with_locale("en_GB".to_string()) // British English
.with_device_type(DeviceType::Browser);
# let _ = client;The client automatically handles token refresh, but you can also set up callbacks:
use tidalrs::TidalClient;
let client = TidalClient::new("client_id".to_string())
.with_authz_refresh_callback(|new_authz| {
println!("Tokens refreshed for user: {:?}", new_authz.user_id);
// Save the new tokens to persistent storage
});
# let _ = client;Check the examples/ directory for complete working examples:
basic_search.rs- Simple search functionalitypkce_auth.rs- Logging in through the browser with OAuth PKCEplaylist_management.rs- Creating and managing playlistsaudio_streaming.rs- Streaming and playing audioaudio_streaming_rodio.rs- Playing a track through rodio with automatic quality fallback (requires therodiofeature)favorites_management.rs- Managing user favorites
The examples read their configuration from environment variables: TIDAL_CLIENT_ID, plus TIDAL_CLIENT_SECRET for the authentication example, and TIDAL_ACCESS_TOKEN, TIDAL_USER_ID and optionally TIDAL_REFRESH_TOKEN and TIDAL_COUNTRY_CODE for the others. Run the authentication example to log in and get these values.
- Rust 1.70+
- Tokio runtime
- Valid Tidal API client credentials
This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.
This library is not officially affiliated with Tidal. Use at your own risk and ensure compliance with Tidal's Terms of Service.