// ── Bandsintown ────────────────────────────────────────────────────────────── // Public REST API: https://rest.bandsintown.com/artists/{name}/events?app_id=… // Bandsintown whitelists app IDs — arbitrary strings now return 403 Forbidden. // `js_app_id` is the ID their own embeddable widget uses and is broadly accepted. pub const BANDSINTOWN_APP_ID: &str = "js_app_id"; #[derive(serde::Serialize, Default)] pub struct BandsintownEvent { datetime: String, // ISO 8601 (e.g. "2026-04-23T20:30:00") venue_name: String, venue_city: String, venue_region: String, venue_country: String, url: String, on_sale_datetime: String, lineup: Vec, } /// Fetch upcoming Bandsintown events for an artist by name. /// Returns an empty list on any failure (404, network, parse) — the UI /// just hides the section in that case. #[tauri::command] pub async fn fetch_bandsintown_events(artist_name: String) -> Result, String> { let trimmed = artist_name.trim(); if trimmed.is_empty() { return Ok(vec![]); } // Bandsintown expects the artist name URL-encoded; their API treats `/` as a // path separator (so e.g. AC/DC must be encoded as AC%252FDC). let encoded: String = url::form_urlencoded::byte_serialize(trimmed.as_bytes()).collect(); let url = format!( "https://rest.bandsintown.com/artists/{}/events?app_id={}", encoded, BANDSINTOWN_APP_ID ); let client = match reqwest::Client::builder() .timeout(std::time::Duration::from_secs(10)) .build() { Ok(c) => c, Err(_) => return Ok(vec![]), }; let resp = match client.get(&url).send().await { Ok(r) => r, Err(_) => return Ok(vec![]), }; if !resp.status().is_success() { return Ok(vec![]); } let raw: serde_json::Value = match resp.json().await { Ok(v) => v, Err(_) => return Ok(vec![]), }; let arr = match raw.as_array() { Some(a) => a, None => return Ok(vec![]), }; let mut out: Vec = Vec::with_capacity(arr.len().min(20)); for item in arr.iter().take(20) { let venue = item.get("venue").cloned().unwrap_or(serde_json::Value::Null); let lineup = item .get("lineup") .and_then(|v| v.as_array()) .map(|a| a.iter().filter_map(|s| s.as_str().map(String::from)).collect()) .unwrap_or_default(); out.push(BandsintownEvent { datetime: item.get("datetime").and_then(|v| v.as_str()).unwrap_or("").to_string(), venue_name: venue.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string(), venue_city: venue.get("city").and_then(|v| v.as_str()).unwrap_or("").to_string(), venue_region: venue.get("region").and_then(|v| v.as_str()).unwrap_or("").to_string(), venue_country: venue.get("country").and_then(|v| v.as_str()).unwrap_or("").to_string(), url: item.get("url").and_then(|v| v.as_str()).unwrap_or("").to_string(), on_sale_datetime: item.get("on_sale_datetime").and_then(|v| v.as_str()).unwrap_or("").to_string(), lineup, }); } Ok(out) }