# ytmusicapi-rs
Rust bindings for the Python [ytmusicapi](https://github.com/sigma67/ytmusicapi) library, via [PyO3](https://pyo3.rs/).
All methods return `Result<serde_json::Value>`. The conversion goes through Python's `json.dumps` / `json.loads`, so every value is valid JSON.
## Requirements
- Rust (stable)
- Python 3.8+
- [uv](https://docs.astral.sh/uv/)
## Setup
```sh
uv sync # create .venv and install ytmusicapi
cargo build
```
The build script (`build.rs`) runs `uv sync` automatically and embeds the venv path at compile time. The `.cargo/config.toml` sets `VIRTUAL_ENV` to the local `.venv`; `pyo3-build-config` resolves this to the correct Python executable on each platform (Unix: `.venv/bin/python3`, Windows: `.venv/Scripts/python.exe`).
## Authentication
### Unauthenticated (public endpoints only)
```rust
let yt = YTMusic::new()?;
```
### Browser auth (recommended)
Use "Copy as cURL" from Chrome DevTools on any YouTube Music request, pipe it through the included `curl2headers.py` converter, then run the setup wizard:
```sh
```rust
let yt = YTMusic::authenticated("browser.json")?;
```
### OAuth (broken upstream — see [#813](https://github.com/sigma67/ytmusicapi/issues/813))
The setup flow works, but YouTube Music rejects Bearer tokens from user-created OAuth clients. Use browser auth instead.
### Full constructor
```rust
let yt = YTMusic::with_options(Some("browser.json"), "en", "")?;
```
### Static setup helpers
```rust
// Interactive browser auth wizard — reads headers from stdin
YTMusic::setup(Some("browser.json"))?;
// OAuth one-time setup — saves token to oauth.json
YTMusic::setup_oauth("CLIENT_ID", "CLIENT_SECRET", Some("oauth.json"), false)?;
// Create instance from OAuth token file
let yt = YTMusic::with_oauth("oauth.json", "CLIENT_ID", "CLIENT_SECRET")?;
```
---
## API Reference
### Search
```rust
// scope: "library" | "uploads"
fn search(
&self,
query: &str,
filter: Option<&str>,
scope: Option<&str>,
limit: Option<u32>,
ignore_spelling: Option<bool>,
) -> Result<Value>
fn get_search_suggestions(
&self,
query: &str,
detailed_runs: Option<bool>,
) -> Result<Value>
// suggestions: list returned by get_search_suggestions
// indices: which entries to remove; omit to remove all
fn remove_search_suggestions(
&self,
suggestions: &Value,
indices: Option<&[u32]>,
) -> Result<Value>
```
### Browse
```rust
fn get_home(&self, limit: Option<u32>) -> Result<Value>
fn get_artist(&self, channel_id: &str) -> Result<Value>
// params comes from get_artist() response
&self,
channel_id: &str,
params: &str,
limit: Option<u32>,
order: Option<&str>,
) -> Result<Value>
fn get_album(&self, browse_id: &str) -> Result<Value>
// Converts an audioPlaylistId (OLAK5uy_…) to a browseId
fn get_album_browse_id(&self, audio_playlist_id: &str) -> Result<Value>
fn get_song(&self, video_id: &str, signature_timestamp: Option<u64>) -> Result<Value>
fn get_song_related(&self, browse_id: &str) -> Result<Value>
// browse_id comes from get_song() → lyrics.browseId
fn get_lyrics(&self, browse_id: &str, timestamps: Option<bool>) -> Result<Value>
fn get_song_credits(&self, browse_id: &str) -> Result<Value>
fn get_user(&self, channel_id: &str) -> Result<Value>
// params comes from get_user() response
fn get_user_playlists(&self, channel_id: &str, params: &str) -> Result<Value>
fn get_user_videos(&self, channel_id: &str, params: &str) -> Result<Value>
```
### Explore
```rust
fn get_explore(&self) -> Result<Value>
fn get_mood_categories(&self) -> Result<Value>
// params comes from get_mood_categories() response
fn get_mood_playlists(&self, params: &str) -> Result<Value>
// country: 2-letter ISO code, or "ZZ" for global charts
fn get_charts(&self, country: Option<&str>) -> Result<Value>
fn get_tasteprofile(&self) -> Result<Value>
fn set_tasteprofile(
&self,
artists: &[&str],
taste_profile: Option<&Value>,
) -> Result<Value>
fn get_watch_playlist(
&self,
video_id: Option<&str>,
playlist_id: Option<&str>,
limit: Option<u32>,
radio: Option<bool>,
shuffle: Option<bool>,
) -> Result<Value>
fn get_account_info(&self) -> Result<Value>
```
### Playlists
```rust
fn get_playlist(
&self,
playlist_id: &str,
limit: Option<u32>,
related: Option<bool>,
suggestions_limit: Option<u32>,
) -> Result<Value>
// privacy_status: "PUBLIC" | "PRIVATE" (default) | "UNLISTED"
fn create_playlist(
&self,
title: &str,
description: &str,
privacy_status: Option<&str>,
video_ids: Option<&[&str]>,
source_playlist: Option<&str>,
) -> Result<Value>
// move_item: (set_video_id, successor_video_id) — moves set_video_id after successor_video_id
fn edit_playlist(
&self,
playlist_id: &str,
title: Option<&str>,
description: Option<&str>,
privacy_status: Option<&str>,
collaboration: Option<bool>,
move_item: Option<(&str, &str)>,
add_playlist_id: Option<&str>,
sort_order: Option<&str>,
add_to_top: Option<bool>,
vote_option: Option<&str>,
) -> Result<Value>
fn delete_playlist(&self, playlist_id: &str) -> Result<Value>
fn add_playlist_items(
&self,
playlist_id: &str,
video_ids: Option<&[&str]>,
source_playlist: Option<&str>,
duplicates: Option<bool>,
) -> Result<Value>
// videos: track objects as returned by get_playlist
fn remove_playlist_items(&self, playlist_id: &str, videos: &Value) -> Result<Value>
fn join_collaborative_playlist(
&self,
playlist_id: &str,
join_collaboration_token: &str,
) -> Result<Value>
```
### Library (requires auth)
```rust
fn get_library_songs(
&self,
limit: Option<u32>,
validate_responses: Option<bool>,
order: Option<&str>,
) -> Result<Value>
fn get_library_albums(&self, limit: Option<u32>, order: Option<&str>) -> Result<Value>
fn get_library_artists(&self, limit: Option<u32>, order: Option<&str>) -> Result<Value>
fn get_library_subscriptions(&self, limit: Option<u32>, order: Option<&str>) -> Result<Value>
fn get_library_playlists(&self, limit: Option<u32>) -> Result<Value>
fn get_liked_songs(&self, limit: Option<u32>) -> Result<Value>
fn get_history(&self) -> Result<Value>
// song: a song object previously returned by get_history or similar
fn add_history_item(&self, song: &Value) -> Result<Value>
fn remove_history_items(&self, feedback_tokens: &[&str]) -> Result<Value>
fn rate_playlist(&self, playlist_id: &str, rating: &str) -> Result<Value>
fn edit_song_library_status(&self, feedback_tokens: &[&str]) -> Result<Value>
fn subscribe_artists(&self, channel_ids: &[&str]) -> Result<Value>
fn unsubscribe_artists(&self, channel_ids: &[&str]) -> Result<Value>
```
### Podcasts
```rust
// playlist_id starts with MPSP
fn get_podcast(&self, playlist_id: &str, limit: Option<u32>) -> Result<Value>
fn get_episode(&self, video_id: &str) -> Result<Value>
// playlist_id defaults to "RDPN" (New Episodes feed)
fn get_episodes_playlist(&self, playlist_id: Option<&str>) -> Result<Value>
fn get_channel(&self, channel_id: &str) -> Result<Value>
// params comes from get_channel() response
fn get_channel_episodes(&self, channel_id: &str, params: &str) -> Result<Value>
fn get_saved_episodes(&self, limit: Option<u32>) -> Result<Value> // requires auth
fn get_library_podcasts(&self, limit: Option<u32>, order: Option<&str>) -> Result<Value> // requires auth
fn get_library_channels(&self, limit: Option<u32>, order: Option<&str>) -> Result<Value> // requires auth
```
### Uploads (requires auth)
```rust
fn get_library_upload_songs(&self, limit: Option<u32>, order: Option<&str>) -> Result<Value>
fn get_library_upload_artists(&self, limit: Option<u32>, order: Option<&str>) -> Result<Value>
fn get_library_upload_albums(&self, limit: Option<u32>, order: Option<&str>) -> Result<Value>
fn get_library_upload_artist(&self, browse_id: &str, limit: Option<u32>) -> Result<Value>
fn get_library_upload_album(&self, browse_id: &str) -> Result<Value>
// filepath must be an absolute path
fn upload_song(&self, filepath: &str) -> Result<Value>
fn delete_upload_entity(&self, entity_id: &str) -> Result<Value>
```
---
## Error handling
All methods return `Result<Value, YtMusicError>`. The error type has two variants:
```rust
pub enum YtMusicError {
Python(String), // any error raised by the Python library
Json(serde_json::Error), // JSON serialization failure (should not occur in practice)
}
```
## Thread safety
`YTMusic` implements `Send + Sync`. The GIL is acquired explicitly on every call, so instances can be shared across threads via `Arc<YTMusic>`.