use std::io::stdin;
use {dotenvy::dotenv, tokio::main};
use qobuz_api_rust::{
api::service::QobuzApiService, errors::QobuzApiError, metadata::MetadataConfig,
utils::sanitize_filename,
};
#[main]
async fn main() -> Result<(), QobuzApiError> {
println!("Qobuz API Rust Client");
if let Err(e) = dotenv() {
println!("Failed to load .env file: {}", e);
}
let mut service = QobuzApiService::new().await?;
println!(
"Qobuz API service initialized with app ID: {}",
service.app_id
);
let config = MetadataConfig::default();
match service.authenticate_with_env().await {
Ok(login_result) => {
println!("Authentication successful!");
if let Some(user) = &login_result.user
&& let Some(user_id) = user.id
{
println!("User ID: {}", user_id);
}
if let Some(auth_token) = &login_result.auth_token {
println!("Auth token: {}", auth_token);
}
}
Err(e) => {
println!("Authentication failed: {}", e);
println!("Please set authentication credentials in your .env file:");
println!(" - For token-based: QOBUZ_USER_ID and QOBUZ_USER_AUTH_TOKEN");
println!(" - For email-based: QOBUZ_EMAIL and QOBUZ_PASSWORD");
println!(" - For username-based: QOBUZ_USERNAME and QOBUZ_PASSWORD");
}
}
loop {
println!("\nWhat do you want to search for? (e.g., 'Miles Davis')");
let mut query = String::new();
stdin().read_line(&mut query).expect("Failed to read line");
let query = query.trim();
println!();
println!("Search for an a) album or t) track?");
let mut search_type = String::new();
stdin()
.read_line(&mut search_type)
.expect("Failed to read line");
let search_type = search_type.trim();
if search_type == "a" {
match service.search_albums(query, Some(10), None, None).await {
Ok(result) => {
if let Some(albums) = result.albums
&& let Some(items) = albums.items
{
println!();
println!("Found {} albums:", items.len());
for (i, album) in items.iter().enumerate() {
println!(
"{}) {} - {}",
i + 1,
album.artist.as_ref().map_or("Unknown Artist", |a| a
.name
.as_deref()
.unwrap_or("Unknown Artist")),
album.title.as_deref().unwrap_or("No title")
);
}
println!("\nEnter the number of the album to download (or 'c' to cancel):");
let mut choice = String::new();
stdin().read_line(&mut choice).expect("Failed to read line");
let choice = choice.trim();
if choice == "c" {
continue;
}
if let Ok(album_index) = choice.parse::<usize>()
&& album_index > 0
&& album_index <= items.len()
{
let selected_album = &items[album_index - 1];
if let Some(album_id) = &selected_album.id {
let quality = choose_quality()?;
let album_artist_name =
if let Some(ref album_artist) = selected_album.artist {
album_artist
.name
.as_ref()
.unwrap_or(&"Unknown Artist".to_string())
.clone()
} else {
"Unknown Artist".to_string()
};
let album_title = selected_album
.title
.as_ref()
.unwrap_or(&"Unknown Album".to_string())
.clone();
let album_artist_dir = sanitize_filename(&album_artist_name);
let album_title_dir = sanitize_filename(&album_title);
let album_path =
format!("downloads/{}/{}", album_artist_dir, album_title_dir);
println!();
println!("Downloading album...");
match service
.download_album(album_id, &quality, &album_path, &config)
.await
{
Ok(_) => println!("Album downloaded successfully!"),
Err(e) => println!("Failed to download album: {}", e),
}
}
}
}
}
Err(e) => {
println!();
println!("Search failed: {}", e)
}
}
} else if search_type == "t" {
match service.search_tracks(query, Some(10), None, None).await {
Ok(result) => {
if let Some(tracks) = result.tracks
&& let Some(items) = tracks.items
{
println!();
println!("Found {} tracks:", items.len());
for (i, track) in items.iter().enumerate() {
println!(
"{}) {} - {}",
i + 1,
track.performer.as_ref().map_or("Unknown Artist", |a| a
.name
.as_deref()
.unwrap_or("Unknown Artist")),
track.title.as_deref().unwrap_or("No title")
);
}
println!("\nEnter the number of the track to download (or 'c' to cancel):");
let mut choice = String::new();
stdin().read_line(&mut choice).expect("Failed to read line");
let choice = choice.trim();
if choice == "c" {
continue;
}
if let Ok(track_index) = choice.parse::<usize>()
&& track_index > 0
&& track_index <= items.len()
{
let selected_track = &items[track_index - 1];
if let Some(track_id) = selected_track.id {
let quality = choose_quality()?;
let extension = match quality.as_str() {
"5" => "mp3", "6" | "7" | "27" => "flac", _ => "flac", };
let track_details =
service.get_track(&track_id.to_string(), None).await?;
let album_details = if let Some(ref track_album) =
track_details.album
{
track_album.as_ref().clone()
} else {
println!("Warning: No album information available for track");
let filename = format!(
"downloads/{}.{}",
track_details.title.as_deref().unwrap_or("track"),
extension
);
match service
.download_track(
&track_id.to_string(),
&quality,
&filename,
&config,
)
.await
{
Ok(_) => {
println!();
println!("Track downloaded successfully!");
}
Err(e) => {
println!();
println!("Failed to download track: {}", e)
}
}
continue;
};
let album_artist_name =
if let Some(ref album_artist) = album_details.artist {
album_artist
.name
.as_ref()
.unwrap_or(&"Unknown Artist".to_string())
.clone()
} else {
"Unknown Artist".to_string()
};
let album_title = album_details
.title
.as_ref()
.unwrap_or(&"Unknown Album".to_string())
.clone();
let track_number = track_details.track_number.unwrap_or(0);
let track_title = track_details
.title
.as_ref()
.unwrap_or(&format!("Track {}", track_id))
.clone();
let album_artist_dir = sanitize_filename(&album_artist_name);
let album_title_dir = sanitize_filename(&album_title);
let album_dir =
format!("downloads/{}/{}", album_artist_dir, album_title_dir);
let track_filename =
format!("{:02}. {}", track_number, track_title);
let sanitized_filename = sanitize_filename(&track_filename);
let filename =
format!("{}/{}.{}", album_dir, sanitized_filename, extension);
println!();
println!("Downloading track...");
println!();
match service
.download_track(
&track_id.to_string(),
&quality,
&filename,
&config,
)
.await
{
Ok(_) => {
println!();
println!("Track downloaded successfully!");
}
Err(e) => {
println!();
println!("Failed to download track: {}", e)
}
}
}
}
}
}
Err(e) => println!("Search failed: {}", e),
}
}
}
}
fn choose_quality() -> Result<String, QobuzApiError> {
println!("\nChoose a quality:");
println!("1) MP3 320 (format_id: 5)");
println!("2) FLAC Lossless (format_id: 6)");
println!("3) FLAC Hi-Res 24 bit <= 96kHz (format_id: 7)");
println!("4) FLAC Hi-Res 24 bit >96 kHz & <= 192 kHz (format_id: 27)");
let mut quality_choice = String::new();
stdin()
.read_line(&mut quality_choice)
.expect("Failed to read line");
let quality_choice = quality_choice.trim();
match quality_choice {
"1" => Ok("5".to_string()),
"2" => Ok("6".to_string()),
"3" => Ok("7".to_string()),
"4" => Ok("27".to_string()),
_ => {
println!("Invalid choice, defaulting to FLAC Lossless.");
Ok("6".to_string())
}
}
}