Add search function and code quality improvements
This commit is contained in:
parent
a96279b190
commit
c523a3a18c
8 changed files with 1026 additions and 941 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -662,7 +662,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "down_on_spot"
|
||||
version = "0.0.1"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"aspotify",
|
||||
"async-std",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ panic = "abort"
|
|||
|
||||
[package]
|
||||
name = "down_on_spot"
|
||||
version = "0.0.1"
|
||||
version = "0.1.1"
|
||||
edition = "2018"
|
||||
authors = ["exttex", "oSumAtrIX"]
|
||||
build = "build.rs"
|
||||
|
|
@ -44,4 +44,4 @@ tokio = { version = "1.12", features = ["fs"] }
|
|||
OriginalFilename = "DownOnSpot.exe"
|
||||
FileDescription = "Download songs from Spotify with Rust"
|
||||
ProductName = "DownOnSpot"
|
||||
ProductVersion = "0.0.1"
|
||||
ProductVersion = "0.1.1"
|
||||
|
|
@ -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)
|
||||
- Download 96, 160kbit/s audio with a free, 256 and 320 kbit/s audio with a premium account from Spotify, directly
|
||||
- Multi-threaded
|
||||
- Search for tracks
|
||||
- Download tracks, playlists, albums and artists
|
||||
- Convert to mp3
|
||||
- Metadata tagging
|
||||
|
|
@ -76,7 +77,7 @@ Settings could not be loaded, because of the following error: IO: NotFound No su
|
|||
|
||||
$ down_on_spot.exe
|
||||
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
|
||||
|
|
|
|||
|
|
@ -58,6 +58,28 @@ impl Downloader {
|
|||
self.tx.send(Message::AddToQueue(downloads)).await.unwrap();
|
||||
}
|
||||
|
||||
/// handle input, either link or search
|
||||
pub async fn handle_input(
|
||||
&self,
|
||||
input: &str,
|
||||
) -> Result<Option<Vec<SearchResult>>, SpotifyError> {
|
||||
if let Ok(uri) = Spotify::parse_uri(input) {
|
||||
if let Err(e) = self.add_uri(&uri).await {
|
||||
return Err(e);
|
||||
}
|
||||
Ok(None)
|
||||
} else {
|
||||
let results: Vec<SearchResult> = self
|
||||
.spotify
|
||||
.search(input)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|item| SearchResult::from(item))
|
||||
.collect();
|
||||
Ok(Some(results))
|
||||
}
|
||||
}
|
||||
|
||||
/// Add URL or URI to queue
|
||||
pub async fn add_uri(&self, uri: &str) -> Result<(), SpotifyError> {
|
||||
let uri = Spotify::parse_uri(uri)?;
|
||||
|
|
@ -331,14 +353,14 @@ impl DownloaderInternal {
|
|||
),
|
||||
];
|
||||
|
||||
// Generate path
|
||||
let mut filename = config.filename_template.to_owned();
|
||||
let mut path_template = config.path.to_owned();
|
||||
for (t, v) in tags {
|
||||
path_template = path_template.replace(t, &v);
|
||||
filename = filename.replace(t, &v);
|
||||
let mut filename_template = config.filename_template.clone();
|
||||
let mut path_template = config.path.clone();
|
||||
for (tag, value) in tags {
|
||||
filename_template = filename_template.replace(tag, &value);
|
||||
path_template = path_template.replace(tag, &value);
|
||||
}
|
||||
let path = PathBuf::from(path_template).join(filename);
|
||||
let path = Path::new(&path_template).join(&filename_template);
|
||||
|
||||
tokio::fs::create_dir_all(path.parent().unwrap()).await?;
|
||||
|
||||
// Download
|
||||
|
|
@ -585,7 +607,6 @@ impl DownloaderInternal {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Download and convert to MP3
|
||||
fn download_track_convert_stream(
|
||||
path: impl AsRef<Path>,
|
||||
|
|
@ -717,6 +738,7 @@ pub enum Message {
|
|||
GetJob,
|
||||
// Update state of download
|
||||
UpdateState(i64, DownloadState),
|
||||
//add to download
|
||||
AddToQueue(Vec<Download>),
|
||||
// Get all downloads to UI
|
||||
GetDownloads,
|
||||
|
|
@ -736,6 +758,23 @@ pub struct Download {
|
|||
pub state: DownloadState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SearchResult {
|
||||
pub track_id: String,
|
||||
pub author: String,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
impl From<aspotify::Track> for SearchResult {
|
||||
fn from(val: aspotify::Track) -> Self {
|
||||
SearchResult {
|
||||
track_id: val.id.unwrap(),
|
||||
author: val.artists[0].name.to_owned(),
|
||||
title: val.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<aspotify::Track> for Download {
|
||||
fn from(val: aspotify::Track) -> Self {
|
||||
Download {
|
||||
|
|
@ -818,3 +857,18 @@ pub struct DownloaderConfig {
|
|||
pub convert_to_mp3: bool,
|
||||
pub separator: String,
|
||||
}
|
||||
|
||||
impl DownloaderConfig {
|
||||
// Create new instance
|
||||
pub fn new() -> DownloaderConfig {
|
||||
DownloaderConfig {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
78
src/main.rs
78
src/main.rs
|
|
@ -8,8 +8,6 @@ use settings::Settings;
|
|||
use spotify::Spotify;
|
||||
use std::{
|
||||
env,
|
||||
ffi::OsStr,
|
||||
path::Path,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
|
|
@ -33,7 +31,7 @@ async fn main() {
|
|||
|
||||
//backwards compatibility.
|
||||
match control::set_virtual_terminal(true) {
|
||||
Ok(_) => {},
|
||||
Ok(_) => {}
|
||||
Err(_) => {}
|
||||
};
|
||||
|
||||
|
|
@ -45,7 +43,7 @@ async fn start() {
|
|||
Ok(settings) => {
|
||||
println!(
|
||||
"{} {}.",
|
||||
"Settings successfully loaded. Continuing with spotify account:".green(),
|
||||
"Settings successfully loaded.\nContinuing with spotify account:".green(),
|
||||
settings.username
|
||||
);
|
||||
settings
|
||||
|
|
@ -56,8 +54,7 @@ async fn start() {
|
|||
"Settings could not be loaded, because of the following error:".red(),
|
||||
e
|
||||
);
|
||||
let default_settings =
|
||||
Settings::new("username", "password", "client_id", "secret").unwrap();
|
||||
let default_settings = Settings::new("username", "password", "client_id", "secret");
|
||||
match default_settings.save().await {
|
||||
Ok(_) => {
|
||||
println!(
|
||||
|
|
@ -103,16 +100,49 @@ async fn start() {
|
|||
|
||||
let downloader = Downloader::new(settings.downloader, spotify);
|
||||
|
||||
match downloader.add_uri(&args[1]).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!("{} {}", "Adding url failed:".red(), e)
|
||||
match downloader.handle_input(&args[1]).await {
|
||||
Ok(search_results) => {
|
||||
if let Some(search_results) = search_results {
|
||||
print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
|
||||
|
||||
for (i, track) in search_results.iter().enumerate() {
|
||||
println!("{}: {} - {}", i + 1, track.author, track.title);
|
||||
}
|
||||
println!("{}", "Select the track (default: 1): ".green());
|
||||
|
||||
let mut selection;
|
||||
loop {
|
||||
let mut input = String::new();
|
||||
std::io::stdin()
|
||||
.read_line(&mut input)
|
||||
.expect("Failed to read line");
|
||||
|
||||
selection = input.trim().parse::<usize>().unwrap_or(1) - 1;
|
||||
|
||||
if selection < search_results.len() {
|
||||
break;
|
||||
}
|
||||
println!("{}", "Invalid selection. Try again or quit (CTRL+C):".red());
|
||||
}
|
||||
|
||||
let track = &search_results[selection];
|
||||
|
||||
if let Err(e) = downloader
|
||||
.add_uri(&format!("spotify:track:{}", track.track_id))
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"{}",
|
||||
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 timeelapsed: u64;
|
||||
let mut time_elapsed: u64;
|
||||
|
||||
'outer: loop {
|
||||
print!("{esc}[2J{esc}[1;1H", esc = 27 as char);
|
||||
|
|
@ -136,11 +166,11 @@ async fn start() {
|
|||
}
|
||||
DownloadState::Post => "Postprocessing... ".to_string(),
|
||||
DownloadState::None => "Preparing... ".to_string(),
|
||||
DownloadState::Lock => "Holding... ".to_string(),
|
||||
DownloadState::Lock => "Preparing... ".to_string(),
|
||||
DownloadState::Error(e) => {
|
||||
exit_flag |= 1;
|
||||
format!("{} ", e)
|
||||
},
|
||||
}
|
||||
DownloadState::Done => {
|
||||
exit_flag |= 1;
|
||||
"Impossible state".to_string()
|
||||
|
|
@ -152,26 +182,26 @@ async fn start() {
|
|||
|
||||
println!("{:<19}| {}", progress, download.title);
|
||||
}
|
||||
timeelapsed = now.elapsed().as_secs();
|
||||
time_elapsed = now.elapsed().as_secs();
|
||||
if exit_flag == 1 {
|
||||
break 'outer;
|
||||
}
|
||||
|
||||
println!("\nElapsed second(s): {}", timeelapsed);
|
||||
println!("\nElapsed second(s): {}", time_elapsed);
|
||||
task::sleep(refresh).await
|
||||
}
|
||||
println!("Finished download(s) in {} second(s).", timeelapsed);
|
||||
println!("Finished download(s) in {} second(s).", time_elapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{} {}", "Handling input failed:".red(), e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"Usage:\n{} (track_url | album_url | playlist_url | artist_url )",
|
||||
env::args()
|
||||
.next()
|
||||
.as_ref()
|
||||
.map(Path::new)
|
||||
.and_then(Path::file_name)
|
||||
.and_then(OsStr::to_str)
|
||||
.map(String::from)
|
||||
.unwrap()
|
||||
args[0]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use crate::downloader::DownloaderConfig;
|
||||
use crate::downloader::Quality;
|
||||
use crate::error::SpotifyError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
|
@ -18,30 +17,18 @@ pub struct Settings {
|
|||
pub refresh_ui_seconds: u64,
|
||||
pub downloader: DownloaderConfig,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
// Create new instance
|
||||
pub fn new(
|
||||
username: &str,
|
||||
password: &str,
|
||||
client_id: &str,
|
||||
client_secret: &str,
|
||||
) -> Option<Settings> {
|
||||
Some(Settings {
|
||||
pub fn new(username: &str, password: &str, client_id: &str, client_secret: &str) -> Settings {
|
||||
Settings {
|
||||
username: username.to_string(),
|
||||
password: password.to_string(),
|
||||
client_id: client_id.to_string(),
|
||||
client_secret: client_secret.to_string(),
|
||||
refresh_ui_seconds: 1,
|
||||
downloader: DownloaderConfig {
|
||||
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(),
|
||||
},
|
||||
})
|
||||
downloader: DownloaderConfig::new()
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize the settings to a json file
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
use aspotify::{
|
||||
Album, Artist, Client, ClientCredentials, Playlist, PlaylistItemType, Track, TrackSimplified,
|
||||
};
|
||||
use aspotify::{Album, Artist, Client, ClientCredentials, ItemType, Playlist, PlaylistItemType, Track, TrackSimplified};
|
||||
use librespot::core::authentication::Credentials;
|
||||
use librespot::core::config::SessionConfig;
|
||||
use librespot::core::session::Session;
|
||||
|
|
@ -88,6 +86,19 @@ impl Spotify {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get search results for query
|
||||
pub async fn search(&self, query: &str) -> Result<Vec<Track>, SpotifyError> {
|
||||
Ok(self
|
||||
.spotify
|
||||
.search()
|
||||
.search(query, [ItemType::Track], true, 50, 0, None)
|
||||
.await?
|
||||
.data
|
||||
.tracks
|
||||
.unwrap()
|
||||
.items)
|
||||
}
|
||||
|
||||
/// Get all tracks from playlist
|
||||
pub async fn full_playlist(&self, id: &str) -> Result<Vec<Track>, SpotifyError> {
|
||||
let mut items = vec![];
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use chrono::NaiveDate;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use crate::downloader::AudioFormat;
|
||||
|
|
@ -16,6 +17,7 @@ pub enum TagWrap {
|
|||
}
|
||||
|
||||
impl TagWrap {
|
||||
|
||||
/// Load from file
|
||||
pub fn new(path: impl AsRef<Path>, format: AudioFormat) -> Result<TagWrap, SpotifyError> {
|
||||
match format {
|
||||
|
|
@ -27,10 +29,10 @@ impl TagWrap {
|
|||
|
||||
/// Get Tag trait
|
||||
pub fn get_tag(&mut self) -> Box<&mut dyn Tag> {
|
||||
match self {
|
||||
TagWrap::Ogg(tag) => Box::new(tag),
|
||||
TagWrap::Id3(tag) => Box::new(tag),
|
||||
}
|
||||
Box::new(match self {
|
||||
TagWrap::Ogg(tag) => tag,
|
||||
TagWrap::Id3(tag) => tag,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue