termusiclib/songtag/
mod.rs

1/*
2 * MIT License
3 *
4 * termusic - Copyright (c) 2021 Larry Hao
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in all
14 * copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 * SOFTWARE.
23 */
24mod 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    // genre: Option<String>,
60}
61
62/// Indicate in which way the song can be downloaded, if at all.
63#[derive(Debug, PartialEq, Clone)]
64pub enum UrlTypes {
65    /// Download is protected by DRM or a fee, something which we dont do here
66    Protected,
67    /// Download is freely available, but requires extra fetching (`Api::song_url()`)
68    AvailableRequiresFetching,
69    /// Url is freely available to be downloaded
70    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
91// Search function of 3 servers. Run in parallel to get results faster.
92pub 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    /// Optionally return the title of the song
143    /// If `None` it wasn't able to read the tags
144    #[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    // get lyric by lyric_id
170    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    /// Fetch a picture for the current song
193    /// For kugou & netease `pic_id()` or for migu `song_id` is used
194    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        // avoid full string clones when sending via a channel
278        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            // start download
291            // check what the result is and print out the path to the download or the error
292            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}