1mod kugou;
25pub mod lrc;
26mod migu;
27mod netease_v2;
28mod service;
29
30use crate::library_db::const_unknown::{UNKNOWN_ARTIST, UNKNOWN_TITLE};
31use crate::types::{DLMsg, Msg, SearchLyricState};
32use crate::utils::get_parent_folder;
33use anyhow::{anyhow, bail, Result};
34use lofty::config::WriteOptions;
35use lofty::id3::v2::{Frame, Id3v2Tag, UnsynchronizedTextFrame};
36use lofty::picture::Picture;
37use lofty::prelude::{Accessor, TagExt};
38use lofty::TextEncoding;
39use service::SongTagService;
40use std::path::{Path, PathBuf};
41use std::sync::mpsc::Sender;
42use std::sync::Arc;
43use std::thread::{self, sleep};
44use std::time::Duration;
45use ytd_rs::{Arg, YoutubeDL};
46
47#[derive(Debug, PartialEq)]
48pub struct SongTag {
49 service_provider: ServiceProvider,
50 song_id: String,
51 artist: Option<String>,
52 title: Option<String>,
53 album: Option<String>,
54 lang_ext: Option<String>,
55 lyric_id: Option<String>,
56 url: Option<UrlTypes>,
57 pic_id: Option<String>,
58 album_id: Option<String>,
59 }
61
62#[derive(Debug, PartialEq, Clone)]
64pub enum UrlTypes {
65 Protected,
67 AvailableRequiresFetching,
69 FreeDownloadable(String),
71}
72
73#[derive(Debug, PartialEq, Clone, Copy)]
74pub enum ServiceProvider {
75 Netease,
76 Kugou,
77 Migu,
78}
79
80impl std::fmt::Display for ServiceProvider {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 let service_provider = match self {
83 Self::Netease => "Netease",
84 Self::Kugou => "Kugou",
85 Self::Migu => "Migu",
86 };
87 write!(f, "{service_provider}")
88 }
89}
90
91pub async fn search(search_str: &str, tx_tageditor: Sender<SearchLyricState>) {
93 let mut results: Vec<SongTag> = Vec::new();
94
95 let handle_netease = async {
96 let neteasev2_api = netease_v2::Api::new();
97 neteasev2_api.search_recording(search_str, 0, 30).await
98 };
99
100 let handle_migu = async {
101 let migu_api = migu::Api::new();
102 migu_api.search_recording(search_str, 0, 30).await
103 };
104
105 let handle_kugou = async {
106 let kugou_api = kugou::Api::new();
107 kugou_api.search_recording(search_str, 0, 30).await
108 };
109
110 let (netease_res, migu_res, kugou_res) =
111 futures::join!(handle_netease, handle_migu, handle_kugou);
112
113 match netease_res {
114 Ok(vec) => results.extend(vec),
115 Err(err) => error!("Netease Error: {:#}", err),
116 }
117
118 match migu_res {
119 Ok(vec) => results.extend(vec),
120 Err(err) => error!("Migu Error: {:#}", err),
121 }
122
123 match kugou_res {
124 Ok(vec) => results.extend(vec),
125 Err(err) => error!("Kogou Error: {:#}", err),
126 }
127
128 tx_tageditor.send(SearchLyricState::Finish(results)).ok();
129}
130
131impl SongTag {
132 #[must_use]
133 pub fn artist(&self) -> Option<&str> {
134 self.artist.as_deref()
135 }
136
137 #[must_use]
138 pub fn album(&self) -> Option<&str> {
139 self.album.as_deref()
140 }
141
142 #[must_use]
145 pub fn title(&self) -> Option<&str> {
146 self.title.as_deref()
147 }
148
149 #[must_use]
150 pub fn lang_ext(&self) -> Option<&str> {
151 self.lang_ext.as_deref()
152 }
153
154 #[must_use]
155 pub const fn service_provider(&self) -> ServiceProvider {
156 self.service_provider
157 }
158
159 #[must_use]
160 pub const fn url(&self) -> Option<&UrlTypes> {
161 self.url.as_ref()
162 }
163
164 #[must_use]
165 pub fn id(&self) -> &str {
166 &self.song_id
167 }
168
169 pub async fn fetch_lyric(&self) -> Result<Option<String>> {
171 let lyric_string = match self.service_provider {
172 ServiceProvider::Kugou => {
173 let kugou_api = kugou::Api::new();
174 kugou_api.get_lyrics(self).await.map_err(|v| anyhow!(v))?
175 }
176 ServiceProvider::Netease => {
177 let neteasev2_api = netease_v2::Api::new();
178 neteasev2_api
179 .get_lyrics(self)
180 .await
181 .map_err(|v| anyhow!(v))?
182 }
183 ServiceProvider::Migu => {
184 let migu_api = migu::Api::new();
185 migu_api.get_lyrics(self).await.map_err(|v| anyhow!(v))?
186 }
187 };
188
189 Ok(Some(lyric_string))
190 }
191
192 pub async fn fetch_photo(&self) -> Result<Picture> {
195 match self.service_provider {
196 ServiceProvider::Kugou => {
197 let kugou_api = kugou::Api::new();
198 Ok(kugou_api.get_picture(self).await.map_err(|v| anyhow!(v))?)
199 }
200 ServiceProvider::Netease => {
201 let neteasev2_api = netease_v2::Api::new();
202 Ok(neteasev2_api
203 .get_picture(self)
204 .await
205 .map_err(|v| anyhow!(v))?)
206 }
207 ServiceProvider::Migu => {
208 let migu_api = migu::Api::new();
209 Ok(migu_api.get_picture(self).await.map_err(|v| anyhow!(v))?)
210 }
211 }
212 }
213
214 #[allow(clippy::too_many_lines)]
215 pub async fn download(&self, file: &str, tx_tageditor: &Sender<Msg>) -> Result<()> {
216 let p_parent = get_parent_folder(Path::new(file));
217 let artist = self
218 .artist
219 .clone()
220 .unwrap_or_else(|| UNKNOWN_ARTIST.to_string());
221 let title = self
222 .title
223 .clone()
224 .unwrap_or_else(|| UNKNOWN_TITLE.to_string());
225
226 let album = self.album.clone().unwrap_or_else(|| String::from("N/A"));
227 let lyric = self.fetch_lyric().await;
228 let photo = self.fetch_photo().await;
229
230 let filename = format!("{artist}-{title}.%(ext)s");
231
232 let args = vec![
233 Arg::new("--quiet"),
234 Arg::new_with_arg("--output", filename.as_ref()),
235 Arg::new("--extract-audio"),
236 Arg::new_with_arg("--audio-format", "mp3"),
237 ];
238
239 let p_full = p_parent.join(format!("{artist}-{title}.mp3"));
240 match std::fs::remove_file(&p_full) {
241 Err(err) if err.kind() == std::io::ErrorKind::NotFound => (),
242 v => v?,
243 }
244
245 if self.url().is_some_and(|v| *v == UrlTypes::Protected) {
246 bail!("The item is protected by copyright, please, select another one.");
247 }
248
249 let mut url = if let Some(UrlTypes::FreeDownloadable(url)) = &self.url {
250 url.clone()
251 } else {
252 String::new()
253 };
254
255 match self.service_provider {
256 ServiceProvider::Netease => {
257 let neteasev2_api = netease_v2::Api::new();
258 url = neteasev2_api
259 .download_recording(self)
260 .await
261 .map_err(|v| anyhow!(v))?;
262 }
263 ServiceProvider::Migu => {}
264 ServiceProvider::Kugou => {
265 let kugou_api = kugou::Api::new();
266 url = kugou_api
267 .download_recording(self)
268 .await
269 .map_err(|v| anyhow!(v))?;
270 }
271 }
272
273 if url.is_empty() {
274 bail!("failed to fetch url, please, try another item.");
275 }
276
277 let url = Arc::from(url.into_boxed_str());
279
280 let ytd = YoutubeDL::new(&PathBuf::from(p_parent), args, &url)?;
281
282 let tx = tx_tageditor.clone();
283 thread::spawn(move || -> Result<()> {
284 tx.send(Msg::Download(DLMsg::DownloadRunning(
285 url.clone(),
286 title.clone(),
287 )))
288 .ok();
289
290 let _download_result = match ytd.download() {
293 Ok(res) => res,
294 Err(err) => {
295 tx.send(Msg::Download(DLMsg::DownloadErrDownload(
296 url.clone(),
297 title.clone(),
298 err.to_string(),
299 )))
300 .ok();
301 sleep(Duration::from_secs(1));
302 tx.send(Msg::Download(DLMsg::DownloadCompleted(url.clone(), None)))
303 .ok();
304
305 return Ok(());
306 }
307 };
308
309 tx.send(Msg::Download(DLMsg::DownloadSuccess(url.clone())))
310 .ok();
311 let mut tag = Id3v2Tag::default();
312
313 tag.set_title(title.clone());
314 tag.set_artist(artist);
315 tag.set_album(album);
316
317 if let Ok(Some(l)) = lyric {
318 let frame = Frame::UnsynchronizedText(UnsynchronizedTextFrame::new(
319 TextEncoding::UTF8,
320 *b"eng",
321 String::from("saved by termusic"),
322 l,
323 ));
324 tag.insert(frame);
325 }
326
327 if let Ok(picture) = photo {
328 tag.insert_picture(picture);
329 }
330
331 if tag.save_to_path(&p_full, WriteOptions::new()).is_ok() {
332 sleep(Duration::from_secs(1));
333 tx.send(Msg::Download(DLMsg::DownloadCompleted(
334 url,
335 Some(p_full.to_string_lossy().to_string()),
336 )))
337 .ok();
338 } else {
339 tx.send(Msg::Download(DLMsg::DownloadErrEmbedData(
340 url.clone(),
341 title,
342 )))
343 .ok();
344 sleep(Duration::from_secs(1));
345 tx.send(Msg::Download(DLMsg::DownloadCompleted(url, None)))
346 .ok();
347 }
348
349 Ok(())
350 });
351 Ok(())
352 }
353}