Compare commits

..

No commits in common. "master" and "v0.1.0" have entirely different histories.

5 changed files with 44 additions and 103 deletions

View File

@ -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", rev = "a3c63b4e055f3ec68432d4a27479bed102e68e9e" } librespot-core = { git = "https://github.com/librespot-org/librespot.git" }
librespot-metadata = { git = "https://github.com/librespot-org/librespot.git", rev = "a3c63b4e055f3ec68432d4a27479bed102e68e9e" } librespot-metadata = { git = "https://github.com/librespot-org/librespot.git" }
librespot-audio = { git = "https://github.com/librespot-org/librespot.git", rev = "a3c63b4e055f3ec68432d4a27479bed102e68e9e" } librespot-audio = { git = "https://github.com/librespot-org/librespot.git" }
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"

View File

@ -1,6 +1,6 @@
MIT License 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 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

View File

@ -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. 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 "spotify-premium-user" "spotify-premium-password" < tracks_list oggify user 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,

32
src/main.rs 100755 → 100644
View File

@ -10,7 +10,6 @@ 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;
@ -29,7 +28,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 || 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 mut core = Core::new().unwrap();
let handle = core.handle(); let handle = core.handle();
@ -54,12 +53,6 @@ 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());
@ -74,10 +67,12 @@ 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_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_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();
@ -96,24 +91,7 @@ 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");
}
}
}); });
} }

17
tag_ogg
View File

@ -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}"