1use anyhow::bail;
2use reqwest::blocking::Client as HttpClient;
3use serde::Deserialize;
4
5use crate::domain::search::{SearchItem, SearchResults, SearchType};
6use crate::error::Result;
7use crate::spotify::auth::AuthService;
8use crate::spotify::base::api_base;
9use crate::spotify::error::format_api_error;
10
11
12#[derive(Debug, Clone)]
14pub struct SearchClient {
15 http: HttpClient,
16 auth: AuthService,
17}
18
19impl SearchClient {
20 pub fn new(http: HttpClient, auth: AuthService) -> Self {
21 Self { http, auth }
22 }
23
24 pub fn search(
25 &self,
26 query: &str,
27 kind: SearchType,
28 limit: u32,
29 market_from_token: bool,
30 ) -> Result<SearchResults> {
31 if kind == SearchType::All {
32 let mut items = Vec::new();
33 let kinds = [
34 SearchType::Track,
35 SearchType::Album,
36 SearchType::Artist,
37 SearchType::Playlist,
38 ];
39 for kind in kinds {
40 let results = self.search(query, kind, limit, market_from_token)?;
41 items.extend(results.items);
42 }
43 return Ok(SearchResults {
44 kind: SearchType::All,
45 items,
46 });
47 }
48
49 let token = self.auth.token()?;
50 let kind_param = search_type_param(kind);
51 let mut url = format!(
52 "{}/search?q={}&type={}&limit={}",
53 api_base(),
54 urlencoding::encode(query),
55 kind_param,
56 limit
57 );
58
59 if market_from_token {
60 url.push_str("&market=from_token");
61 }
62
63 let response = self
64 .http
65 .get(url)
66 .bearer_auth(token.access_token)
67 .send()?;
68
69 if !response.status().is_success() {
70 let status = response.status();
71 let body = response.text().unwrap_or_else(|_| "<no body>".to_string());
72 bail!(format_api_error("spotify search failed", status, &body));
73 }
74
75 let payload: SearchResponse = response.json()?;
76 let items = match kind {
77 SearchType::Track => payload
78 .tracks
79 .map(|list| {
80 list.items
81 .into_iter()
82 .filter_map(|item| item)
83 .map(|item| SearchItem {
84 id: item.id,
85 name: item.name,
86 uri: item.uri,
87 kind: SearchType::Track,
88 artists: item.artists.into_iter().map(|artist| artist.name).collect(),
89 owner: None,
90 score: None,
91 })
92 .collect::<Vec<_>>()
93 })
94 .unwrap_or_default(),
95 SearchType::Album => payload
96 .albums
97 .map(|list| {
98 list.items
99 .into_iter()
100 .filter_map(|item| item)
101 .map(|item| SearchItem {
102 id: item.id,
103 name: item.name,
104 uri: item.uri,
105 kind: SearchType::Album,
106 artists: item.artists.into_iter().map(|artist| artist.name).collect(),
107 owner: None,
108 score: None,
109 })
110 .collect::<Vec<_>>()
111 })
112 .unwrap_or_default(),
113 SearchType::Artist => payload
114 .artists
115 .map(|list| {
116 list.items
117 .into_iter()
118 .filter_map(|item| item)
119 .map(|item| SearchItem {
120 id: item.id,
121 name: item.name,
122 uri: item.uri,
123 kind: SearchType::Artist,
124 artists: Vec::new(),
125 owner: None,
126 score: None,
127 })
128 .collect::<Vec<_>>()
129 })
130 .unwrap_or_default(),
131 SearchType::Playlist => payload
132 .playlists
133 .map(|list| {
134 list.items
135 .into_iter()
136 .filter_map(|item| item)
137 .map(|item| SearchItem {
138 id: item.id,
139 name: item.name,
140 uri: item.uri,
141 kind: SearchType::Playlist,
142 artists: Vec::new(),
143 owner: item.owner.and_then(|owner| owner.display_name),
144 score: None,
145 })
146 .collect::<Vec<_>>()
147 })
148 .unwrap_or_default(),
149 SearchType::All => Vec::new(),
150 };
151
152 Ok(SearchResults { kind, items })
153 }
154}
155
156fn search_type_param(kind: SearchType) -> &'static str {
157 match kind {
158 SearchType::All => "track,album,artist,playlist",
159 SearchType::Track => "track",
160 SearchType::Album => "album",
161 SearchType::Artist => "artist",
162 SearchType::Playlist => "playlist",
163 }
164}
165
166#[derive(Debug, Deserialize)]
167struct SearchResponse {
168 tracks: Option<ItemList<SpotifyTrack>>,
169 albums: Option<ItemList<SpotifyAlbum>>,
170 artists: Option<ItemList<SpotifyArtist>>,
171 playlists: Option<ItemList<SpotifyPlaylist>>,
172}
173
174#[derive(Debug, Deserialize)]
175struct ItemList<T> {
176 items: Vec<Option<T>>,
177}
178
179#[derive(Debug, Deserialize)]
180struct SpotifyTrack {
181 id: String,
182 name: String,
183 uri: String,
184 artists: Vec<SpotifyArtistRef>,
185}
186
187#[derive(Debug, Deserialize)]
188struct SpotifyAlbum {
189 id: String,
190 name: String,
191 uri: String,
192 artists: Vec<SpotifyArtistRef>,
193}
194
195#[derive(Debug, Deserialize)]
196struct SpotifyArtist {
197 id: String,
198 name: String,
199 uri: String,
200}
201
202#[derive(Debug, Deserialize)]
203struct SpotifyPlaylist {
204 id: String,
205 name: String,
206 uri: String,
207 owner: Option<SpotifyOwner>,
208}
209
210#[derive(Debug, Deserialize)]
211struct SpotifyArtistRef {
212 name: String,
213}
214
215#[derive(Debug, Deserialize)]
216struct SpotifyOwner {
217 display_name: Option<String>,
218}