Skip to main content
Annie Mei uses the Spotify Web API to search for anime opening and ending theme songs and provide direct links to them. This enhances the theme song information from MyAnimeList with playable music links.

Authentication

The bot uses the Client Credentials Flow for server-to-server authentication. This requires:
SPOTIFY_CLIENT_ID
SPOTIFY_CLIENT_SECRET
Register your application at the Spotify Developer Dashboard to obtain these credentials.

Client Setup

fn get_spotify_client() -> ClientCredsSpotify {
    let client_id = env::var(SPOTIFY_CLIENT_ID)
        .expect("Expected a spotify client id in the environment");
    let client_secret = env::var(SPOTIFY_CLIENT_SECRET)
        .expect("Expected a spotify client secret in the environment");
    let credentials = Credentials {
        id: client_id,
        secret: Some(client_secret),
    };
    let spotify = ClientCredsSpotify::new(credentials);
    spotify
}
Source: src/utils/spotify.rs:15-27

Search Functionality

The main function searches for a song using both romaji and kana (Japanese) names:
pub fn get_song_url(
    romaji_name: String,
    kana_name: Option<String>,
    artist_name: String,
) -> Option<String>
Source: src/utils/spotify.rs:29-33

Search Strategy

  1. Try Romaji First: Search using the romanized song name
  2. Fallback to Kana: If no results, try the native Japanese name (if available)
  3. Return URL: Return the Spotify track URL from the top result
let romaji_search = send_search_request(&romaji_name, &artist_name);
match romaji_search {
    Ok(search_result) => {
        if let Some(url) = get_url_from_search_result(search_result) {
            return Some(url);
        } else if let Some(kana_name) = kana_name {
            let kana_search = send_search_request(&kana_name, &artist_name);
            // ... handle kana search result
        }
    }
}
Source: src/utils/spotify.rs:49-82

Search Request

Searches are formatted as combined track and artist queries:
fn send_search_request(
    song_name: &String,
    artist_name: &String,
) -> Result<SearchResult, ClientError> {
    let spotify = get_spotify_client();
    spotify.request_token().unwrap();
    spotify.search(
        format!("track:{song_name} artist:{artist_name}").as_str(),
        SearchType::Track,
        Some(Market::Country(Country::UnitedStates)),
        None,
        Some(5),  // Limit to top 5 results
        None,
    )
}
Source: src/utils/spotify.rs:89-103

Search Parameters

  • Query: track:{song_name} artist:{artist_name}
  • Type: Track search only
  • Market: United States (for content availability)
  • Limit: Top 5 results

Result Parsing

The top search result’s Spotify URL is extracted:
fn get_url_from_search_result(search_result: SearchResult) -> Option<String> {
    if let SearchResult::Tracks(page) = search_result {
        if !page.items.is_empty() {
            let track = &page.items[0];
            return Some(track.external_urls["spotify"].to_owned());
        }
        None
    } else {
        None
    }
}
Source: src/utils/spotify.rs:105-118

Redis Caching

Search results are cached in Redis to reduce API calls and improve response times:
let cache_key = format!("{romaji_name}:{kana_name:#?}:{artist_name}");
match check_cache(&cache_key) {
    Ok(value) => {
        return match value.as_str() {
            "None" => None,
            _ => Some(value),
        };
    }
    Err(e) => {
        // Cache miss, proceed with API search
    }
};
Source: src/utils/spotify.rs:35-47

Cache Strategy

  • Key Format: {romaji_name}:{kana_name}:{artist_name}
  • Cached on Success: Stores the Spotify URL
  • Cached on Failure: Stores "None" to avoid repeated failed searches
  • Cache Duration: Determined by Redis configuration
try_to_cache_response(&cache_key, &url);  // Cache successful result
try_to_cache_response(&cache_key, "None");  // Cache negative result
Source: src/utils/spotify.rs:54 and src/utils/spotify.rs:85

Usage Example

The Spotify integration is called from the MyAnimeList response transformer:
use crate::utils::spotify::get_song_url;

let romaji_name = "Guren no Yumiya".to_string();
let kana_name = Some("紅蓮の弓矢".to_string());
let artist_name = "Linked Horizon".to_string();

let spotify_url = get_song_url(romaji_name, kana_name, artist_name);

match spotify_url {
    Some(url) => {
        // Create clickable link in Discord embed
        let link = format!("[**{}**]({})", song_name, url);
    }
    None => {
        // Display song name without link
        let text = format!("**{}**", song_name);
    }
}
Source: src/models/mal_response.rs:99-108

Error Handling

The search function returns Option<String> for graceful handling:
  • Some(url): Song found on Spotify
  • None: No match found (displays unlinked song name)
match romaji_search {
    Ok(search_result) => {
        // Process result
    }
    Err(err) => {
        info!("Could not find track: {err:#?}");
    }
}
Source: src/utils/spotify.rs:83-84

Rate Limiting

Spotify enforces rate limits on their API:
  • Default Limit: 180 requests per minute for non-premium users
  • Mitigation: Redis caching significantly reduces API calls
  • Best Practice: Implement exponential backoff for 429 errors
The rspotify library handles token management automatically, requesting new tokens as needed.

Dependencies

The Spotify integration uses the rspotify crate:
[dependencies]
rspotify = { version = "0.15.3", default-features = false, features = ["client-ureq", "ureq-rustls-tls"] }
Key imports:
use rspotify::{
    ClientCredsSpotify, 
    ClientError, 
    Credentials,
    model::{Country, Market, SearchResult, SearchType},
    prelude::*,
};
Source: src/utils/spotify.rs:1-10

Logging

The integration includes detailed logging for debugging:
info!("Spotify client established");
info!("Romaji Song Name: {:#?}", romaji_name);
info!("Kana Song Name: {:#?}", kana_name);
info!("Searched track: {search_result:#?}");
info!("Found track: {track:#?}");
info!("Spotify Url: {:#?}", spotify_url);
info!("Cache hit for {:#?}", cache_key);
info!("Cache miss for {:#?} with error {:#?}", cache_key, e);
These logs help track search performance, cache effectiveness, and API response quality.