1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use anyhow::{Result, anyhow, bail};
5use lofty::TextEncoding;
6use lofty::config::WriteOptions;
7use lofty::id3::v2::{Frame, Id3v2Tag, UnsynchronizedTextFrame};
8use lofty::picture::Picture;
9use lofty::prelude::{Accessor, TagExt};
10use service::SongTagService;
11use ytd_rs::{Arg, YoutubeDL};
12
13use crate::common::const_unknown::{UNKNOWN_ARTIST, UNKNOWN_TITLE};
14use crate::utils::get_parent_folder;
15
16mod kugou;
17pub mod lrc;
18mod migu;
19mod netease_v2;
20mod service;
21
22#[derive(Debug, PartialEq, Eq, Clone)]
23pub struct SongTag {
24 service_provider: ServiceProvider,
25 song_id: String,
26 artist: Option<String>,
27 title: Option<String>,
28 album: Option<String>,
29 lang_ext: Option<String>,
30 lyric_id: Option<String>,
31 url: Option<UrlTypes>,
32 pic_id: Option<String>,
33 album_id: Option<String>,
34 }
36
37#[derive(Debug, PartialEq, Eq, Clone)]
39pub enum UrlTypes {
40 Protected,
42 AvailableRequiresFetching,
44 FreeDownloadable(String),
46}
47
48#[derive(Debug, PartialEq, Eq, Clone, Copy)]
49pub enum ServiceProvider {
50 Netease,
51 Kugou,
52 Migu,
53}
54
55impl std::fmt::Display for ServiceProvider {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 let service_provider = match self {
58 Self::Netease => "Netease",
59 Self::Kugou => "Kugou",
60 Self::Migu => "Migu",
61 };
62 write!(f, "{service_provider}")
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum SongtagSearchResult {
69 Finish(Vec<SongTag>),
70}
71
72pub async fn search(search_str: &str, tx_done: impl Fn(SongtagSearchResult) + Send + 'static) {
74 let mut results: Vec<SongTag> = Vec::new();
75
76 let handle_netease = async {
77 let neteasev2_api = netease_v2::Api::new();
78 neteasev2_api.search_recording(search_str, 0, 30).await
79 };
80
81 let handle_migu = async {
82 let migu_api = migu::Api::new();
83 migu_api.search_recording(search_str, 0, 30).await
84 };
85
86 let handle_kugou = async {
87 let kugou_api = kugou::Api::new();
88 kugou_api.search_recording(search_str, 0, 30).await
89 };
90
91 let (netease_res, migu_res, kugou_res) =
92 futures_util::join!(handle_netease, handle_migu, handle_kugou);
93
94 match netease_res {
95 Ok(vec) => results.extend(vec),
96 Err(err) => error!("Netease Error: {err:#}"),
97 }
98
99 match migu_res {
100 Ok(vec) => results.extend(vec),
101 Err(err) => error!("Migu Error: {err:#}"),
102 }
103
104 match kugou_res {
105 Ok(vec) => results.extend(vec),
106 Err(err) => error!("Kogou Error: {err:#}"),
107 }
108
109 tx_done(SongtagSearchResult::Finish(results));
110}
111
112pub type TrackDLMsgURL = Arc<str>;
113
114#[derive(Clone, PartialEq, Eq, Debug)]
115pub enum TrackDLMsg {
116 Start(TrackDLMsgURL, String),
120 Success(TrackDLMsgURL),
124 Completed(TrackDLMsgURL, Option<String>),
128 Err(TrackDLMsgURL, String, String),
132 ErrEmbedData(TrackDLMsgURL, String),
137}
138
139impl SongTag {
140 #[must_use]
141 pub fn artist(&self) -> Option<&str> {
142 self.artist.as_deref()
143 }
144
145 #[must_use]
146 pub fn album(&self) -> Option<&str> {
147 self.album.as_deref()
148 }
149
150 #[must_use]
153 pub fn title(&self) -> Option<&str> {
154 self.title.as_deref()
155 }
156
157 #[must_use]
158 pub fn lang_ext(&self) -> Option<&str> {
159 self.lang_ext.as_deref()
160 }
161
162 #[must_use]
163 pub const fn service_provider(&self) -> ServiceProvider {
164 self.service_provider
165 }
166
167 #[must_use]
168 pub const fn url(&self) -> Option<&UrlTypes> {
169 self.url.as_ref()
170 }
171
172 #[must_use]
173 pub fn id(&self) -> &str {
174 &self.song_id
175 }
176
177 pub async fn fetch_lyric(&self) -> Result<Option<String>> {
179 let lyric_string = match self.service_provider {
180 ServiceProvider::Kugou => {
181 let kugou_api = kugou::Api::new();
182 kugou_api.get_lyrics(self).await.map_err(|v| anyhow!(v))?
183 }
184 ServiceProvider::Netease => {
185 let neteasev2_api = netease_v2::Api::new();
186 neteasev2_api
187 .get_lyrics(self)
188 .await
189 .map_err(|v| anyhow!(v))?
190 }
191 ServiceProvider::Migu => {
192 let migu_api = migu::Api::new();
193 migu_api.get_lyrics(self).await.map_err(|v| anyhow!(v))?
194 }
195 };
196
197 Ok(Some(lyric_string))
198 }
199
200 pub async fn fetch_photo(&self) -> Result<Picture> {
203 match self.service_provider {
204 ServiceProvider::Kugou => {
205 let kugou_api = kugou::Api::new();
206 Ok(kugou_api.get_picture(self).await.map_err(|v| anyhow!(v))?)
207 }
208 ServiceProvider::Netease => {
209 let neteasev2_api = netease_v2::Api::new();
210 Ok(neteasev2_api
211 .get_picture(self)
212 .await
213 .map_err(|v| anyhow!(v))?)
214 }
215 ServiceProvider::Migu => {
216 let migu_api = migu::Api::new();
217 Ok(migu_api.get_picture(self).await.map_err(|v| anyhow!(v))?)
218 }
219 }
220 }
221
222 pub async fn download(
226 &self,
227 file: &Path,
228 tx: impl Fn(TrackDLMsg) + Send + 'static,
229 ) -> Result<()> {
230 if self.url().is_some_and(|v| *v == UrlTypes::Protected) {
231 bail!("The item is protected by copyright, please select another one.");
232 }
233
234 let artist = self
235 .artist
236 .clone()
237 .unwrap_or_else(|| UNKNOWN_ARTIST.to_string());
238 let title = self
239 .title
240 .clone()
241 .unwrap_or_else(|| UNKNOWN_TITLE.to_string());
242
243 let album = self.album.clone().unwrap_or_else(|| String::from("N/A"));
244 let lyric = self
245 .fetch_lyric()
246 .await
247 .map_err(|err| {
248 warn!("Fetching Lyric failed: {err:#?}");
249 })
250 .ok()
251 .flatten();
252 let photo = self
253 .fetch_photo()
254 .await
255 .map_err(|err| {
256 warn!("Fetching Photo failed: {err:#?}");
257 })
258 .ok();
259
260 let p_parent = get_parent_folder(file);
261 let out_path = p_parent.join(format!("{artist}-{title}.mp3"));
262 match std::fs::remove_file(&out_path) {
263 Err(err) if err.kind() == std::io::ErrorKind::NotFound => (),
264 v => v?,
265 }
266
267 let mut url = if let Some(UrlTypes::FreeDownloadable(url)) = &self.url {
268 url.clone()
269 } else {
270 String::new()
271 };
272
273 match self.service_provider {
274 ServiceProvider::Netease => {
275 let neteasev2_api = netease_v2::Api::new();
276 url = neteasev2_api
277 .download_recording(self)
278 .await
279 .map_err(|v| anyhow!(v))?;
280 }
281 ServiceProvider::Migu => {}
282 ServiceProvider::Kugou => {
283 let kugou_api = kugou::Api::new();
284 url = kugou_api
285 .download_recording(self)
286 .await
287 .map_err(|v| anyhow!(v))?;
288 }
289 }
290
291 if url.is_empty() {
292 bail!("failed to fetch url, please, try another item.");
293 }
294
295 let filename = format!("{artist}-{title}.%(ext)s");
296
297 let mut tag = Id3v2Tag::default();
298
299 tag.set_title(title.clone());
300 tag.set_artist(artist);
301 tag.set_album(album);
302
303 if let Some(l) = lyric {
304 let frame = Frame::UnsynchronizedText(UnsynchronizedTextFrame::new(
305 TextEncoding::UTF8,
306 *b"eng",
307 String::from("saved by termusic"),
308 l,
309 ));
310 tag.insert(frame);
311 }
312
313 if let Some(picture) = photo {
314 tag.insert_picture(picture);
315 }
316
317 let args = vec![
318 Arg::new("--quiet"),
319 Arg::new_with_arg("--output", filename.as_ref()),
320 Arg::new("--extract-audio"),
321 Arg::new_with_arg("--audio-format", "mp3"),
322 ];
323
324 let url = Arc::from(url.into_boxed_str());
326
327 let ytd = YoutubeDL::new(&PathBuf::from(p_parent), args, &url)?;
328
329 let jh = tokio::task::spawn_blocking(move || {
330 Self::download_ytd(url, &out_path, title, &tag, &ytd, tx);
331 });
332 drop(jh);
333
334 Ok(())
335 }
336
337 fn download_ytd(
341 url: Arc<str>,
342 out_path: &Path,
343 title: String,
344 tag: &Id3v2Tag,
345 ytd: &YoutubeDL,
346 tx: impl Fn(TrackDLMsg),
347 ) {
348 tx(TrackDLMsg::Start(url.clone(), title.clone()));
349
350 if let Err(err) = ytd.download() {
352 tx(TrackDLMsg::Err(url.clone(), title.clone(), err.to_string()));
353
354 return;
355 }
356
357 tx(TrackDLMsg::Success(url.clone()));
358
359 if tag.save_to_path(out_path, WriteOptions::new()).is_ok() {
360 tx(TrackDLMsg::Completed(
361 url,
362 Some(out_path.to_string_lossy().to_string()),
363 ));
364 } else {
365 tx(TrackDLMsg::ErrEmbedData(url.clone(), title));
366 tx(TrackDLMsg::Completed(url, None));
367 }
368 }
369}