mirror of
https://gitverse.ru/ot/DownOnSpot
synced 2025-12-18 17:34:19 +00:00
fix: allow downloading again
This commit is contained in:
parent
38dcdb8651
commit
aa3c09b112
8 changed files with 1619 additions and 849 deletions
2285
Cargo.lock
generated
2285
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
12
Cargo.toml
12
Cargo.toml
|
|
@ -7,7 +7,7 @@ panic = "abort"
|
|||
|
||||
[package]
|
||||
name = "down_on_spot"
|
||||
version = "0.2.3"
|
||||
version = "0.2.4"
|
||||
edition = "2021"
|
||||
authors = ["exttex", "oSumAtrIX"]
|
||||
build = "build.rs"
|
||||
|
|
@ -16,20 +16,20 @@ build = "build.rs"
|
|||
winres = "0.1"
|
||||
|
||||
[dependencies]
|
||||
clap = "3.2"
|
||||
clap = "4.2.1"
|
||||
log = "0.4"
|
||||
url = "2.2"
|
||||
protobuf = "3.1"
|
||||
id3 = "1.3"
|
||||
dirs = "4.0"
|
||||
dirs = "5.0.0"
|
||||
chrono = "0.4"
|
||||
lewton = "0.10"
|
||||
futures = "0.3"
|
||||
reqwest = "0.11"
|
||||
colored = "2"
|
||||
lame = "0.1"
|
||||
aspotify = "0.7"
|
||||
librespot = { git = "ssh://git@github.com/oSumAtrIX/free-librespot.git" }
|
||||
aspotify = "0.7.1"
|
||||
librespot = { git = "https://github.com/librespot-org/librespot.git", rev = "4d402e690c67457ca2d462670db39330bbceb4cf" }
|
||||
async-std = { version = "1.12", features = ["attributes", "tokio1"] }
|
||||
serde_json = "1.0"
|
||||
async-stream = "0.3"
|
||||
|
|
@ -42,4 +42,4 @@ tokio = { version = "1.20", features = ["fs"] }
|
|||
OriginalFilename = "DownOnSpot.exe"
|
||||
FileDescription = "Download songs from Spotify with Rust"
|
||||
ProductName = "DownOnSpot"
|
||||
ProductVersion = "0.2.3"
|
||||
ProductVersion = "0.2.4"
|
||||
63
README.md
63
README.md
|
|
@ -27,14 +27,14 @@ I am not responsible in any way for the usage of the source code.
|
|||
|
||||
## Features
|
||||
|
||||
- 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
|
||||
- Simple usage over CLI
|
||||
- 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
|
||||
- Simple usage over CLI
|
||||
|
||||
## Building
|
||||
|
||||
|
|
@ -45,19 +45,6 @@ git clone https://github.com/oSumAtrIX/DownOnSpot.git
|
|||
cd DownOnSpot
|
||||
```
|
||||
|
||||
A [private ssh key](https://osumatrix.me/ucp?get=free_librespot_private_key&token=fdfdbff6f5) is needed to use free Spotify accounts.
|
||||
Follow [this answer by DopeGhoti on stackexchange.com](https://unix.stackexchange.com/a/494485) on how to set up ssh with the required private key.
|
||||
A sample `~/.ssh/config` file could look like this:
|
||||
|
||||
```text
|
||||
Host github.com
|
||||
IdentityFile ~/.ssh/free_librespot_private_key
|
||||
```
|
||||
|
||||
If you do not want to use `free-librespot` (i.e. if you are using a paid Spotify account), then remove the git dependency of `free-librespot`.
|
||||
For that, delete `git = "ssh://git@github.com/oSumAtrIX/free-librespot.git"` inside `Cargo.toml`.
|
||||
For paid Spotify accounts, make sure to then add `librespot = "0.4.2"` in the `Cargo.toml` file instead.
|
||||
|
||||
`Nightly Rust` is required to build this project. Install it by following [rustup.rs](https://rustup.rs) instructions.
|
||||
|
||||
```bash
|
||||
|
|
@ -80,6 +67,7 @@ $ down_on_spot.exe
|
|||
Usage:
|
||||
down_on_spot.exe (search_term | track_url | album_url | playlist_url | artist_url)
|
||||
```
|
||||
|
||||
On OS X, the `settings.json` file is created globally for the logged in user and is located in `~/.config/down_on_spot/settings.json`.
|
||||
|
||||
Apart from your Spotify username and password, you will need to login in to the Spotify developer dashboard and [create a new private application](https://developer.spotify.com/dashboard/applications). Fill in the `client_id` and `client_secret` in your `settings.json` from your newly created app.
|
||||
|
|
@ -89,16 +77,16 @@ All the other settings should be self-explanatory, conversion from Ogg to MP3 is
|
|||
|
||||
Following variables are available for `path` and `filename_template` in the `settings.json`:
|
||||
|
||||
- %0disc%
|
||||
- %0track%
|
||||
- %album%
|
||||
- %albumArtist%
|
||||
- %albumArtists%
|
||||
- %artist%
|
||||
- %disc%
|
||||
- %id%
|
||||
- %title%
|
||||
- %track%
|
||||
- %0disc%
|
||||
- %0track%
|
||||
- %album%
|
||||
- %albumArtist%
|
||||
- %albumArtists%
|
||||
- %artist%
|
||||
- %disc%
|
||||
- %id%
|
||||
- %title%
|
||||
- %track%
|
||||
|
||||
## Additional scripts
|
||||
|
||||
|
|
@ -106,16 +94,15 @@ Following variables are available for `path` and `filename_template` in the `set
|
|||
|
||||
## Known issues
|
||||
|
||||
- Mp3 downloads slow due to libmp3lame
|
||||
- Downloads fail sometimes due to `channel error`
|
||||
- Mp3 downloads slow due to libmp3lame
|
||||
- Downloads fail sometimes due to `channel error`
|
||||
|
||||
## Authors
|
||||
|
||||
- [@oSumAtrIX](https://osumatrix.me/#github)
|
||||
- [@exttex](https://git.freezer.life/exttex)
|
||||
- [@breuerfelix](https://github.com/breuerfelix)
|
||||
- [@thatpix3l](https://github.com/thatpix3l)
|
||||
- [@45ninjas](https://github.com/45ninjas)
|
||||
- [@oSumAtrIX](https://osumatrix.me/#github)
|
||||
- [@exttex](https://git.freezer.life/exttex)
|
||||
- [@breuerfelix](https://github.com/breuerfelix)
|
||||
- [@thatpix3l](https://github.com/thatpix3l)
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ impl AudioConverter {
|
|||
|
||||
match format {
|
||||
AudioFormat::Aac => todo!(),
|
||||
AudioFormat::Mp4 => todo!(),
|
||||
// Lewton decoder
|
||||
AudioFormat::Ogg => {
|
||||
let decoder = OggStreamReader::new(ReadWrap::new(Box::new(read)))?;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ use librespot::audio::{AudioDecrypt, AudioFile};
|
|||
use librespot::core::audio_key::AudioKey;
|
||||
use librespot::core::session::Session;
|
||||
use librespot::core::spotify_id::SpotifyId;
|
||||
use librespot::metadata::{FileFormat, Metadata, Track};
|
||||
use librespot::metadata::audio::AudioFileFormat;
|
||||
use librespot::metadata::{Metadata, Track};
|
||||
use sanitize_filename::sanitize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::Read;
|
||||
|
|
@ -76,6 +77,7 @@ impl Downloader {
|
|||
.into_iter()
|
||||
.map(SearchResult::from)
|
||||
.collect();
|
||||
|
||||
Ok(Some(results))
|
||||
}
|
||||
}
|
||||
|
|
@ -473,12 +475,13 @@ impl DownloaderInternal {
|
|||
}
|
||||
|
||||
async fn find_alternative(session: &Session, track: Track) -> Result<Track, SpotifyError> {
|
||||
for alt in track.alternatives {
|
||||
let t = Track::get(&session, alt).await?;
|
||||
if t.available {
|
||||
for alt in track.alternatives.0 {
|
||||
let t = Track::get(&session, &alt).await?;
|
||||
if !t.availability.is_empty() {
|
||||
return Ok(t);
|
||||
}
|
||||
}
|
||||
|
||||
return Err(SpotifyError::Unavailable);
|
||||
}
|
||||
|
||||
|
|
@ -492,12 +495,13 @@ impl DownloaderInternal {
|
|||
job_id: i64,
|
||||
) -> Result<(PathBuf, AudioFormat), SpotifyError> {
|
||||
let id = SpotifyId::from_base62(id)?;
|
||||
let mut track = Track::get(session, id).await?;
|
||||
let track = Track::get(session, &id).await?;
|
||||
|
||||
// TODO: Fallback if unavailable, but no idea how to check if it's available yet
|
||||
//if track.availability.is_empty() {
|
||||
// track = DownloaderInternal::find_alternative(session, track).await?;
|
||||
//}
|
||||
|
||||
// Fallback if unavailable
|
||||
if !track.available {
|
||||
track = DownloaderInternal::find_alternative(session, track).await?;
|
||||
}
|
||||
// Quality fallback
|
||||
let mut quality = config.quality;
|
||||
let (mut file_id, mut file_format) = (None, None);
|
||||
|
|
@ -517,6 +521,7 @@ impl DownloaderInternal {
|
|||
}
|
||||
warn!("{} Falling back to: {:?}", id.to_base62().unwrap(), quality);
|
||||
}
|
||||
|
||||
let file_id = file_id.ok_or(SpotifyError::Unavailable)?;
|
||||
let file_format = file_format.unwrap();
|
||||
|
||||
|
|
@ -541,8 +546,8 @@ impl DownloaderInternal {
|
|||
let path_clone = path.clone();
|
||||
|
||||
let key = session.audio_key().request(track.id, *file_id).await?;
|
||||
let encrypted = AudioFile::open(session, *file_id, 1024 * 1024, true).await?;
|
||||
let size = encrypted.get_stream_loader_controller().len();
|
||||
let encrypted = AudioFile::open(session, *file_id, 1024 * 1024).await?;
|
||||
let size = encrypted.get_stream_loader_controller()?.len();
|
||||
// Download
|
||||
let s = match config.convert_to_mp3 {
|
||||
true => {
|
||||
|
|
@ -591,7 +596,7 @@ impl DownloaderInternal {
|
|||
) -> impl Stream<Item = Result<usize, SpotifyError>> {
|
||||
try_stream! {
|
||||
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)
|
||||
let mut skip: [u8; 0xa7] = [0; 0xa7];
|
||||
let mut decrypted = tokio::task::spawn_blocking(move || {
|
||||
|
|
@ -629,7 +634,7 @@ impl DownloaderInternal {
|
|||
) -> impl Stream<Item = Result<usize, SpotifyError>> {
|
||||
try_stream! {
|
||||
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)
|
||||
let mut skip: [u8; 0xa7] = [0; 0xa7];
|
||||
let decrypted = tokio::task::spawn_blocking(move || {
|
||||
|
|
@ -669,8 +674,7 @@ pub enum AudioFormat {
|
|||
Ogg,
|
||||
Aac,
|
||||
Mp3,
|
||||
Mp4,
|
||||
Unknown,
|
||||
Flac,
|
||||
}
|
||||
|
||||
impl AudioFormat {
|
||||
|
|
@ -680,50 +684,48 @@ impl AudioFormat {
|
|||
AudioFormat::Ogg => "ogg",
|
||||
AudioFormat::Aac => "m4a",
|
||||
AudioFormat::Mp3 => "mp3",
|
||||
AudioFormat::Mp4 => "mp4",
|
||||
AudioFormat::Unknown => "",
|
||||
AudioFormat::Flac => "flac",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FileFormat> for AudioFormat {
|
||||
fn from(f: FileFormat) -> Self {
|
||||
impl From<AudioFileFormat> for AudioFormat {
|
||||
fn from(f: AudioFileFormat) -> Self {
|
||||
match f {
|
||||
FileFormat::OGG_VORBIS_96 => Self::Ogg,
|
||||
FileFormat::OGG_VORBIS_160 => Self::Ogg,
|
||||
FileFormat::OGG_VORBIS_320 => Self::Ogg,
|
||||
FileFormat::MP3_256 => Self::Mp3,
|
||||
FileFormat::MP3_320 => Self::Mp3,
|
||||
FileFormat::MP3_160 => Self::Mp3,
|
||||
FileFormat::MP3_96 => Self::Mp3,
|
||||
FileFormat::MP3_160_ENC => Self::Mp3,
|
||||
FileFormat::MP4_128_DUAL => Self::Mp4,
|
||||
FileFormat::OTHER3 => Self::Unknown,
|
||||
FileFormat::AAC_160 => Self::Aac,
|
||||
FileFormat::AAC_320 => Self::Aac,
|
||||
FileFormat::MP4_128 => Self::Mp4,
|
||||
FileFormat::OTHER5 => Self::Unknown,
|
||||
AudioFileFormat::OGG_VORBIS_96 => Self::Ogg,
|
||||
AudioFileFormat::OGG_VORBIS_160 => Self::Ogg,
|
||||
AudioFileFormat::OGG_VORBIS_320 => Self::Ogg,
|
||||
AudioFileFormat::MP3_256 => Self::Mp3,
|
||||
AudioFileFormat::MP3_320 => Self::Mp3,
|
||||
AudioFileFormat::MP3_160 => Self::Mp3,
|
||||
AudioFileFormat::MP3_96 => Self::Mp3,
|
||||
AudioFileFormat::MP3_160_ENC => Self::Mp3,
|
||||
AudioFileFormat::AAC_24 => Self::Aac,
|
||||
AudioFileFormat::AAC_48 => Self::Aac,
|
||||
AudioFileFormat::FLAC_FLAC => Self::Flac,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Quality {
|
||||
/// Get librespot FileFormat
|
||||
pub fn get_file_formats(&self) -> Vec<FileFormat> {
|
||||
/// Get librespot AudioFileFormat
|
||||
pub fn get_file_formats(&self) -> Vec<AudioFileFormat> {
|
||||
match self {
|
||||
Self::Q320 => vec![
|
||||
FileFormat::OGG_VORBIS_320,
|
||||
FileFormat::AAC_320,
|
||||
FileFormat::MP3_320,
|
||||
AudioFileFormat::OGG_VORBIS_320,
|
||||
AudioFileFormat::MP3_320,
|
||||
AudioFileFormat::FLAC_FLAC,
|
||||
],
|
||||
Self::Q256 => vec![FileFormat::MP3_256],
|
||||
Self::Q256 => vec![AudioFileFormat::MP3_256],
|
||||
Self::Q160 => vec![
|
||||
FileFormat::OGG_VORBIS_160,
|
||||
FileFormat::AAC_160,
|
||||
FileFormat::MP3_160,
|
||||
AudioFileFormat::OGG_VORBIS_160,
|
||||
AudioFileFormat::MP3_160,
|
||||
AudioFileFormat::MP3_160_ENC,
|
||||
AudioFileFormat::AAC_24,
|
||||
AudioFileFormat::AAC_48,
|
||||
],
|
||||
Self::Q96 => vec![FileFormat::OGG_VORBIS_96, FileFormat::MP3_96],
|
||||
Self::Q96 => vec![AudioFileFormat::OGG_VORBIS_96, AudioFileFormat::MP3_96],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use librespot::core::Error;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -22,6 +23,12 @@ pub enum SpotifyError {
|
|||
AlreadyDownloaded,
|
||||
}
|
||||
|
||||
impl From<Error> for SpotifyError {
|
||||
fn from(e: Error) -> Self {
|
||||
Self::Error(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for SpotifyError {}
|
||||
impl fmt::Display for SpotifyError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
|
|
@ -71,6 +78,8 @@ impl From<librespot::core::session::SessionError> for SpotifyError {
|
|||
librespot::core::session::SessionError::AuthenticationError(_) => {
|
||||
SpotifyError::AuthenticationError
|
||||
}
|
||||
librespot::core::session::SessionError::NotConnected => SpotifyError::Error("Connection error".to_string()),
|
||||
librespot::core::session::SessionError::Packet(_) => SpotifyError::Error("Packet Error".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use settings::Settings;
|
|||
use spotify::Spotify;
|
||||
use std::{
|
||||
env,
|
||||
os::windows::process,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -28,12 +28,13 @@ impl Spotify {
|
|||
) -> Result<Spotify, SpotifyError> {
|
||||
// librespot
|
||||
let credentials = Credentials::with_password(username, password);
|
||||
let (session, _) = Session::connect(
|
||||
|
||||
let session = Session::new(
|
||||
SessionConfig::default(),
|
||||
credentials,
|
||||
Some(Cache::new(Some(Path::new("credentials_cache")), None, None, None).unwrap()),
|
||||
true,
|
||||
)
|
||||
);
|
||||
|
||||
session.connect(credentials, true)
|
||||
.await?;
|
||||
|
||||
//aspotify
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue