mirror of
https://github.com/GuillemCastro/spotify-dl.git
synced 2024-11-23 18:50:24 +01:00
Add files
This commit is contained in:
commit
3fc874ce34
8 changed files with 254 additions and 0 deletions
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal 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
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "librespot"]
|
||||
path = librespot
|
||||
url = https://github.com/librespot-org/librespot
|
20
Cargo.toml
Normal file
20
Cargo.toml
Normal 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
21
LICENSE
Normal 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
32
README.md
Normal 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
1
librespot
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 1b1c22b6bc1e330fef731f5b3bc3b130d52a5bff
|
44
src/file_sink.rs
Normal file
44
src/file_sink.rs
Normal 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
120
src/main.rs
Normal 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);
|
||||
|
||||
}
|
Loading…
Reference in a new issue