Compare commits

..

18 Commits

Author SHA1 Message Date
Adriel Sand 5a027fa538 Removed unnecessary lines 2024-06-16 16:43:42 -07:00
Adriel Sand fe66ff977a Added existing file checking and other fixes 2024-06-16 16:36:32 -07:00
Lorenzo Pistone f11fb48ce7
Merge pull request #3 from danieleds/patch-1
Fix revision of librespot dependency
2021-12-29 19:58:26 +01:00
Daniel Di Sarli 9deaba0e63
Fix revision of librespot dependency 2021-12-18 17:42:18 +01:00
Lorenzo Pistone eb82e3c5b9
cleanup tag_ogg 2019-02-19 13:05:38 +01:00
Lorenzo Pistone ba672d1f91
tag_ogg: print name after tagging 2019-02-19 13:03:50 +01:00
Lorenzo Pistone a805996ee0
Update README.md 2019-02-19 13:02:15 +01:00
Lorenzo Pistone 02f1c17da9 Merge branch 'master' of github.com:pisto/oggify 2019-02-19 13:00:45 +01:00
Lorenzo Pistone f7b0e2f17d fix trailing whitespace in vorbis comments 2019-02-19 13:00:36 +01:00
Lorenzo Pistone db389c18aa
Update LICENSE 2019-02-18 17:50:22 +01:00
Lorenzo Pistone 6d11933999 Merge branch 'master' of github.com:pisto/oggify 2019-02-18 09:56:29 +01:00
Lorenzo Pistone 264538e3ab print track filename in tag_ogg 2019-02-18 09:56:19 +01:00
Lorenzo Pistone 9d76391b17
Do not strip \0 in filenames
unlikely that strings in bash can hold them in any case
2019-02-17 02:02:35 +01:00
Lorenzo Pistone 99f61a7e85 cleanup 2019-02-14 17:55:29 +01:00
Lorenzo Pistone 124c08cfb8 readme updates 2019-02-14 16:46:06 +01:00
Lorenzo Pistone 348f885920 Also give album name and spotify id to the helper script 2019-02-14 16:45:55 +01:00
Lorenzo Pistone ffd5b407e9 tag_ogg helper script 2019-02-14 16:07:21 +01:00
Lorenzo Pistone 9ad4ee771c add support for helper script 2019-02-14 16:07:11 +01:00
5 changed files with 103 additions and 44 deletions

View File

@ -11,9 +11,9 @@ readme = "README.md"
[dependencies]
tokio-core = "0.1.17"
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" }
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" }
regex = "1.1.0"
log = "0.4.6"
env_logger = "0.6.0"

View File

@ -1,6 +1,6 @@
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
of this software and associated documentation files (the "Software"), to deal

View File

@ -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.
# 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,

98
src/main.rs 100644 → 100755
View File

@ -10,6 +10,7 @@ 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;
@ -28,7 +29,7 @@ fn main() {
Builder::from_env(Env::default().default_filter_or("info")).init();
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 handle = core.handle();
@ -53,45 +54,66 @@ fn main() {
.and_then(|capture|SpotifyId::from_base62(&capture[1]).ok())))
.for_each(|id|{
info!("Getting track {}...", id.to_base62());
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());
let alt_track = track.alternatives.iter().find_map(|id|{
let alt_track = core.run(Track::get(&session, *id)).expect("Cannot get track metadata");
match alt_track.available {
true => Some(alt_track),
false => None
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());
let alt_track = track.alternatives.iter().find_map(|id|{
let alt_track = core.run(Track::get(&session, *id)).expect("Cannot get track metadata");
match alt_track.available {
true => Some(alt_track),
false => None
}
});
track = alt_track.expect(&format!("Could not find alternative for 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();
debug!("File formats: {}", track.files.keys().map(|filetype|format!("{:?}", filetype)).collect::<Vec<_>>().join(" "));
let file_id = 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();
let mut buffer = Vec::new();
let mut read_all: Result<usize> = Ok(0);
let fetched = AtomicBool::new(false);
threadpool.scoped(|scope|{
scope.execute(||{
read_all = encrypted_file.read_to_end(&mut buffer);
fetched.store(true, Ordering::Release);
});
while !fetched.load(Ordering::Acquire) {
core.turn(Some(Duration::from_millis(100)));
}
});
track = alt_track.expect(&format!("Could not find alternative for 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_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_320)
.or(track.files.get(&FileFormat::OGG_VORBIS_160))
.or(track.files.get(&FileFormat::OGG_VORBIS_96))
.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();
let mut buffer = Vec::new();
let mut read_all: Result<usize> = Ok(0);
let fetched = AtomicBool::new(false);
threadpool.scoped(|scope|{
scope.execute(||{
read_all = encrypted_file.read_to_end(&mut buffer);
fetched.store(true, Ordering::Release);
});
while !fetched.load(Ordering::Acquire) {
core.turn(Some(Duration::from_millis(100)));
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");
}
});
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");
std::fs::write(&fname, &decrypted_buffer[0xa7..]).expect("Cannot write decrypted track");
info!("Filename: {}", fname);
}
});
}

17
tag_ogg 100755
View File

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