spotify_cli/cli/commands/
mod.rs1#[macro_use]
2mod macros;
3
4mod albums;
5mod audiobooks;
6mod auth;
7mod categories;
8mod chapters;
9#[cfg(unix)]
10mod daemon;
11mod episodes;
12mod follow;
13mod library;
14mod markets;
15pub(crate) mod now_playing;
16mod pin;
17mod player;
18mod playlist;
19mod resource;
20mod search;
21mod shows;
22mod user;
23
24pub use albums::*;
25pub use audiobooks::*;
26pub use auth::*;
27pub use categories::*;
28pub use chapters::*;
29#[cfg(unix)]
30pub use daemon::*;
31pub use episodes::*;
32pub use follow::*;
33pub use library::*;
34pub use markets::*;
35pub use pin::*;
36pub use player::*;
37pub use playlist::*;
38pub use resource::*;
39pub use search::*;
40pub use shows::*;
41pub use user::*;
42
43#[derive(Default)]
45pub struct SearchFilters {
46 pub artist: Option<String>,
47 pub album: Option<String>,
48 pub track: Option<String>,
49 pub year: Option<String>,
50 pub genre: Option<String>,
51 pub isrc: Option<String>,
52 pub upc: Option<String>,
53 pub new: bool,
54 pub hipster: bool,
55}
56
57impl SearchFilters {
58 pub fn build_query(&self, base_query: &str) -> String {
60 let mut parts: Vec<String> = Vec::new();
61
62 if !base_query.is_empty() {
63 parts.push(base_query.to_string());
64 }
65
66 if let Some(ref artist) = self.artist {
67 parts.push(format!("artist:{}", artist));
68 }
69 if let Some(ref album) = self.album {
70 parts.push(format!("album:{}", album));
71 }
72 if let Some(ref track) = self.track {
73 parts.push(format!("track:{}", track));
74 }
75 if let Some(ref year) = self.year {
76 parts.push(format!("year:{}", year));
77 }
78 if let Some(ref genre) = self.genre {
79 parts.push(format!("genre:{}", genre));
80 }
81 if let Some(ref isrc) = self.isrc {
82 parts.push(format!("isrc:{}", isrc));
83 }
84 if let Some(ref upc) = self.upc {
85 parts.push(format!("upc:{}", upc));
86 }
87 if self.new {
88 parts.push("tag:new".to_string());
89 }
90 if self.hipster {
91 parts.push("tag:hipster".to_string());
92 }
93
94 parts.join(" ")
95 }
96
97 pub fn has_filters(&self) -> bool {
99 self.artist.is_some()
100 || self.album.is_some()
101 || self.track.is_some()
102 || self.year.is_some()
103 || self.genre.is_some()
104 || self.isrc.is_some()
105 || self.upc.is_some()
106 || self.new
107 || self.hipster
108 }
109}
110
111use std::future::Future;
112
113use crate::http::api::SpotifyApi;
114use crate::io::output::{ErrorKind, Response};
115use crate::oauth::flow::OAuthFlow;
116use crate::storage::config::Config;
117use crate::storage::pins::{Pin, PinStore};
118use crate::storage::token_store::TokenStore;
119use tracing::info;
120
121pub(crate) fn init_token_store() -> Result<TokenStore, Response> {
123 TokenStore::new().map_err(|e| {
124 Response::err_with_details(
125 500,
126 "Failed to initialize token store",
127 ErrorKind::Storage,
128 e.to_string(),
129 )
130 })
131}
132
133pub(crate) fn init_pin_store() -> Result<PinStore, Response> {
135 PinStore::new().map_err(|e| {
136 Response::err_with_details(
137 500,
138 "Failed to load pin store",
139 ErrorKind::Storage,
140 e.to_string(),
141 )
142 })
143}
144
145pub(crate) fn load_config() -> Result<Config, Response> {
147 Config::load().map_err(|e| {
148 Response::err_with_details(
149 500,
150 "Failed to load config",
151 ErrorKind::Config,
152 e.to_string(),
153 )
154 })
155}
156
157pub(crate) async fn get_authenticated_client() -> Result<SpotifyApi, Response> {
160 let token_store = init_token_store()?;
161
162 let token = token_store.load().map_err(|_| {
163 Response::err(
164 401,
165 "Not logged in. Run: spotify-cli auth login",
166 ErrorKind::Auth,
167 )
168 })?;
169
170 if token.is_expired() {
171 let refresh_token = token.refresh_token.as_ref().ok_or_else(|| {
173 Response::err(
174 401,
175 "Token expired and no refresh token. Run: spotify-cli auth login",
176 ErrorKind::Auth,
177 )
178 })?;
179
180 let config = load_config()?;
181 let flow = OAuthFlow::new(config.client_id().to_string());
182
183 match flow.refresh(refresh_token).await {
184 Ok(new_token) => {
185 info!("Auto-refreshed expired token");
186 if let Err(e) = token_store.save(&new_token) {
187 return Err(Response::err_with_details(
188 500,
189 "Failed to save refreshed token",
190 ErrorKind::Storage,
191 e.to_string(),
192 ));
193 }
194 return Ok(SpotifyApi::new(new_token.access_token));
195 }
196 Err(_) => {
197 return Err(Response::err(
198 401,
199 "Token expired and refresh failed. Run: spotify-cli auth login",
200 ErrorKind::Auth,
201 ));
202 }
203 }
204 }
205
206 Ok(SpotifyApi::new(token.access_token))
207}
208
209pub(crate) fn extract_id(input: &str) -> String {
211 Pin::extract_id(input)
212}
213
214pub(crate) async fn with_client<F, Fut>(f: F) -> Response
216where
217 F: FnOnce(SpotifyApi) -> Fut,
218 Fut: Future<Output = Response>,
219{
220 match get_authenticated_client().await {
221 Ok(client) => f(client).await,
222 Err(e) => e,
223 }
224}