spotifav/
lib.rs

1use std::{collections::HashSet, fs::{create_dir_all, File}, io::Write};
2
3use rspotify::{model::PlayableItem, prelude::{BaseClient, OAuthClient}, AuthCodeSpotify, Config, Credentials, OAuth};
4use serde::Deserialize;
5
6static APP_SCOPES: [&str; 1] = [
7    "user-read-currently-playing",
8];
9
10async fn login(spotify: &AuthCodeSpotify) -> Result<(), Box<dyn std::error::Error>> {
11    let url = spotify.get_authorize_url(false)?;
12    match open::that(&url) {
13        Ok(_) => println!("A browser should have opened. Please log in and paste the URL you are redirected to."),
14        Err(_) => println!("If a browser did not open, please open the following URL in your browser: {}", url),
15    }
16    print!("URL: ");
17    std::io::stdout().flush()?;
18    let stdin = std::io::stdin();
19    let mut buffer = String::new();
20    stdin.read_line(&mut buffer)?;
21    let buffer = buffer.trim();
22    let code = spotify.parse_response_code(buffer).unwrap();
23
24    spotify.request_token(&code).await?;
25    spotify.write_token_cache().await?;
26
27    Ok(())
28}
29
30fn read_configs() -> Result<AuthConfig, Box<dyn std::error::Error>> {
31    let configs = directories::ProjectDirs::from("org", "prabo", "spotifav")
32        .ok_or("Failed to get project directories")?
33        .config_dir()
34        .join("config.toml");
35    if !configs.exists() {
36        create_dir_all(configs.parent().unwrap())?;
37        File::create(&configs)?;
38        println!("Please fill in the following informations in the file at: {}", configs.display());
39        println!("  client_id = \"<your client id>\"");
40        println!("  client_secret = \"<your client secret>\"");
41        println!("  redirect_uri = \"http://localhost:8888\"");
42        println!("More information can be found at: https://developer.spotify.com/documentation/web-api/tutorials/code-flow/");
43        println!("Generally create a new app at: https://developer.spotify.com/dashboard/");
44        return Err(std::io::Error::new(std::io::ErrorKind::NotFound, "Config file not found").into());
45    }
46    let conf: AuthConfig = toml::from_str(&std::fs::read_to_string(configs)?)?;
47    Ok(conf)
48}
49
50#[derive(Deserialize, Debug)]
51struct AuthConfig {
52    #[serde(with = "MockCredentials")]
53    pub creds: Credentials,
54    #[serde(with = "MockOAuth")]
55    pub oauth: OAuth,
56}
57
58#[derive(Deserialize)]
59#[serde(remote = "Credentials")]
60struct MockCredentials {
61    pub id: String,
62    pub secret: Option<String>,
63}
64
65fn get_scopes() -> HashSet<String> {
66    HashSet::from(APP_SCOPES.map(|s| s.to_owned()))
67}
68
69fn scrape_from_remote() -> String {
70    OAuth::default().state
71}
72
73#[derive(Deserialize)]
74#[serde(remote = "OAuth")]
75struct MockOAuth {
76    pub redirect_uri: String,
77    #[serde(default = "get_scopes")]
78    pub scopes: HashSet<String>,
79    pub proxies: Option<String>,
80    #[serde(default = "scrape_from_remote", skip_serializing)]
81    pub state: String,
82}
83
84pub async fn get_client() -> Result<AuthCodeSpotify, Box<dyn std::error::Error>> {
85    let conf = Config {
86        token_cached: true,
87        token_refreshing: true,
88        ..Config::default()
89    };
90    let auth_conf = match Credentials::from_env() {
91        Some(c) => match OAuth::from_env(HashSet::from(APP_SCOPES.map(|s| s.to_owned()))) {
92            Some(o) => AuthConfig {
93                creds: c,
94                oauth: o,
95            },
96            None => read_configs()?,
97        },
98        None => read_configs()?, 
99    };
100    let spotify = AuthCodeSpotify::with_config(
101        auth_conf.creds,
102        auth_conf.oauth,
103        conf,
104    );
105    match spotify.read_token_cache(true).await {
106        Ok(t) => {
107            match t {
108                Some(t) => *spotify.get_token().lock().await.expect("cannot lock spotify token mutex") = Some(t),
109                None => login(&spotify).await?,
110            }
111            spotify.refresh_token().await?; 
112        }
113        Err(_) => login(&spotify).await?,
114    }
115    spotify.refresh_token().await?;
116    Ok(spotify)
117}
118
119pub async fn do_toggle(spotify: &AuthCodeSpotify) -> Result<(), Box<dyn std::error::Error>> {
120    match spotify.current_user_playing_item().await? {
121        Some(item) => match item.item {
122            Some(i) => match i {
123                PlayableItem::Track(t) => {
124                    let id = t.id.ok_or("Failed to get track id")?;
125                    if spotify.current_user_saved_tracks_contains(vec![id.clone()]).await?[0] {
126                        spotify.current_user_saved_tracks_delete(vec![id]).await?;
127                    } else {
128                        spotify.current_user_saved_tracks_add(vec![id]).await?;
129                    }
130                },
131                PlayableItem::Episode(e) => println!("Currently playing: {}", e.name),
132            }
133            None => println!("Nothing is currently playing."),
134        },
135        None => println!("Nothing is currently playing."),
136    }
137    Ok(())
138}
139