Initial commit
This commit is contained in:
commit
6dd66b582b
15
Cargo.toml
Normal file
15
Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "lrclib-downloader"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1.0"
|
||||
audiotags = "0.5.0"
|
||||
http = "1.4.0"
|
||||
mp3-duration = "0.1.10"
|
||||
urlencoding = "2.1.3"
|
||||
reqwest = { version = "0.11", features = ["blocking", "rustls-tls"] }
|
||||
serde_json = "1.0.149"
|
||||
serde = "1.0.228"
|
||||
clap = { version = "4.5.55", features = ["derive"] }
|
||||
20
flake.nix
Normal file
20
flake.nix
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||
};
|
||||
outputs = { self, nixpkgs }:
|
||||
let pkgs = nixpkgs.legacyPackages."x86_64-linux"; in {
|
||||
devShells."x86_64-linux".default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
cargo
|
||||
rustc
|
||||
rustfmt
|
||||
clippy
|
||||
rust-analyzer
|
||||
openssl
|
||||
pkg-config
|
||||
];
|
||||
env.RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
|
||||
};
|
||||
};
|
||||
}
|
||||
1
src/cli.rs
Normal file
1
src/cli.rs
Normal file
@ -0,0 +1 @@
|
||||
|
||||
15
src/json_parsing.rs
Normal file
15
src/json_parsing.rs
Normal file
@ -0,0 +1,15 @@
|
||||
use crate::lrclib;
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn get_lyrics_text(artist_name: &str, track_name: &str, album_name: &str, duration: Duration) {
|
||||
match lrclib::get_lyrics(artist_name, track_name, album_name, duration) {
|
||||
Some(lyrics_json) => match serde_json::from_str::<serde_json::Value>(&lyrics_json) {
|
||||
Ok(parsed) => {
|
||||
let sync_lyrics = &parsed["syncedLyrics"];
|
||||
println!("{sync_lyrics}\n");
|
||||
}
|
||||
Err(e) => eprintln!("Failed to parse lyrics JSON: {e}"),
|
||||
},
|
||||
None => eprintln!("Failed to fetch lyrics for {track_name}"),
|
||||
}
|
||||
}
|
||||
31
src/lrclib.rs
Normal file
31
src/lrclib.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use std::time::Duration;
|
||||
use urlencoding::encode;
|
||||
|
||||
pub fn get_lyrics(
|
||||
artist_name: &str,
|
||||
track_name: &str,
|
||||
album_name: &str,
|
||||
duration: Duration,
|
||||
) -> Option<String> {
|
||||
let duration_secs = duration.as_secs();
|
||||
|
||||
let url = format!(
|
||||
"https://lrclib.net/api/get?artist_name={}&track_name={}&album_name={}&duration={}",
|
||||
encode(artist_name),
|
||||
encode(track_name),
|
||||
encode(album_name),
|
||||
duration_secs
|
||||
);
|
||||
|
||||
match reqwest::blocking::get(&url) {
|
||||
Ok(response) if response.status().is_success() => response.text().ok(),
|
||||
Ok(response) => {
|
||||
eprintln!("Request failed with status: {}", response.status());
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Request error: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/main.rs
Normal file
43
src/main.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use clap::Parser;
|
||||
use std::path::PathBuf;
|
||||
|
||||
mod json_parsing;
|
||||
mod lrclib;
|
||||
mod music;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct Args {
|
||||
#[arg(short, long, num_args = 1..)]
|
||||
file: Option<Vec<PathBuf>>,
|
||||
|
||||
#[arg(short, long)]
|
||||
directory: Option<PathBuf>,
|
||||
|
||||
#[arg(short, long)]
|
||||
watch: bool,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
if let Some(files) = &args.file {
|
||||
for file in files {
|
||||
if file.is_file() {
|
||||
if let Err(e) = music::get_song_file(file) {
|
||||
eprintln!("Failed to process {}: {e}", file.display());
|
||||
}
|
||||
} else {
|
||||
println!("Not a file: {:?}", file)
|
||||
}
|
||||
}
|
||||
} else if let Some(directory) = &args.directory {
|
||||
if !args.watch {
|
||||
if let Err(e) = music::get_all_music_files(directory) {
|
||||
eprintln!("Failed to process directory {}: {e}", directory.display());
|
||||
}
|
||||
} else {
|
||||
println!("Watch directory for new files")
|
||||
}
|
||||
}
|
||||
}
|
||||
99
src/music.rs
Normal file
99
src/music.rs
Normal file
@ -0,0 +1,99 @@
|
||||
use crate::json_parsing;
|
||||
use audiotags::Tag;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TrackInfo {
|
||||
pub track_name: String,
|
||||
pub artist_name: String,
|
||||
pub album_name: String,
|
||||
pub duration: Duration,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MusicError {
|
||||
#[error("failed to read directory: {0}")]
|
||||
ReadDir(#[from] std::io::Error),
|
||||
|
||||
#[error("failed to read audio tags from {0}")]
|
||||
TagRead(String),
|
||||
|
||||
#[error("failed to get song duration for {0}")]
|
||||
DurationRead(String),
|
||||
}
|
||||
|
||||
fn is_mp3(path: &Path) -> bool {
|
||||
path.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.map(|ext| ext.eq_ignore_ascii_case("mp3"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn get_song_file(path: &Path) -> Result<(), MusicError> {
|
||||
if !is_mp3(path) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let info = get_song_info(path)?;
|
||||
|
||||
println!("Track: {}", info.track_name);
|
||||
println!("Artist: {}", info.artist_name);
|
||||
println!("Album: {}", info.album_name);
|
||||
println!("Duration: {:?}\n", info.duration);
|
||||
|
||||
json_parsing::get_lyrics_text(
|
||||
&info.artist_name,
|
||||
&info.track_name,
|
||||
&info.album_name,
|
||||
info.duration,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_all_music_files(directory: &Path) -> Result<(), MusicError> {
|
||||
if !directory.is_dir() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(directory)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if entry.file_type()?.is_dir() {
|
||||
get_all_music_files(&path)?;
|
||||
} else if let Err(e) = get_song_file(&path) {
|
||||
eprintln!("Skipping file {}: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_song_info(path: &Path) -> Result<TrackInfo, MusicError> {
|
||||
let tag = Tag::new()
|
||||
.read_from_path(path)
|
||||
.map_err(|_| MusicError::TagRead(path.display().to_string()))?;
|
||||
|
||||
let track_name = tag.title().unwrap_or("Unknown Track").to_string();
|
||||
let artist_name = tag.artist().unwrap_or("Unknown Artist").to_string();
|
||||
let album_name = tag
|
||||
.album()
|
||||
.map_or("Unknown Album".to_string(), |a| a.title.to_string());
|
||||
|
||||
let duration = get_song_length(path)?;
|
||||
|
||||
Ok(TrackInfo {
|
||||
track_name,
|
||||
artist_name,
|
||||
album_name,
|
||||
duration,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_song_length(path: &Path) -> Result<Duration, MusicError> {
|
||||
mp3_duration::from_path(path).map_err(|_| MusicError::DurationRead(path.display().to_string()))
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user