mirror of
https://github.com/arabianq/lyrics_fetcher.git
synced 2026-04-27 22:11:22 +00:00
use lofty to support formats different from mp3
This commit is contained in:
Generated
+48
-1
@@ -355,6 +355,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.1.1"
|
||||
@@ -836,7 +842,6 @@ dependencies = [
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"flate2",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -984,6 +989,32 @@ dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lofty"
|
||||
version = "0.23.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d0c107dba5af049cf1c36b646fc1ba0cd2705f40d766d2c4c64f1b797c5fbed"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"data-encoding",
|
||||
"flate2",
|
||||
"lofty_attr",
|
||||
"log",
|
||||
"ogg_pager",
|
||||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lofty_attr"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "458ace39169e4b83c4f77ae3d42d5d1d11c422feef590219a97c973d3b524557"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
@@ -1008,6 +1039,7 @@ dependencies = [
|
||||
"futures",
|
||||
"id3",
|
||||
"itertools",
|
||||
"lofty",
|
||||
"reqwest",
|
||||
"scraper",
|
||||
"serde",
|
||||
@@ -1058,6 +1090,15 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "ogg_pager"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d6d1ca8364b84e0cf725eed06b1460c44671e6c0fb28765f5262de3ece07fdc"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
@@ -1105,6 +1146,12 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
|
||||
+2
-1
@@ -21,7 +21,8 @@ anyhow = "1.0.102"
|
||||
itertools = "0.14.0"
|
||||
dotenv = "0.15.0"
|
||||
|
||||
id3 = { version = "1.16.4", default-features = false, features = ["tokio"] }
|
||||
lofty = "0.23.3"
|
||||
id3 = "1.16.4"
|
||||
reqwest = { version = "0.13.2", default-features = false, features = [
|
||||
"rustls",
|
||||
"json",
|
||||
|
||||
+76
-18
@@ -1,5 +1,9 @@
|
||||
mod sources;
|
||||
|
||||
use lofty::{
|
||||
file::{AudioFile, TaggedFileExt},
|
||||
tag::TagExt,
|
||||
};
|
||||
use sources::*;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -16,7 +20,8 @@ use tokio::sync::Semaphore;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use id3::{Tag, TagLike, Version, frame::Lyrics};
|
||||
use id3::{TagLike, Version};
|
||||
use lofty::{config::WriteOptions, tag::ItemKey};
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
@@ -175,28 +180,44 @@ async fn process_file(
|
||||
overwrite: bool,
|
||||
allow_inaccurate: bool,
|
||||
) {
|
||||
let mut tag = match Tag::async_read_from_path(file_path).await {
|
||||
Ok(tag) => tag,
|
||||
let mut tagged_file = match lofty::read_from_path(&file_path) {
|
||||
Ok(file) => file,
|
||||
Err(e) => {
|
||||
eprintln!("[ERROR] Failed to read '{}': {}", file_path.display(), e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let search_tag = match tagged_file.primary_tag().or(tagged_file.first_tag()) {
|
||||
Some(t) => t.clone(),
|
||||
None => {
|
||||
eprintln!(
|
||||
"[ERROR] Failed to read ID3 tag for file '{}': {}",
|
||||
file_path.display(),
|
||||
e
|
||||
"[ERROR] No tags in '{}', don't know what to search for. Skipping",
|
||||
file_path.display()
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !overwrite && tag.lyrics().next().is_some() {
|
||||
let properties = tagged_file.properties();
|
||||
|
||||
let has_lyrics = search_tag.items().any(|item| {
|
||||
matches!(item.key(), ItemKey::Lyrics) || format!("{:?}", item.key()).contains("Lyric")
|
||||
});
|
||||
|
||||
if !overwrite && has_lyrics {
|
||||
println!(
|
||||
"[INFO] File '{}' already has lyrics, skipping (use --overwrite to force)",
|
||||
"[INFO] Lyrics already exist in '{}', skipping",
|
||||
file_path.display()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for source in sources.iter() {
|
||||
match source.fetch_lyrics(&tag, allow_inaccurate).await {
|
||||
match source
|
||||
.fetch_lyrics(&search_tag, properties, allow_inaccurate)
|
||||
.await
|
||||
{
|
||||
Ok(lyrics) => {
|
||||
let lyrics = lyrics.trim();
|
||||
if lyrics.is_empty() {
|
||||
@@ -209,34 +230,71 @@ async fn process_file(
|
||||
}
|
||||
|
||||
println!(
|
||||
"[INFO] Successfully fetched lyrics for file '{}' from source '{}'",
|
||||
"[INFO] Found lyrics for '{}' via '{}'",
|
||||
file_path.display(),
|
||||
source.name()
|
||||
);
|
||||
|
||||
tag.remove_all_lyrics();
|
||||
tag.remove_all_synchronised_lyrics();
|
||||
let ext = file_path
|
||||
.extension()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_lowercase();
|
||||
|
||||
tag.add_frame(Lyrics {
|
||||
if ext == "mp3" {
|
||||
let mut mp3_tag =
|
||||
id3::Tag::read_from_path(&file_path).unwrap_or_else(|_| id3::Tag::new());
|
||||
|
||||
mp3_tag.add_frame(id3::frame::Lyrics {
|
||||
lang: "XXX".to_string(),
|
||||
description: format!("Fetched from {}", source.name()),
|
||||
description: String::new(),
|
||||
text: lyrics.to_string(),
|
||||
});
|
||||
|
||||
if let Err(e) = tag.write_to_path(file_path, Version::Id3v24) {
|
||||
if let Err(e) = mp3_tag.write_to_path(&file_path, Version::Id3v24) {
|
||||
eprintln!(
|
||||
"[ERROR] Failed to write tags into {}: {}",
|
||||
"[ERROR] Failed to save MP3 tag in {}: {}",
|
||||
file_path.display(),
|
||||
e
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"[INFO] Successfully wrote lyrics for '{}' via id3 (MP3)",
|
||||
file_path.display()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if tagged_file.primary_tag().is_none() {
|
||||
let tag_type = tagged_file.primary_tag_type();
|
||||
tagged_file.insert_tag(lofty::tag::Tag::new(tag_type));
|
||||
}
|
||||
|
||||
let tag_to_save = tagged_file.primary_tag_mut().unwrap();
|
||||
|
||||
tag_to_save.remove_key(ItemKey::Lyrics);
|
||||
tag_to_save.insert_text(ItemKey::Lyrics, lyrics.to_string());
|
||||
|
||||
if let Err(e) = tag_to_save.save_to_path(&file_path, WriteOptions::default()) {
|
||||
eprintln!(
|
||||
"[ERROR] Failed to save tags in {}: {}",
|
||||
file_path.display(),
|
||||
e
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"[INFO] Successfully wrote lyrics for '{}' via lofty ({})",
|
||||
file_path.display(),
|
||||
ext.to_uppercase()
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"[ERROR] Failed to fetch lyrics for file '{}' from source '{}': {}",
|
||||
file_path.display(),
|
||||
"[ERROR] Source '{}' error for '{}': {}",
|
||||
source.name(),
|
||||
file_path.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
+14
-4
@@ -2,7 +2,10 @@ use crate::sources::LyricsSource;
|
||||
|
||||
use anyhow::{Context, Ok, Result, anyhow};
|
||||
use async_trait::async_trait;
|
||||
use id3::{Tag, TagLike};
|
||||
use lofty::{
|
||||
properties::FileProperties,
|
||||
tag::{Accessor, Tag},
|
||||
};
|
||||
use reqwest::Client;
|
||||
use scraper::{ElementRef, Html, Node, Selector};
|
||||
use serde::Deserialize;
|
||||
@@ -66,13 +69,20 @@ impl LyricsSource for GeniusSource {
|
||||
"Genius"
|
||||
}
|
||||
|
||||
async fn fetch_lyrics(&self, tag: &Tag, allow_inaccurate: bool) -> Result<String> {
|
||||
async fn fetch_lyrics(
|
||||
&self,
|
||||
tag: &Tag,
|
||||
_properties: &FileProperties,
|
||||
allow_inaccurate: bool,
|
||||
) -> Result<String> {
|
||||
let title = tag
|
||||
.title()
|
||||
.ok_or_else(|| anyhow!("Missing track title in ID3 tag"))?;
|
||||
.ok_or_else(|| anyhow!("Missing track title in ID3 tag"))?
|
||||
.to_string();
|
||||
let artist = tag
|
||||
.artist()
|
||||
.ok_or_else(|| anyhow!("Missing artist name in ID3 tag"))?;
|
||||
.ok_or_else(|| anyhow!("Missing artist name in ID3 tag"))?
|
||||
.to_string();
|
||||
|
||||
let query = format!("{} {}", artist, title);
|
||||
|
||||
|
||||
+20
-10
@@ -2,7 +2,10 @@ use crate::sources::LyricsSource;
|
||||
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use async_trait::async_trait;
|
||||
use id3::{Tag, TagLike};
|
||||
use lofty::{
|
||||
properties::FileProperties,
|
||||
tag::{Accessor, Tag},
|
||||
};
|
||||
use reqwest::{Client, StatusCode};
|
||||
use serde::Deserialize;
|
||||
|
||||
@@ -63,25 +66,32 @@ impl LyricsSource for LrcLibSource {
|
||||
"LrcLib"
|
||||
}
|
||||
|
||||
async fn fetch_lyrics(&self, tag: &Tag, allow_inaccurate: bool) -> Result<String> {
|
||||
async fn fetch_lyrics(
|
||||
&self,
|
||||
tag: &Tag,
|
||||
properties: &FileProperties,
|
||||
allow_inaccurate: bool,
|
||||
) -> Result<String> {
|
||||
let base_url = "https://lrclib.net/api";
|
||||
|
||||
let track_name = tag
|
||||
.title()
|
||||
.ok_or_else(|| anyhow!("Missing track title in ID3 tag"))?;
|
||||
.ok_or_else(|| anyhow!("Missing track title in ID3 tag"))?
|
||||
.to_string();
|
||||
let artist_name = tag
|
||||
.artist()
|
||||
.ok_or_else(|| anyhow!("Missing artist name in ID3 tag"))?;
|
||||
let album_name = tag.album().unwrap_or_default();
|
||||
let target_duration = tag.duration().unwrap_or_default() / 1000;
|
||||
.ok_or_else(|| anyhow!("Missing artist name in ID3 tag"))?
|
||||
.to_string();
|
||||
let album_name = tag.album().unwrap_or_default().to_string();
|
||||
let target_duration = properties.duration().as_secs() as u32;
|
||||
|
||||
let mut get_req = self
|
||||
.client
|
||||
.get(format!("{base_url}/get"))
|
||||
.query(&[("track_name", track_name), ("artist_name", artist_name)]);
|
||||
.query(&[("track_name", &track_name), ("artist_name", &artist_name)]);
|
||||
|
||||
if !album_name.is_empty() {
|
||||
get_req = get_req.query(&[("album_name", album_name)]);
|
||||
get_req = get_req.query(&[("album_name", &album_name)]);
|
||||
}
|
||||
|
||||
if target_duration > 0 {
|
||||
@@ -100,10 +110,10 @@ impl LyricsSource for LrcLibSource {
|
||||
let mut search_req = self
|
||||
.client
|
||||
.get(format!("{base_url}/search"))
|
||||
.query(&[("track_name", track_name), ("artist_name", artist_name)]);
|
||||
.query(&[("track_name", &track_name), ("artist_name", &artist_name)]);
|
||||
|
||||
if !album_name.is_empty() {
|
||||
search_req = search_req.query(&[("album_name", album_name)]);
|
||||
search_req = search_req.query(&[("album_name", &album_name)]);
|
||||
}
|
||||
|
||||
let response = search_req.send().await?;
|
||||
|
||||
+7
-2
@@ -6,13 +6,18 @@ pub use lrclib::LrcLibSource;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use async_trait::async_trait;
|
||||
use id3::Tag;
|
||||
use lofty::{properties::FileProperties, tag::Tag};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[async_trait]
|
||||
pub trait LyricsSource: Send + Sync {
|
||||
fn name(&self) -> &'static str;
|
||||
async fn fetch_lyrics(&self, tag: &Tag, allow_inaccurate: bool) -> Result<String>;
|
||||
async fn fetch_lyrics(
|
||||
&self,
|
||||
tag: &Tag,
|
||||
properties: &FileProperties,
|
||||
allow_inaccurate: bool,
|
||||
) -> Result<String>;
|
||||
}
|
||||
|
||||
pub async fn create_source(name: &str) -> Result<Arc<dyn LyricsSource>> {
|
||||
|
||||
Reference in New Issue
Block a user