Compare commits
18 Commits
Author | SHA1 | Date |
---|---|---|
|
5a027fa538 | |
|
fe66ff977a | |
|
f11fb48ce7 | |
|
9deaba0e63 | |
|
eb82e3c5b9 | |
|
ba672d1f91 | |
|
a805996ee0 | |
|
02f1c17da9 | |
|
f7b0e2f17d | |
|
db389c18aa | |
|
6d11933999 | |
|
264538e3ab | |
|
9d76391b17 | |
|
99f61a7e85 | |
|
124c08cfb8 | |
|
348f885920 | |
|
ffd5b407e9 | |
|
9ad4ee771c |
|
@ -11,9 +11,9 @@ readme = "README.md"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio-core = "0.1.17"
|
tokio-core = "0.1.17"
|
||||||
librespot-core = { git = "https://github.com/librespot-org/librespot.git" }
|
librespot-core = { git = "https://github.com/librespot-org/librespot.git", rev = "a3c63b4e055f3ec68432d4a27479bed102e68e9e" }
|
||||||
librespot-metadata = { git = "https://github.com/librespot-org/librespot.git" }
|
librespot-metadata = { git = "https://github.com/librespot-org/librespot.git", rev = "a3c63b4e055f3ec68432d4a27479bed102e68e9e" }
|
||||||
librespot-audio = { git = "https://github.com/librespot-org/librespot.git" }
|
librespot-audio = { git = "https://github.com/librespot-org/librespot.git", rev = "a3c63b4e055f3ec68432d4a27479bed102e68e9e" }
|
||||||
regex = "1.1.0"
|
regex = "1.1.0"
|
||||||
log = "0.4.6"
|
log = "0.4.6"
|
||||||
env_logger = "0.6.0"
|
env_logger = "0.6.0"
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2018 Lorenzo Pistone
|
Copyright (c) 2019 Lorenzo Pistone
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
24
README.md
24
README.md
|
@ -4,7 +4,27 @@ Download Spotify tracks to Ogg Vorbis (with a premium account).
|
||||||
This library uses [librespot](https://github.com/librespot-org/librespot). It is my first program in Rust so you may see some horrors in the way I handle tokio, futures and such.
|
This library uses [librespot](https://github.com/librespot-org/librespot). It is my first program in Rust so you may see some horrors in the way I handle tokio, futures and such.
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
To download a number of tracks as `"artists" - "title".ogg`, run
|
||||||
```
|
```
|
||||||
oggify user password < tracks_list
|
oggify "spotify-premium-user" "spotify-premium-password" < tracks_list
|
||||||
|
```
|
||||||
|
Oggify reads from stdin and looks for a track URL or URI in each line. The two formats are those you get with the track menu items "Share->Copy Song Link" or "Share->Copy Song URI" in the Spotify client, for example `open.spotify.com/track/1xPQDRSXDN5QJWm7qHg5Ku` or `spotify:track:1xPQDRSXDN5QJWm7qHg5Ku`.
|
||||||
|
|
||||||
|
## Helper script
|
||||||
|
A second form of invocation of oggify is
|
||||||
|
```
|
||||||
|
oggify "spotify-premium-user" "spotify-premium-password" "helper_script" < tracks_list
|
||||||
|
```
|
||||||
|
In this form `helper_script` is invoked for each new track:
|
||||||
|
```
|
||||||
|
helper_script "spotify_id" "title" "album" "artist1" ["artist2"...] < ogg_stream
|
||||||
|
```
|
||||||
|
The script `tag_ogg` in the source tree can be used to automatically add the track information (spotify ID, title, album, artists) as vorbis comments.
|
||||||
|
|
||||||
|
### Converting to MP3
|
||||||
|
Use `oggify` with the `tag_ogg` helper script as described above, then convert with ffmpeg:
|
||||||
|
```
|
||||||
|
for ogg in *.ogg; do
|
||||||
|
ffmpeg -i "$ogg" -map_metadata 0:s:0 -id3v2_version 3 -codec:a libmp3lame -qscale:a 2 "$(basename "$ogg" .ogg).mp3"
|
||||||
|
done
|
||||||
```
|
```
|
||||||
The program takes 2 arguments, your Spotify Premium user and password, then reads from stdin and looks for a track URL or URI in each line. The two formats are those you get with the track menu items "Share->Copy Song Link" or "Share->Copy Song URI" in the Spotify client, for example `open.spotify.com/track/1xPQDRSXDN5QJWm7qHg5Ku` or `spotify:track:1xPQDRSXDN5QJWm7qHg5Ku`. For example,
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ extern crate tokio_core;
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::io::{self, BufRead, Read, Result};
|
use std::io::{self, BufRead, Read, Result};
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
@ -28,7 +29,7 @@ fn main() {
|
||||||
Builder::from_env(Env::default().default_filter_or("info")).init();
|
Builder::from_env(Env::default().default_filter_or("info")).init();
|
||||||
|
|
||||||
let args: Vec<_> = env::args().collect();
|
let args: Vec<_> = env::args().collect();
|
||||||
assert!(args.len() == 3, "Usage: {} USERNAME PASSWORD < tracks_file", args[0]);
|
assert!(args.len() == 3 || args.len() == 4, "Usage: {} user password [helper_script] < tracks_file", args[0]);
|
||||||
|
|
||||||
let mut core = Core::new().unwrap();
|
let mut core = Core::new().unwrap();
|
||||||
let handle = core.handle();
|
let handle = core.handle();
|
||||||
|
@ -53,6 +54,12 @@ fn main() {
|
||||||
.and_then(|capture|SpotifyId::from_base62(&capture[1]).ok())))
|
.and_then(|capture|SpotifyId::from_base62(&capture[1]).ok())))
|
||||||
.for_each(|id|{
|
.for_each(|id|{
|
||||||
info!("Getting track {}...", id.to_base62());
|
info!("Getting track {}...", id.to_base62());
|
||||||
|
let fname = format!("{}.ogg", id.to_base62());
|
||||||
|
use std::path::Path;
|
||||||
|
if !Path::new(&fname).exists() {
|
||||||
|
info!("File {} already exists... Skipping...", id.to_base62());
|
||||||
|
} else {
|
||||||
|
info!("File does not exist, continuing. Errors {}", Path::new(&fname).exists());
|
||||||
let mut track = core.run(Track::get(&session, id)).expect("Cannot get track metadata");
|
let mut track = core.run(Track::get(&session, id)).expect("Cannot get track metadata");
|
||||||
if !track.available {
|
if !track.available {
|
||||||
warn!("Track {} is not available, finding alternative...", id.to_base62());
|
warn!("Track {} is not available, finding alternative...", id.to_base62());
|
||||||
|
@ -67,12 +74,10 @@ fn main() {
|
||||||
warn!("Found track alternative {} -> {}", id.to_base62(), track.id.to_base62());
|
warn!("Found track alternative {} -> {}", id.to_base62(), track.id.to_base62());
|
||||||
}
|
}
|
||||||
let artists_strs: Vec<_> = track.artists.iter().map(|id|core.run(Artist::get(&session, *id)).expect("Cannot get artist metadata").name).collect();
|
let artists_strs: Vec<_> = track.artists.iter().map(|id|core.run(Artist::get(&session, *id)).expect("Cannot get artist metadata").name).collect();
|
||||||
let artists_display = artists_strs.join(", ");
|
|
||||||
let fname = format!("{} - {}.ogg", artists_display, track.name);
|
|
||||||
debug!("File formats: {}", track.files.keys().map(|filetype|format!("{:?}", filetype)).collect::<Vec<_>>().join(" "));
|
debug!("File formats: {}", track.files.keys().map(|filetype|format!("{:?}", filetype)).collect::<Vec<_>>().join(" "));
|
||||||
let file_id = track.files.get(&FileFormat::OGG_VORBIS_320)
|
let file_id = track.files.get(&FileFormat::OGG_VORBIS_160)
|
||||||
.or(track.files.get(&FileFormat::OGG_VORBIS_160))
|
|
||||||
.or(track.files.get(&FileFormat::OGG_VORBIS_96))
|
.or(track.files.get(&FileFormat::OGG_VORBIS_96))
|
||||||
|
.or(track.files.get(&FileFormat::OGG_VORBIS_320))
|
||||||
.expect("Could not find a OGG_VORBIS format for the track.");
|
.expect("Could not find a OGG_VORBIS format for the track.");
|
||||||
let key = core.run(session.audio_key().request(track.id, *file_id)).expect("Cannot get audio key");
|
let key = core.run(session.audio_key().request(track.id, *file_id)).expect("Cannot get audio key");
|
||||||
let mut encrypted_file = core.run(AudioFile::open(&session, *file_id)).unwrap();
|
let mut encrypted_file = core.run(AudioFile::open(&session, *file_id)).unwrap();
|
||||||
|
@ -91,7 +96,24 @@ fn main() {
|
||||||
read_all.expect("Cannot read file stream");
|
read_all.expect("Cannot read file stream");
|
||||||
let mut decrypted_buffer = Vec::new();
|
let mut decrypted_buffer = Vec::new();
|
||||||
AudioDecrypt::new(key, &buffer[..]).read_to_end(&mut decrypted_buffer).expect("Cannot decrypt stream");
|
AudioDecrypt::new(key, &buffer[..]).read_to_end(&mut decrypted_buffer).expect("Cannot decrypt stream");
|
||||||
|
if args.len() == 3 {
|
||||||
|
let fname = format!("{}.ogg", id.to_base62());
|
||||||
std::fs::write(&fname, &decrypted_buffer[0xa7..]).expect("Cannot write decrypted track");
|
std::fs::write(&fname, &decrypted_buffer[0xa7..]).expect("Cannot write decrypted track");
|
||||||
info!("Filename: {}", fname);
|
info!("Filename: {}", fname);
|
||||||
|
} else {
|
||||||
|
// let album = core.run(Album::get(&session, track.album)).expect("Cannot get album metadata");
|
||||||
|
let fname = format!("{}.ogg", id.to_base62());
|
||||||
|
std::fs::write(&fname, &decrypted_buffer[0xa7..]).expect("Cannot write decrypted track");
|
||||||
|
info!("Filename: {}", fname);
|
||||||
|
let mut cmd = Command::new(args[3].to_owned());
|
||||||
|
cmd.stdin(Stdio::piped());
|
||||||
|
cmd.arg(id.to_base62()).arg(track.name).args(artists_strs.iter());
|
||||||
|
info!("Running Helper");
|
||||||
|
cmd.spawn().expect("Could not run helper program");
|
||||||
|
// let pipe = child.stdin.as_mut().expect("Could not open helper stdin");
|
||||||
|
// pipe.write_all(&decrypted_buffer[0xa7..]).expect("Failed to write to stdin");
|
||||||
|
// assert!(child.wait().expect("Out of ideas for error messages").success(), "Helper script returned an error");
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
fname="${4} - ${2}.ogg"
|
||||||
|
fname="${fname//\//-}"
|
||||||
|
cat > "${fname}"
|
||||||
|
{
|
||||||
|
echo "SPOTIFY_ID=${1}"
|
||||||
|
echo "TITLE=${2//'\n'/' '}"
|
||||||
|
echo "ALBUM=${3//'\n'/' '}"
|
||||||
|
shift 3
|
||||||
|
for artist in "$@"; do
|
||||||
|
echo "ARTIST=${artist//'\n'/' '}"
|
||||||
|
done
|
||||||
|
} | vorbiscomment -a "${fname}"
|
||||||
|
echo "${fname}"
|
Loading…
Reference in New Issue