Compare commits

...
Sign in to create a new pull request.

6 commits

Author SHA1 Message Date
grufkork
2ab4b57b4f
Merge pull request #4 from kompass/librespot-update
Fix hrs and min printing
2024-12-30 13:38:11 +01:00
Nicolas
2cbe41ad14
Fix hrs and min printing 2024-12-30 12:03:11 +01:00
grufkork
3a5224e440 Update 2024-09-10 13:43:45 +02:00
grufkork
3c1c4cad2e
Update README.md 2024-09-10 13:42:35 +02:00
grufkork
d98a84e1d0 Crude relink, pin dep, FLAC support (?) 2024-09-08 23:05:21 +02:00
grufkork
ec66b5ad91 Update to use latest librespot from GithHub repo 2024-09-08 13:24:17 +02:00
10 changed files with 1384 additions and 753 deletions

1997
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -30,7 +30,7 @@ reqwest = "0"
colored = "2" colored = "2"
lame = "0" lame = "0"
aspotify = "0" aspotify = "0"
librespot = { git = "ssh://git@github.com/oSumAtrIX/free-librespot.git" } librespot = { git = "https://github.com/librespot-org/librespot", rev = "f647331" }
async-std = { version = "1", features = ["attributes", "tokio1"] } async-std = { version = "1", features = ["attributes", "tokio1"] }
serde_json = "1" serde_json = "1"
async-stream = "0" async-stream = "0"

View file

@ -2,41 +2,9 @@
# DownOnSpot # DownOnSpot
A Spotify downloader written in Rust
<img src="assets/icon.svg" alt="drawing" width="500"/>
<br> <br>
[![Build project](https://github.com/oSumAtrIX/DownOnSpot/actions/workflows/build.yml/badge.svg)](https://github.com/oSumAtrIX/DownOnSpot/actions/workflows/build.yml)
[![GitHub license](https://img.shields.io/github/license/oSumAtrIX/DownOnSpot)](https://github.com/oSumAtrIX/DownOnSpot/blob/main/LICENSE)
[![GitHub issues](https://img.shields.io/github/issues/oSumAtrIX/DownOnSpot)](https://github.com/oSumAtrIX/DownOnSpot/issues)
[![GitHub forks](https://img.shields.io/github/forks/oSumAtrIX/DownOnSpot)](https://github.com/oSumAtrIX/DownOnSpot/network)
[![GitHub stars](https://img.shields.io/github/stars/oSumAtrIX/DownOnSpot)](https://github.com/oSumAtrIX/DownOnSpot/stargazers)
[![Stability: Experimental](https://masterminds.github.io/stability/experimental.svg)](https://masterminds.github.io/stability/experimental.html)
</div> </div>
## 🆘 Help needed
> [!NOTE]
Currently, I am [rewriting DownOnSpot](https://github.com/oSumAtrIX/DownOnSpot/pull/68).
If you want to help me accelerate this process, please feel free to contact me at [osumatrix.me](https://osumatrix.me).
## ⭐ Features
- ✅ Actually downloads from Spotify, free and premium
- ✅ Chose between 96, 160, 256 and 320 kbit/s (free users can't exceed 160kbit/s)
- ✅ Download tracks, playlists, albums and artists
- ✅ Multi-threaded
- ✅ Search for tracks
- ✅ Download MP3 and original OGG files
- ✅ Metadata tagging
- ✅ Simple CLI interface
> [!NOTE]
> Free Spotify users can not exceed 160kbit/s. Change the `quality` setting in the `settings.json` file to `Q160` or lower. If you want to download 256 or 320kbit/s, you need to use a premium account.
## ⚒️ Building ## ⚒️ Building
1. Clone the repository using git and change to the local repository directory: 1. Clone the repository using git and change to the local repository directory:
@ -58,12 +26,8 @@ If you want to help me accelerate this process, please feel free to contact me a
cargo build --release cargo build --release
``` ```
> [!NOTE]
> If you do not want to use `free-librespot` (i.e. if you are using a paid Spotify account), replace `git = "ssh://git@github.com/oSumAtrIX/free-librespot.git"` with `librespot = "0.4.2"` inside the `Cargo.toml` file.
## 🕹️ Usage ## 🕹️ Usage
1. Create a [new application](https://developer.spotify.com/dashboard/applications) on the Spotify developer dashboard
2. Run DownOnSpot 2. Run DownOnSpot
```bash ```bash
@ -111,12 +75,6 @@ You can use the following template variables for `path` and `filename_template`
- Slow MP3 downloads due to libmp3lame - Slow MP3 downloads due to libmp3lame
- Sporadic `channel error` when downloading tracks - Sporadic `channel error` when downloading tracks
## 💪 Contributors
<a href="https://github.com/osumatrix/downonspot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=osumatrix/downonspot" />
</a>
## 🔑 License ## 🔑 License
DownOnSpot is licensed under the GPLv3 licence. Please see the [licence file](LICENSE) for more information. DownOnSpot is licensed under the GPLv3 licence. Please see the [licence file](LICENSE) for more information.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -1,3 +1,4 @@
use aspotify::Tracks;
use async_std::channel::{bounded, Receiver, Sender}; use async_std::channel::{bounded, Receiver, Sender};
use async_stream::try_stream; use async_stream::try_stream;
use chrono::NaiveDate; use chrono::NaiveDate;
@ -7,7 +8,8 @@ use librespot::audio::{AudioDecrypt, AudioFile};
use librespot::core::audio_key::AudioKey; use librespot::core::audio_key::AudioKey;
use librespot::core::session::Session; use librespot::core::session::Session;
use librespot::core::spotify_id::SpotifyId; use librespot::core::spotify_id::SpotifyId;
use librespot::metadata::{FileFormat, Metadata, Track}; use librespot::metadata::{Metadata, Track};
use librespot::protocol::metadata::audio_file::Format as FileFormat;
use sanitize_filename::sanitize; use sanitize_filename::sanitize;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Display; use std::fmt::Display;
@ -199,6 +201,7 @@ async fn communication_thread(
} }
/// Spotify downloader /// Spotify downloader
pub struct DownloaderInternal { pub struct DownloaderInternal {
spotify: Spotify, spotify: Spotify,
pub tx: Sender<DownloaderMessage>, pub tx: Sender<DownloaderMessage>,
@ -483,16 +486,23 @@ impl DownloaderInternal {
Ok(()) Ok(())
} }
async fn find_alternative(session: &Session, track: Track) -> Result<Track, SpotifyError> {
for alt in track.alternatives { async fn find_alternative(session: &Session, track: Track) -> Result<Track, SpotifyError> {
let t = Track::get(session, alt).await?; let librespot::metadata::track::Tracks(ids) = track.alternatives;
if t.available { for id in ids {
return Ok(t); let t = Track::get(session, &id).await?;
} if !Self::track_has_alternatives(&t) {
} return Ok(t);
}
}
Err(SpotifyError::Unavailable) Err(SpotifyError::Unavailable)
} }
fn track_has_alternatives(track: &Track) -> bool {
let librespot::metadata::track::Tracks(alts) = &track.alternatives;
!alts.is_empty()
}
/// Download track by id /// Download track by id
async fn download_track( async fn download_track(
@ -504,12 +514,18 @@ impl DownloaderInternal {
job_id: i64, job_id: i64,
) -> Result<(PathBuf, AudioFormat), SpotifyError> { ) -> Result<(PathBuf, AudioFormat), SpotifyError> {
let id = SpotifyId::from_base62(id)?; let id = SpotifyId::from_base62(id)?;
let mut track = Track::get(session, id).await?; let mut track = Track::get(session, &id).await?;
// Fallback if unavailable // Fallback if unavailable
if !track.available { if Self::track_has_alternatives(&track) {
track = DownloaderInternal::find_alternative(session, track).await?; track = Self::find_alternative(session, track).await?;
} }
// if !track.available {
// track = DownloaderInternal::find_alternative(session, track).await?;
// } //TODO
// Quality fallback // Quality fallback
let mut quality = config.quality; let mut quality = config.quality;
@ -554,8 +570,8 @@ impl DownloaderInternal {
let path_clone = path.clone(); let path_clone = path.clone();
let key = session.audio_key().request(track.id, *file_id).await?; let key = session.audio_key().request(track.id, *file_id).await?;
let encrypted = AudioFile::open(session, *file_id, 1024 * 1024, true).await?; let encrypted = AudioFile::open(session, *file_id, 1024 * 1024).await?;
let size = encrypted.get_stream_loader_controller().len(); let size = encrypted.get_stream_loader_controller()?.len();
// Download // Download
let s = match config.convert_to_mp3 { let s = match config.convert_to_mp3 {
true => { true => {
@ -604,7 +620,7 @@ impl DownloaderInternal {
) -> impl Stream<Item = Result<usize, SpotifyError>> { ) -> impl Stream<Item = Result<usize, SpotifyError>> {
try_stream! { try_stream! {
let mut file = File::create(path).await?; let mut file = File::create(path).await?;
let mut decrypted = AudioDecrypt::new(key, encrypted); let mut decrypted = AudioDecrypt::new(Some(key), encrypted);
// Skip (i guess encrypted shit) // Skip (i guess encrypted shit)
let mut skip: [u8; 0xa7] = [0; 0xa7]; let mut skip: [u8; 0xa7] = [0; 0xa7];
let mut decrypted = tokio::task::spawn_blocking(move || { let mut decrypted = tokio::task::spawn_blocking(move || {
@ -642,7 +658,7 @@ impl DownloaderInternal {
) -> impl Stream<Item = Result<usize, SpotifyError>> { ) -> impl Stream<Item = Result<usize, SpotifyError>> {
try_stream! { try_stream! {
let mut file = File::create(path).await?; let mut file = File::create(path).await?;
let mut decrypted = AudioDecrypt::new(key, encrypted); let mut decrypted = AudioDecrypt::new(Some(key), encrypted);
// Skip (i guess encrypted shit) // Skip (i guess encrypted shit)
let mut skip: [u8; 0xa7] = [0; 0xa7]; let mut skip: [u8; 0xa7] = [0; 0xa7];
let decrypted = tokio::task::spawn_blocking(move || { let decrypted = tokio::task::spawn_blocking(move || {
@ -683,6 +699,7 @@ pub enum AudioFormat {
Aac, Aac,
Mp3, Mp3,
Mp4, Mp4,
Flac,
Unknown, Unknown,
} }
@ -694,6 +711,7 @@ impl AudioFormat {
AudioFormat::Aac => "m4a", AudioFormat::Aac => "m4a",
AudioFormat::Mp3 => "mp3", AudioFormat::Mp3 => "mp3",
AudioFormat::Mp4 => "mp4", AudioFormat::Mp4 => "mp4",
AudioFormat::Flac => "flac",
AudioFormat::Unknown => "", AudioFormat::Unknown => "",
} }
.to_string() .to_string()
@ -711,12 +729,9 @@ impl From<FileFormat> for AudioFormat {
FileFormat::MP3_160 => Self::Mp3, FileFormat::MP3_160 => Self::Mp3,
FileFormat::MP3_96 => Self::Mp3, FileFormat::MP3_96 => Self::Mp3,
FileFormat::MP3_160_ENC => Self::Mp3, FileFormat::MP3_160_ENC => Self::Mp3,
FileFormat::MP4_128_DUAL => Self::Mp4, FileFormat::AAC_24 => Self::Aac,
FileFormat::OTHER3 => Self::Unknown, FileFormat::AAC_48 => Self::Aac,
FileFormat::AAC_160 => Self::Aac, FileFormat::FLAC_FLAC => Self::Flac
FileFormat::AAC_320 => Self::Aac,
FileFormat::MP4_128 => Self::Mp4,
FileFormat::OTHER5 => Self::Unknown,
} }
} }
} }
@ -727,13 +742,13 @@ impl Quality {
match self { match self {
Self::Q320 => vec![ Self::Q320 => vec![
FileFormat::OGG_VORBIS_320, FileFormat::OGG_VORBIS_320,
FileFormat::AAC_320, FileFormat::AAC_48, // TODO
FileFormat::MP3_320, FileFormat::MP3_320,
], ],
Self::Q256 => vec![FileFormat::MP3_256], Self::Q256 => vec![FileFormat::MP3_256],
Self::Q160 => vec![ Self::Q160 => vec![
FileFormat::OGG_VORBIS_160, FileFormat::OGG_VORBIS_160,
FileFormat::AAC_160, FileFormat::AAC_24, // TODO
FileFormat::MP3_160, FileFormat::MP3_160,
], ],
Self::Q96 => vec![FileFormat::OGG_VORBIS_96, FileFormat::MP3_96], Self::Q96 => vec![FileFormat::OGG_VORBIS_96, FileFormat::MP3_96],

View file

@ -19,6 +19,8 @@ pub enum SpotifyError {
ID3Error(String, String), ID3Error(String, String),
Reqwest(String), Reqwest(String),
InvalidFormat, InvalidFormat,
NotConnected,
UnknownPacket(u8),
AlreadyDownloaded, AlreadyDownloaded,
} }
@ -43,6 +45,8 @@ impl fmt::Display for SpotifyError {
SpotifyError::ID3Error(k, e) => write!(f, "ID3 Error: {} {}", k, e), SpotifyError::ID3Error(k, e) => write!(f, "ID3 Error: {} {}", k, e),
SpotifyError::Reqwest(e) => write!(f, "Reqwest Error: {}", e), SpotifyError::Reqwest(e) => write!(f, "Reqwest Error: {}", e),
SpotifyError::InvalidFormat => write!(f, "Invalid Format!"), SpotifyError::InvalidFormat => write!(f, "Invalid Format!"),
SpotifyError::NotConnected => write!(f, "Not Connected"),
SpotifyError::UnknownPacket(e) => write!(f, "Unknown Packet: {}", e),
SpotifyError::AlreadyDownloaded => write!(f, "Already Downloaded"), SpotifyError::AlreadyDownloaded => write!(f, "Already Downloaded"),
} }
} }
@ -64,13 +68,21 @@ impl From<librespot::core::mercury::MercuryError> for SpotifyError {
} }
} }
impl From<librespot::core::error::Error> for SpotifyError {
fn from(e: librespot::core::error::Error) -> Self {
SpotifyError::Error(e.to_string())
}
}
impl From<librespot::core::session::SessionError> for SpotifyError { impl From<librespot::core::session::SessionError> for SpotifyError {
fn from(e: librespot::core::session::SessionError) -> Self { fn from(e: librespot::core::session::SessionError) -> Self {
match e { match e {
librespot::core::session::SessionError::IoError(e) => e.into(), librespot::core::session::SessionError::IoError(e) => e.into(),
librespot::core::session::SessionError::AuthenticationError(_) => { librespot::core::session::SessionError::AuthenticationError(_) => {
SpotifyError::AuthenticationError SpotifyError::AuthenticationError
} },
librespot::core::session::SessionError::NotConnected => SpotifyError::NotConnected,
librespot::core::session::SessionError::Packet(e) => SpotifyError::UnknownPacket(e)
} }
} }
} }

View file

@ -14,6 +14,7 @@ use async_std::task;
use colored::Colorize; use colored::Colorize;
use downloader::{DownloadState, Downloader}; use downloader::{DownloadState, Downloader};
use error::SpotifyError; use error::SpotifyError;
use librespot::core::spotify_id::SpotifyIdResult;
use settings::Settings; use settings::Settings;
use spotify::Spotify; use spotify::Spotify;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@ -333,7 +334,7 @@ async fn start() {
} }
fn secs_to_hrs_min_sec(secs: i32) -> String { fn secs_to_hrs_min_sec(secs: i32) -> String {
format!("{:0>2}:{:0>2}:{:0>2}", secs / 360, secs / 60, secs % 60) format!("{:0>2}:{:0>2}:{:0>2}", secs / 3600, (secs % 3600) / 60, secs % 60)
} }
// !cargo b --release // !cargo b --release

View file

@ -12,6 +12,7 @@ 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,
@ -34,13 +35,9 @@ impl Spotify {
Some(creds) => creds, Some(creds) => creds,
None => Credentials::with_password(username, password), None => Credentials::with_password(username, password),
}; };
let (session, _) = Session::connect(
SessionConfig::default(), let session = Session::new(SessionConfig::default(), Some(cache));
credentials, session.connect(credentials, true).await?;
Some(cache),
true,
)
.await?;
//aspotify //aspotify
let credentials = ClientCredentials { let credentials = ClientCredentials {