Add search function and code quality improvements

This commit is contained in:
oSumAtrIX 2021-11-07 03:22:22 +01:00
parent a96279b190
commit c523a3a18c
No known key found for this signature in database
GPG key ID: A9B3094ACDB604B4
8 changed files with 1026 additions and 941 deletions

2
Cargo.lock generated
View file

@ -662,7 +662,7 @@ dependencies = [
[[package]] [[package]]
name = "down_on_spot" name = "down_on_spot"
version = "0.0.1" version = "0.1.1"
dependencies = [ dependencies = [
"aspotify", "aspotify",
"async-std", "async-std",

View file

@ -9,7 +9,7 @@ panic = "abort"
[package] [package]
name = "down_on_spot" name = "down_on_spot"
version = "0.0.1" version = "0.1.1"
edition = "2018" edition = "2018"
authors = ["exttex", "oSumAtrIX"] authors = ["exttex", "oSumAtrIX"]
build = "build.rs" build = "build.rs"
@ -44,4 +44,4 @@ tokio = { version = "1.12", features = ["fs"] }
OriginalFilename = "DownOnSpot.exe" OriginalFilename = "DownOnSpot.exe"
FileDescription = "Download songs from Spotify with Rust" FileDescription = "Download songs from Spotify with Rust"
ProductName = "DownOnSpot" ProductName = "DownOnSpot"
ProductVersion = "0.0.1" ProductVersion = "0.1.1"

View file

@ -31,6 +31,7 @@ I am not responsible in any way for the usage of the source code.
- Works with free Spotify accounts (if using free-librespot fork) - Works with free Spotify accounts (if using free-librespot fork)
- Download 96, 160kbit/s audio with a free, 256 and 320 kbit/s audio with a premium account from Spotify, directly - Download 96, 160kbit/s audio with a free, 256 and 320 kbit/s audio with a premium account from Spotify, directly
- Multi-threaded - Multi-threaded
- Search for tracks
- Download tracks, playlists, albums and artists - Download tracks, playlists, albums and artists
- Convert to mp3 - Convert to mp3
- Metadata tagging - Metadata tagging
@ -76,7 +77,7 @@ Settings could not be loaded, because of the following error: IO: NotFound No su
$ down_on_spot.exe $ down_on_spot.exe
Usage: Usage:
down_on_spot.exe (track_url | album_url | playlist_url | artist_url) down_on_spot.exe (search_term | track_url | album_url | playlist_url | artist_url)
``` ```
### Template variables ### Template variables

File diff suppressed because it is too large Load diff

View file

@ -8,8 +8,6 @@ use settings::Settings;
use spotify::Spotify; use spotify::Spotify;
use std::{ use std::{
env, env,
ffi::OsStr,
path::Path,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
@ -29,14 +27,14 @@ async fn main() {
#[cfg(windows)] #[cfg(windows)]
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
use colored::control; use colored::control;
//backwards compatibility. //backwards compatibility.
match control::set_virtual_terminal(true) { match control::set_virtual_terminal(true) {
Ok(_) => {}, Ok(_) => {}
Err(_) => {} Err(_) => {}
}; };
start().await; start().await;
} }
@ -45,7 +43,7 @@ async fn start() {
Ok(settings) => { Ok(settings) => {
println!( println!(
"{} {}.", "{} {}.",
"Settings successfully loaded. Continuing with spotify account:".green(), "Settings successfully loaded.\nContinuing with spotify account:".green(),
settings.username settings.username
); );
settings settings
@ -56,8 +54,7 @@ async fn start() {
"Settings could not be loaded, because of the following error:".red(), "Settings could not be loaded, because of the following error:".red(),
e e
); );
let default_settings = let default_settings = Settings::new("username", "password", "client_id", "secret");
Settings::new("username", "password", "client_id", "secret").unwrap();
match default_settings.save().await { match default_settings.save().await {
Ok(_) => { Ok(_) => {
println!( println!(
@ -103,75 +100,108 @@ async fn start() {
let downloader = Downloader::new(settings.downloader, spotify); let downloader = Downloader::new(settings.downloader, spotify);
match downloader.add_uri(&args[1]).await { match downloader.handle_input(&args[1]).await {
Ok(_) => {} Ok(search_results) => {
Err(e) => { if let Some(search_results) = search_results {
error!("{} {}", "Adding url failed:".red(), e) print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
}
}
let refresh = Duration::from_secs(settings.refresh_ui_seconds); for (i, track) in search_results.iter().enumerate() {
let now = Instant::now(); println!("{}: {} - {}", i + 1, track.author, track.title);
let mut timeelapsed: u64; }
println!("{}", "Select the track (default: 1): ".green());
'outer: loop { let mut selection;
print!("{esc}[2J{esc}[1;1H", esc = 27 as char); loop {
let mut exit_flag: i8 = 1; let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.expect("Failed to read line");
for download in downloader.get_downloads().await { selection = input.trim().parse::<usize>().unwrap_or(1) - 1;
let state = download.state;
let progress: String; if selection < search_results.len() {
break;
}
println!("{}", "Invalid selection. Try again or quit (CTRL+C):".red());
}
if state != DownloadState::Done { let track = &search_results[selection];
exit_flag &= 0;
progress = match state { if let Err(e) = downloader
DownloadState::Downloading(r, t) => { .add_uri(&format!("spotify:track:{}", track.track_id))
let p = r as f32 / t as f32 * 100.0; .await
if p > 100.0 { {
"100%".to_string() error!(
} else { "{}",
format!("{}%", p as i8) format!(
"{}: {}",
"Track could not be added to download queue.".red(),
e
)
);
} else {
let refresh = Duration::from_secs(settings.refresh_ui_seconds);
let now = Instant::now();
let mut time_elapsed: u64;
'outer: loop {
print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
let mut exit_flag: i8 = 1;
for download in downloader.get_downloads().await {
let state = download.state;
let progress: String;
if state != DownloadState::Done {
exit_flag &= 0;
progress = match state {
DownloadState::Downloading(r, t) => {
let p = r as f32 / t as f32 * 100.0;
if p > 100.0 {
"100%".to_string()
} else {
format!("{}%", p as i8)
}
}
DownloadState::Post => "Postprocessing... ".to_string(),
DownloadState::None => "Preparing... ".to_string(),
DownloadState::Lock => "Preparing... ".to_string(),
DownloadState::Error(e) => {
exit_flag |= 1;
format!("{} ", e)
}
DownloadState::Done => {
exit_flag |= 1;
"Impossible state".to_string()
}
};
} else {
progress = "Done.".to_string();
}
println!("{:<19}| {}", progress, download.title);
} }
time_elapsed = now.elapsed().as_secs();
if exit_flag == 1 {
break 'outer;
}
println!("\nElapsed second(s): {}", time_elapsed);
task::sleep(refresh).await
} }
DownloadState::Post => "Postprocessing... ".to_string(), println!("Finished download(s) in {} second(s).", time_elapsed);
DownloadState::None => "Preparing... ".to_string(), }
DownloadState::Lock => "Holding... ".to_string(),
DownloadState::Error(e) => {
exit_flag |= 1;
format!("{} ", e)
},
DownloadState::Done => {
exit_flag |= 1;
"Impossible state".to_string()
}
};
} else {
progress = "Done.".to_string();
} }
println!("{:<19}| {}", progress, download.title);
} }
timeelapsed = now.elapsed().as_secs(); Err(e) => {
if exit_flag == 1 { error!("{} {}", "Handling input failed:".red(), e)
break 'outer;
} }
println!("\nElapsed second(s): {}", timeelapsed);
task::sleep(refresh).await
} }
println!("Finished download(s) in {} second(s).", timeelapsed);
} else { } else {
println!( println!(
"Usage:\n{} (track_url | album_url | playlist_url | artist_url )", "Usage:\n{} (track_url | album_url | playlist_url | artist_url )",
env::args() args[0]
.next()
.as_ref()
.map(Path::new)
.and_then(Path::file_name)
.and_then(OsStr::to_str)
.map(String::from)
.unwrap()
); );
} }
} }

View file

@ -1,5 +1,4 @@
use crate::downloader::DownloaderConfig; use crate::downloader::DownloaderConfig;
use crate::downloader::Quality;
use crate::error::SpotifyError; use crate::error::SpotifyError;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -18,30 +17,18 @@ pub struct Settings {
pub refresh_ui_seconds: u64, pub refresh_ui_seconds: u64,
pub downloader: DownloaderConfig, pub downloader: DownloaderConfig,
} }
impl Settings { impl Settings {
// Create new instance // Create new instance
pub fn new( pub fn new(username: &str, password: &str, client_id: &str, client_secret: &str) -> Settings {
username: &str, Settings {
password: &str,
client_id: &str,
client_secret: &str,
) -> Option<Settings> {
Some(Settings {
username: username.to_string(), username: username.to_string(),
password: password.to_string(), password: password.to_string(),
client_id: client_id.to_string(), client_id: client_id.to_string(),
client_secret: client_secret.to_string(), client_secret: client_secret.to_string(),
refresh_ui_seconds: 1, refresh_ui_seconds: 1,
downloader: DownloaderConfig { downloader: DownloaderConfig::new()
concurrent_downloads: 4, }
quality: Quality::Q320,
path: "downloads".to_string(),
filename_template: "%artist% - %title%".to_string(),
id3v24: true,
convert_to_mp3: false,
separator: ", ".to_string(),
},
})
} }
// Serialize the settings to a json file // Serialize the settings to a json file

View file

@ -1,6 +1,4 @@
use aspotify::{ use aspotify::{Album, Artist, Client, ClientCredentials, ItemType, Playlist, PlaylistItemType, Track, TrackSimplified};
Album, Artist, Client, ClientCredentials, Playlist, PlaylistItemType, Track, TrackSimplified,
};
use librespot::core::authentication::Credentials; use librespot::core::authentication::Credentials;
use librespot::core::config::SessionConfig; use librespot::core::config::SessionConfig;
use librespot::core::session::Session; use librespot::core::session::Session;
@ -10,183 +8,196 @@ use url::Url;
use crate::error::SpotifyError; use crate::error::SpotifyError;
pub struct Spotify { pub struct Spotify {
// librespotify sessopm // librespotify sessopm
pub session: Session, pub session: Session,
pub spotify: Client, pub spotify: Client,
} }
impl Spotify { impl Spotify {
/// Create new instance /// Create new instance
pub async fn new( pub async fn new(
username: &str, username: &str,
password: &str, password: &str,
client_id: &str, client_id: &str,
client_secret: &str, client_secret: &str,
) -> Result<Spotify, SpotifyError> { ) -> Result<Spotify, SpotifyError> {
// librespot // librespot
let credentials = Credentials::with_password(username, password); let credentials = Credentials::with_password(username, password);
let session = Session::connect(SessionConfig::default(), credentials, None).await?; let session = Session::connect(SessionConfig::default(), credentials, None).await?;
//aspotify //aspotify
let credentials = ClientCredentials { let credentials = ClientCredentials {
id: client_id.to_string(), id: client_id.to_string(),
secret: client_secret.to_string(), secret: client_secret.to_string(),
}; };
let spotify = Client::new(credentials); let spotify = Client::new(credentials);
Ok(Spotify { session, spotify }) Ok(Spotify { session, spotify })
} }
/// Parse URI or URL into URI /// Parse URI or URL into URI
pub fn parse_uri(uri: &str) -> Result<String, SpotifyError> { pub fn parse_uri(uri: &str) -> Result<String, SpotifyError> {
// Already URI // Already URI
if uri.starts_with("spotify:") { if uri.starts_with("spotify:") {
if uri.split(':').count() < 3 { if uri.split(':').count() < 3 {
return Err(SpotifyError::InvalidUri); return Err(SpotifyError::InvalidUri);
} }
return Ok(uri.to_string()); return Ok(uri.to_string());
} }
// Parse URL // Parse URL
let url = Url::parse(uri)?; let url = Url::parse(uri)?;
// Spotify Web Player URL // Spotify Web Player URL
if url.host_str() == Some("open.spotify.com") { if url.host_str() == Some("open.spotify.com") {
let path = url let path = url
.path_segments() .path_segments()
.ok_or_else(|| SpotifyError::Error("Missing URL path".into()))? .ok_or_else(|| SpotifyError::Error("Missing URL path".into()))?
.collect::<Vec<&str>>(); .collect::<Vec<&str>>();
if path.len() < 2 { if path.len() < 2 {
return Err(SpotifyError::InvalidUri); return Err(SpotifyError::InvalidUri);
} }
return Ok(format!("spotify:{}:{}", path[0], path[1])); return Ok(format!("spotify:{}:{}", path[0], path[1]));
} }
Err(SpotifyError::InvalidUri) Err(SpotifyError::InvalidUri)
} }
/// Fetch data for URI /// Fetch data for URI
pub async fn resolve_uri(&self, uri: &str) -> Result<SpotifyItem, SpotifyError> { pub async fn resolve_uri(&self, uri: &str) -> Result<SpotifyItem, SpotifyError> {
let parts = uri.split(':').skip(1).collect::<Vec<&str>>(); let parts = uri.split(':').skip(1).collect::<Vec<&str>>();
let id = parts[1]; let id = parts[1];
match parts[0] { match parts[0] {
"track" => { "track" => {
let track = self.spotify.tracks().get_track(id, None).await?; let track = self.spotify.tracks().get_track(id, None).await?;
Ok(SpotifyItem::Track(track.data)) Ok(SpotifyItem::Track(track.data))
} }
"playlist" => { "playlist" => {
let playlist = self.spotify.playlists().get_playlist(id, None).await?; let playlist = self.spotify.playlists().get_playlist(id, None).await?;
Ok(SpotifyItem::Playlist(playlist.data)) Ok(SpotifyItem::Playlist(playlist.data))
} }
"album" => { "album" => {
let album = self.spotify.albums().get_album(id, None).await?; let album = self.spotify.albums().get_album(id, None).await?;
Ok(SpotifyItem::Album(album.data)) Ok(SpotifyItem::Album(album.data))
} }
"artist" => { "artist" => {
let artist = self.spotify.artists().get_artist(id).await?; let artist = self.spotify.artists().get_artist(id).await?;
Ok(SpotifyItem::Artist(artist.data)) Ok(SpotifyItem::Artist(artist.data))
} }
// Unsupported / Unimplemented // Unsupported / Unimplemented
_ => Ok(SpotifyItem::Other(uri.to_string())), _ => Ok(SpotifyItem::Other(uri.to_string())),
} }
} }
/// Get all tracks from playlist /// Get search results for query
pub async fn full_playlist(&self, id: &str) -> Result<Vec<Track>, SpotifyError> { pub async fn search(&self, query: &str) -> Result<Vec<Track>, SpotifyError> {
let mut items = vec![]; Ok(self
let mut offset = 0; .spotify
loop { .search()
let page = self .search(query, [ItemType::Track], true, 50, 0, None)
.spotify .await?
.playlists() .data
.get_playlists_items(id, 100, offset, None) .tracks
.await?; .unwrap()
items.append( .items)
&mut page }
.data
.items
.iter()
.filter_map(|i| -> Option<Track> {
if let Some(PlaylistItemType::Track(t)) = &i.item {
Some(t.to_owned())
} else {
None
}
})
.collect(),
);
// End /// Get all tracks from playlist
offset += page.data.items.len(); pub async fn full_playlist(&self, id: &str) -> Result<Vec<Track>, SpotifyError> {
if page.data.total == offset { let mut items = vec![];
return Ok(items); let mut offset = 0;
} loop {
} let page = self
} .spotify
.playlists()
.get_playlists_items(id, 100, offset, None)
.await?;
items.append(
&mut page
.data
.items
.iter()
.filter_map(|i| -> Option<Track> {
if let Some(PlaylistItemType::Track(t)) = &i.item {
Some(t.to_owned())
} else {
None
}
})
.collect(),
);
/// Get all tracks from album // End
pub async fn full_album(&self, id: &str) -> Result<Vec<TrackSimplified>, SpotifyError> { offset += page.data.items.len();
let mut items = vec![]; if page.data.total == offset {
let mut offset = 0; return Ok(items);
loop { }
let page = self }
.spotify }
.albums()
.get_album_tracks(id, 50, offset, None)
.await?;
items.append(&mut page.data.items.to_vec());
// End /// Get all tracks from album
offset += page.data.items.len(); pub async fn full_album(&self, id: &str) -> Result<Vec<TrackSimplified>, SpotifyError> {
if page.data.total == offset { let mut items = vec![];
return Ok(items); let mut offset = 0;
} loop {
} let page = self
} .spotify
.albums()
.get_album_tracks(id, 50, offset, None)
.await?;
items.append(&mut page.data.items.to_vec());
/// Get all tracks from artist // End
pub async fn full_artist(&self, id: &str) -> Result<Vec<TrackSimplified>, SpotifyError> { offset += page.data.items.len();
let mut items = vec![]; if page.data.total == offset {
let mut offset = 0; return Ok(items);
loop { }
let page = self }
.spotify }
.artists()
.get_artist_albums(id, None, 50, offset, None)
.await?;
for album in &mut page.data.items.iter() { /// Get all tracks from artist
items.append(&mut self.full_album(&album.id).await?) pub async fn full_artist(&self, id: &str) -> Result<Vec<TrackSimplified>, SpotifyError> {
} let mut items = vec![];
let mut offset = 0;
loop {
let page = self
.spotify
.artists()
.get_artist_albums(id, None, 50, offset, None)
.await?;
// End for album in &mut page.data.items.iter() {
offset += page.data.items.len(); items.append(&mut self.full_album(&album.id).await?)
if page.data.total == offset { }
return Ok(items);
} // End
} offset += page.data.items.len();
} if page.data.total == offset {
return Ok(items);
}
}
}
} }
impl Clone for Spotify { impl Clone for Spotify {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {
session: self.session.clone(), session: self.session.clone(),
spotify: Client::new(self.spotify.credentials.clone()), spotify: Client::new(self.spotify.credentials.clone()),
} }
} }
} }
/// Basic debug implementation so can be used in other structs /// Basic debug implementation so can be used in other structs
impl fmt::Debug for Spotify { impl fmt::Debug for Spotify {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "<Spotify Instance>") write!(f, "<Spotify Instance>")
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum SpotifyItem { pub enum SpotifyItem {
Track(Track), Track(Track),
Album(Album), Album(Album),
Playlist(Playlist), Playlist(Playlist),
Artist(Artist), Artist(Artist),
/// Unimplemented /// Unimplemented
Other(String), Other(String),
} }

View file

@ -1,4 +1,5 @@
use chrono::NaiveDate; use chrono::NaiveDate;
use std::path::Path; use std::path::Path;
use crate::downloader::AudioFormat; use crate::downloader::AudioFormat;
@ -16,6 +17,7 @@ pub enum TagWrap {
} }
impl TagWrap { impl TagWrap {
/// Load from file /// Load from file
pub fn new(path: impl AsRef<Path>, format: AudioFormat) -> Result<TagWrap, SpotifyError> { pub fn new(path: impl AsRef<Path>, format: AudioFormat) -> Result<TagWrap, SpotifyError> {
match format { match format {
@ -27,10 +29,10 @@ impl TagWrap {
/// Get Tag trait /// Get Tag trait
pub fn get_tag(&mut self) -> Box<&mut dyn Tag> { pub fn get_tag(&mut self) -> Box<&mut dyn Tag> {
match self { Box::new(match self {
TagWrap::Ogg(tag) => Box::new(tag), TagWrap::Ogg(tag) => tag,
TagWrap::Id3(tag) => Box::new(tag), TagWrap::Id3(tag) => tag,
} })
} }
} }