Add files

This commit is contained in:
Guillem Castro 2020-10-25 01:39:20 +02:00
commit 3fc874ce34
8 changed files with 254 additions and 0 deletions

13
.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
/target
.vscode
*.flac
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "librespot"]
path = librespot
url = https://github.com/librespot-org/librespot

20
Cargo.toml Normal file
View file

@ -0,0 +1,20 @@
[package]
name = "spotify-dl"
version = "0.1.0"
authors = ["Guillem Castro <guillemcastro4@gmail.com>"]
edition = "2018"
[dependencies]
librespot = { path = "./librespot" }
tokio-core = "0.1.17"
futures = "0.1"
futures-state-stream = "0.1"
structopt = { version = "0.3", default-features = false }
rpassword = "5.0"
indicatif = "0.15.0"
[dependencies.flac-bound]
version = "0.2.0"
[package.metadata.deb]
depends="libflac-dev"

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Guillem Castro
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

32
README.md Normal file
View file

@ -0,0 +1,32 @@
# spotify-dl
A command line utility to download songs and playlists directly from Spotify's servers.
You need a Spotify Premium account.
## Usage
```
spotify-dl 0.1.0
A commandline utility to download music directly from Spotify
USAGE:
spotify-dl [OPTIONS] --username <username> [tracks]...
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
-d, --destination <destination> The directory where the songs will be downloaded [default: .]
-u, --username <username> Your Spotify username
ARGS:
<tracks>... A list of Spotify URIs (songs, podcasts or playlists)
```
Songs and playlists must be passed as Spotify URIs (e.g. `spotify:track:123456789abcdefghABCDEF` for songs and `spotify:playlist:123456789abcdefghABCDEF` for playlists).
## License
spotify-dl is licensed under the MIT license. See [LICENSE](LICENSE).

1
librespot Submodule

@ -0,0 +1 @@
Subproject commit 1b1c22b6bc1e330fef731f5b3bc3b130d52a5bff

44
src/file_sink.rs Normal file
View file

@ -0,0 +1,44 @@
use librespot::playback::audio_backend::{Open, Sink};
use std::io::{self};
extern crate flac_bound;
use flac_bound::{FlacEncoder};
pub struct FileSink {
sink: String,
content: Vec<i32>
}
impl Open for FileSink {
fn open(path: Option<String>) -> Self {
if let Some(path) = path {
let file = path;
FileSink {
sink: file,
content: Vec::new()
}
} else {
panic!();
}
}
}
impl Sink for FileSink {
fn start(&mut self) -> io::Result<()> {
Ok(())
}
fn stop(&mut self) -> io::Result<()> {
let mut encoder = FlacEncoder::new().unwrap().channels(2).bits_per_sample(16).compression_level(0).init_file(&self.sink).unwrap();
encoder.process_interleaved(self.content.as_slice(), (self.content.len()/2) as u32).unwrap();
encoder.finish().unwrap();
Ok(())
}
fn write(&mut self, data: &[i16]) -> io::Result<()> {
let mut input: Vec<i32> = data.iter().map(|el| i32::from(*el)).collect();
self.content.append(&mut input);
Ok(())
}
}

120
src/main.rs Normal file
View file

@ -0,0 +1,120 @@
mod file_sink;
extern crate rpassword;
use std::{path::PathBuf};
use tokio_core::reactor::Core;
use librespot::{core::authentication::Credentials, metadata::Playlist};
use librespot::core::config::SessionConfig;
use librespot::core::session::Session;
use librespot::core::spotify_id::SpotifyId;
use librespot::playback::config::PlayerConfig;
use librespot::playback::player::Player;
use librespot::playback::audio_backend::{Open};
use librespot::metadata::{Track, Artist, Metadata};
use structopt::StructOpt;
use indicatif::{ProgressBar, ProgressStyle};
#[derive(Debug, StructOpt)]
#[structopt(name = "spotify-dl", about = "A commandline utility to download music directly from Spotify")]
struct Opt {
#[structopt(help = "A list of Spotify URIs (songs, podcasts or playlists)")]
tracks: Vec<String>,
#[structopt(short = "u", long = "username", help = "Your Spotify username")]
username: String,
#[structopt(short = "d", long = "destination", default_value = ".", help = "The directory where the songs will be downloaded")]
destination: String
}
fn create_session(core: &mut Core, credentials: Credentials) -> Session {
let session_config = SessionConfig::default();
let session = core.run(Session::connect(session_config, credentials, None, core.handle()))
.unwrap();
session
}
fn download_tracks(core: &mut Core, session: &Session, destination: PathBuf, tracks: Vec<SpotifyId>) {
let player_config = PlayerConfig::default();
let bar_style = ProgressStyle::default_bar()
.template("[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} (ETA: {eta}) {msg}")
.progress_chars("##-");
let bar = ProgressBar::new(tracks.len() as u64);
bar.set_style(bar_style);
bar.enable_steady_tick(500);
for track in tracks {
let track_item = core.run(Track::get(&session, track)).unwrap();
let artist_name: String;
if track_item.artists.len() > 1 {
let mut tmp: String = String::new();
for artist in track_item.artists {
let artist_item = core.run(Artist::get(&session, artist)).unwrap();
tmp.push_str(artist_item.name.as_str());
tmp.push_str(", ");
}
artist_name = String::from(tmp.trim_end_matches(", "));
}
else {
artist_name = core.run(Artist::get(&session, track_item.artists[0])).unwrap().name;
}
let full_track_name = format!("{} - {}", artist_name, track_item.name);
let filename = format!("{}.flac", full_track_name);
let joined_path = destination.join(&filename);
let path = joined_path.to_str().unwrap();
bar.set_message(full_track_name.as_str());
let file_sink = file_sink::FileSink::open(Some(path.to_owned()));
let (mut player, _) = Player::new(player_config.clone(), session.clone(), None, move || {
Box::new(file_sink)
});
player.load(track, true, 0);
core.run(player.get_end_of_track_future()).unwrap();
player.stop();
bar.inc(1);
}
bar.finish();
}
fn main() {
let opt = Opt::from_args();
let mut core = Core::new().unwrap();
let username = opt.username;
let password = rpassword::read_password_from_tty(Some("Password: ")).unwrap();
let credentials = Credentials::with_password(username, password);
let session = create_session(&mut core, credentials.clone());
let mut tracks: Vec<SpotifyId> = Vec::new();
for track_url in opt.tracks {
let track = SpotifyId::from_uri(track_url.as_str()).unwrap();
match &track.audio_type {
librespot::core::spotify_id::SpotifyAudioType::Track => {
tracks.push(track);
}
librespot::core::spotify_id::SpotifyAudioType::Podcast => {
tracks.push(track);
}
librespot::core::spotify_id::SpotifyAudioType::NonPlayable => {
match core.run(Playlist::get(&session, track)) {
Ok(mut playlist) => {
println!("Adding all songs from playlist {} (by {}) to the queue", &playlist.name, &playlist.user);
tracks.append(&mut playlist.tracks);
}
Err(_) => {
println!("Unsupported track {}", &track_url);
}
}
}
}
}
download_tracks(&mut core, &session, PathBuf::from(opt.destination), tracks);
}