1use std::path::{Path, PathBuf};
4use std::sync::Arc;
5use std::thread::{self, sleep};
6use std::time::Duration;
7
8use anyhow::{anyhow, bail, Result};
9use lofty::config::WriteOptions;
10use lofty::id3::v2::{Frame, Id3v2Tag, UnsynchronizedTextFrame};
11use lofty::picture::Picture;
12use lofty::prelude::{Accessor, TagExt};
13use lofty::TextEncoding;
14use service::SongTagService;
15use tokio::sync::mpsc::UnboundedSender;
16use ytd_rs::{Arg, YoutubeDL};
17
18use crate::library_db::const_unknown::{UNKNOWN_ARTIST, UNKNOWN_TITLE};
19use crate::types::{DLMsg, Msg, SongTagRecordingResult, TEMsg};
20use crate::utils::get_parent_folder;
21
22mod kugou;
23pub mod lrc;
24mod migu;
25mod netease_v2;
26mod service;
27
28#[derive(Debug, PartialEq, Eq, Clone)]
29pub struct SongTag {
30 service_provider: ServiceProvider,
31 song_id: String,
32 artist: Option<String>,
33 title: Option<String>,
34 album: Option<String>,
35 lang_ext: Option<String>,
36 lyric_id: Option<String>,
37 url: Option<UrlTypes>,
38 pic_id: Option<String>,
39 album_id: Option<String>,
40 }
42
43#[derive(Debug, PartialEq, Eq, Clone)]
45pub enum UrlTypes {
46 Protected,
48 AvailableRequiresFetching,
50 FreeDownloadable(String),
52}
53
54#[derive(Debug, PartialEq, Eq, Clone, Copy)]
55pub enum ServiceProvider {
56 Netease,
57 Kugou,
58 Migu,
59}
60
61impl std::fmt::Display for ServiceProvider {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 let service_provider = match self {
64 Self::Netease => "Netease",
65 Self::Kugou => "Kugou",
66 Self::Migu => "Migu",
67 };
68 write!(f, "{service_provider}")
69 }
70}
71
72pub async fn search(search_str: &str, tx_done: UnboundedSender<Msg>) {
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 let _ = tx_done.send(Msg::TagEditor(TEMsg::TESearchLyricResult(
110 SongTagRecordingResult::Finish(results),
111 )));
112}
113
114impl SongTag {
115 #[must_use]
116 pub fn artist(&self) -> Option<&str> {
117 self.artist.as_deref()
118 }
119
120 #[must_use]
121 pub fn album(&self) -> Option<&str> {
122 self.album.as_deref()
123 }
124
125 #[must_use]
128 pub fn title(&self) -> Option<&str> {
129 self.title.as_deref()
130 }
131
132 #[must_use]
133 pub fn lang_ext(&self) -> Option<&str> {
134 self.lang_ext.as_deref()
135 }
136
137 #[must_use]
138 pub const fn service_provider(&self) -> ServiceProvider {
139 self.service_provider
140 }
141
142 #[must_use]
143 pub const fn url(&self) -> Option<&UrlTypes> {
144 self.url.as_ref()
145 }
146
147 #[must_use]
148 pub fn id(&self) -> &str {
149 &self.song_id
150 }
151
152 pub async fn fetch_lyric(&self) -> Result<Option<String>> {
154 let lyric_string = match self.service_provider {
155 ServiceProvider::Kugou => {
156 let kugou_api = kugou::Api::new();
157 kugou_api.get_lyrics(self).await.map_err(|v| anyhow!(v))?
158 }
159 ServiceProvider::Netease => {
160 let neteasev2_api = netease_v2::Api::new();
161 neteasev2_api
162 .get_lyrics(self)
163 .await
164 .map_err(|v| anyhow!(v))?
165 }
166 ServiceProvider::Migu => {
167 let migu_api = migu::Api::new();
168 migu_api.get_lyrics(self).await.map_err(|v| anyhow!(v))?
169 }
170 };
171
172 Ok(Some(lyric_string))
173 }
174
175 pub async fn fetch_photo(&self) -> Result<Picture> {
178 match self.service_provider {
179 ServiceProvider::Kugou => {
180 let kugou_api = kugou::Api::new();
181 Ok(kugou_api.get_picture(self).await.map_err(|v| anyhow!(v))?)
182 }
183 ServiceProvider::Netease => {
184 let neteasev2_api = netease_v2::Api::new();
185 Ok(neteasev2_api
186 .get_picture(self)
187 .await
188 .map_err(|v| anyhow!(v))?)
189 }
190 ServiceProvider::Migu => {
191 let migu_api = migu::Api::new();
192 Ok(migu_api.get_picture(self).await.map_err(|v| anyhow!(v))?)
193 }
194 }
195 }
196
197 #[allow(clippy::too_many_lines)]
198 pub async fn download(&self, file: &Path, tx: UnboundedSender<Msg>) -> Result<()> {
199 let p_parent = get_parent_folder(file);
200 let artist = self
201 .artist
202 .clone()
203 .unwrap_or_else(|| UNKNOWN_ARTIST.to_string());
204 let title = self
205 .title
206 .clone()
207 .unwrap_or_else(|| UNKNOWN_TITLE.to_string());
208
209 let album = self.album.clone().unwrap_or_else(|| String::from("N/A"));
210 let lyric = self.fetch_lyric().await;
211 let photo = self.fetch_photo().await;
212
213 let filename = format!("{artist}-{title}.%(ext)s");
214
215 let args = vec![
216 Arg::new("--quiet"),
217 Arg::new_with_arg("--output", filename.as_ref()),
218 Arg::new("--extract-audio"),
219 Arg::new_with_arg("--audio-format", "mp3"),
220 ];
221
222 let p_full = p_parent.join(format!("{artist}-{title}.mp3"));
223 match std::fs::remove_file(&p_full) {
224 Err(err) if err.kind() == std::io::ErrorKind::NotFound => (),
225 v => v?,
226 }
227
228 if self.url().is_some_and(|v| *v == UrlTypes::Protected) {
229 bail!("The item is protected by copyright, please, select another one.");
230 }
231
232 let mut url = if let Some(UrlTypes::FreeDownloadable(url)) = &self.url {
233 url.clone()
234 } else {
235 String::new()
236 };
237
238 match self.service_provider {
239 ServiceProvider::Netease => {
240 let neteasev2_api = netease_v2::Api::new();
241 url = neteasev2_api
242 .download_recording(self)
243 .await
244 .map_err(|v| anyhow!(v))?;
245 }
246 ServiceProvider::Migu => {}
247 ServiceProvider::Kugou => {
248 let kugou_api = kugou::Api::new();
249 url = kugou_api
250 .download_recording(self)
251 .await
252 .map_err(|v| anyhow!(v))?;
253 }
254 }
255
256 if url.is_empty() {
257 bail!("failed to fetch url, please, try another item.");
258 }
259
260 let url = Arc::from(url.into_boxed_str());
262
263 let ytd = YoutubeDL::new(&PathBuf::from(p_parent), args, &url)?;
264
265 thread::spawn(move || -> Result<()> {
266 tx.send(Msg::Download(DLMsg::DownloadRunning(
267 url.clone(),
268 title.clone(),
269 )))
270 .ok();
271
272 let _download_result = match ytd.download() {
275 Ok(res) => res,
276 Err(err) => {
277 tx.send(Msg::Download(DLMsg::DownloadErrDownload(
278 url.clone(),
279 title.clone(),
280 err.to_string(),
281 )))
282 .ok();
283 sleep(Duration::from_secs(1));
284 tx.send(Msg::Download(DLMsg::DownloadCompleted(url.clone(), None)))
285 .ok();
286
287 return Ok(());
288 }
289 };
290
291 tx.send(Msg::Download(DLMsg::DownloadSuccess(url.clone())))
292 .ok();
293 let mut tag = Id3v2Tag::default();
294
295 tag.set_title(title.clone());
296 tag.set_artist(artist);
297 tag.set_album(album);
298
299 if let Ok(Some(l)) = lyric {
300 let frame = Frame::UnsynchronizedText(UnsynchronizedTextFrame::new(
301 TextEncoding::UTF8,
302 *b"eng",
303 String::from("saved by termusic"),
304 l,
305 ));
306 tag.insert(frame);
307 }
308
309 if let Ok(picture) = photo {
310 tag.insert_picture(picture);
311 }
312
313 if tag.save_to_path(&p_full, WriteOptions::new()).is_ok() {
314 sleep(Duration::from_secs(1));
315 tx.send(Msg::Download(DLMsg::DownloadCompleted(
316 url,
317 Some(p_full.to_string_lossy().to_string()),
318 )))
319 .ok();
320 } else {
321 tx.send(Msg::Download(DLMsg::DownloadErrEmbedData(
322 url.clone(),
323 title,
324 )))
325 .ok();
326 sleep(Duration::from_secs(1));
327 tx.send(Msg::Download(DLMsg::DownloadCompleted(url, None)))
328 .ok();
329 }
330
331 Ok(())
332 });
333 Ok(())
334 }
335}