Compare commits
No commits in common. "master" and "v0.1.0" have entirely different histories.
|
@ -11,9 +11,9 @@ readme = "README.md"
|
|||
|
||||
[dependencies]
|
||||
tokio-core = "0.1.17"
|
||||
librespot-core = { git = "https://github.com/librespot-org/librespot.git", rev = "a3c63b4e055f3ec68432d4a27479bed102e68e9e" }
|
||||
librespot-metadata = { git = "https://github.com/librespot-org/librespot.git", rev = "a3c63b4e055f3ec68432d4a27479bed102e68e9e" }
|
||||
librespot-audio = { git = "https://github.com/librespot-org/librespot.git", rev = "a3c63b4e055f3ec68432d4a27479bed102e68e9e" }
|
||||
librespot-core = { git = "https://github.com/librespot-org/librespot.git" }
|
||||
librespot-metadata = { git = "https://github.com/librespot-org/librespot.git" }
|
||||
librespot-audio = { git = "https://github.com/librespot-org/librespot.git" }
|
||||
regex = "1.1.0"
|
||||
log = "0.4.6"
|
||||
env_logger = "0.6.0"
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019 Lorenzo Pistone
|
||||
Copyright (c) 2018 Lorenzo Pistone
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
24
README.md
24
README.md
|
@ -4,27 +4,7 @@ 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.
|
||||
|
||||
# Usage
|
||||
To download a number of tracks as `"artists" - "title".ogg`, run
|
||||
```
|
||||
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
|
||||
oggify user password < tracks_list
|
||||
```
|
||||
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,7 +10,6 @@ extern crate tokio_core;
|
|||
|
||||
use std::env;
|
||||
use std::io::{self, BufRead, Read, Result};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
|
@ -29,7 +28,7 @@ fn main() {
|
|||
Builder::from_env(Env::default().default_filter_or("info")).init();
|
||||
|
||||
let args: Vec<_> = env::args().collect();
|
||||
assert!(args.len() == 3 || args.len() == 4, "Usage: {} user password [helper_script] < tracks_file", args[0]);
|
||||
assert!(args.len() == 3, "Usage: {} USERNAME PASSWORD < tracks_file", args[0]);
|
||||
|
||||
let mut core = Core::new().unwrap();
|
||||
let handle = core.handle();
|
||||
|
@ -54,12 +53,6 @@ fn main() {
|
|||
.and_then(|capture|SpotifyId::from_base62(&capture[1]).ok())))
|
||||
.for_each(|id|{
|
||||
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");
|
||||
if !track.available {
|
||||
warn!("Track {} is not available, finding alternative...", id.to_base62());
|
||||
|
@ -74,10 +67,12 @@ fn main() {
|
|||
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_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(" "));
|
||||
let file_id = track.files.get(&FileFormat::OGG_VORBIS_160)
|
||||
let file_id = track.files.get(&FileFormat::OGG_VORBIS_320)
|
||||
.or(track.files.get(&FileFormat::OGG_VORBIS_160))
|
||||
.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.");
|
||||
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();
|
||||
|
@ -96,24 +91,7 @@ fn main() {
|
|||
read_all.expect("Cannot read file stream");
|
||||
let mut decrypted_buffer = Vec::new();
|
||||
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");
|
||||
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");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
17
tag_ogg
17
tag_ogg
|
@ -1,17 +0,0 @@
|
|||
#!/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