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